Convert polygerrit to es6-modules This change replace all HTML imports with es6-modules. The only exceptions are: * gr-app.html file, which can be deleted only after updating the gerrit/httpd/raw/PolyGerritIndexHtml.soy file. * dark-theme.html which is loaded via importHref. Must be updated manually later in a separate change. This change was produced automatically by ./es6-modules-converter.sh script. No manual changes were made. Change-Id: I0c447dd8c05757741e2c940720652d01d9fb7d67
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js index 66c00f9..6d9f9d7 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 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. @@ -14,106 +14,118 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js'; - const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g; +import '../../../scripts/bundled-polymer.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-avatar/gr-avatar.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-dropdown_html.js'; - /** - * @appliesMixin Gerrit.DisplayNameMixin - * @extends Polymer.Element - */ - class GrAccountDropdown extends Polymer.mixinBehaviors( [ - Gerrit.DisplayNameBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-account-dropdown'; } +const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g; - static get properties() { - return { - account: Object, - config: Object, - links: { - type: Array, - computed: '_getLinks(_switchAccountUrl, _path)', - }, - topContent: { - type: Array, - computed: '_getTopContent(account)', - }, - _path: { - type: String, - value: '/', - }, - _hasAvatars: Boolean, - _switchAccountUrl: String, - }; - } +/** + * @appliesMixin Gerrit.DisplayNameMixin + * @extends Polymer.Element + */ +class GrAccountDropdown extends mixinBehaviors( [ + Gerrit.DisplayNameBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this._handleLocationChange(); - this.listen(window, 'location-change', '_handleLocationChange'); - this.$.restAPI.getConfig().then(cfg => { - this.config = cfg; + static get is() { return 'gr-account-dropdown'; } - if (cfg && cfg.auth && cfg.auth.switch_account_url) { - this._switchAccountUrl = cfg.auth.switch_account_url; - } else { - this._switchAccountUrl = ''; - } - this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); - }); - } - - /** @override */ - detached() { - super.detached(); - this.unlisten(window, 'location-change', '_handleLocationChange'); - } - - _getLinks(switchAccountUrl, path) { - // Polymer 2: check for undefined - if ([switchAccountUrl, path].some(arg => arg === undefined)) { - return undefined; - } - - const links = [{name: 'Settings', url: '/settings/'}]; - if (switchAccountUrl) { - const replacements = {path}; - const url = this._interpolateUrl(switchAccountUrl, replacements); - links.push({name: 'Switch account', url, external: true}); - } - links.push({name: 'Sign out', url: '/logout'}); - return links; - } - - _getTopContent(account) { - return [ - {text: this._accountName(account), bold: true}, - {text: account.email ? account.email : ''}, - ]; - } - - _handleLocationChange() { - this._path = - window.location.pathname + - window.location.search + - window.location.hash; - } - - _interpolateUrl(url, replacements) { - return url.replace( - INTERPOLATE_URL_PATTERN, - (match, p1) => replacements[p1] || ''); - } - - _accountName(account) { - return this.getUserName(this.config, account, true); - } + static get properties() { + return { + account: Object, + config: Object, + links: { + type: Array, + computed: '_getLinks(_switchAccountUrl, _path)', + }, + topContent: { + type: Array, + computed: '_getTopContent(account)', + }, + _path: { + type: String, + value: '/', + }, + _hasAvatars: Boolean, + _switchAccountUrl: String, + }; } - customElements.define(GrAccountDropdown.is, GrAccountDropdown); -})(); + /** @override */ + attached() { + super.attached(); + this._handleLocationChange(); + this.listen(window, 'location-change', '_handleLocationChange'); + this.$.restAPI.getConfig().then(cfg => { + this.config = cfg; + + if (cfg && cfg.auth && cfg.auth.switch_account_url) { + this._switchAccountUrl = cfg.auth.switch_account_url; + } else { + this._switchAccountUrl = ''; + } + this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); + }); + } + + /** @override */ + detached() { + super.detached(); + this.unlisten(window, 'location-change', '_handleLocationChange'); + } + + _getLinks(switchAccountUrl, path) { + // Polymer 2: check for undefined + if ([switchAccountUrl, path].some(arg => arg === undefined)) { + return undefined; + } + + const links = [{name: 'Settings', url: '/settings/'}]; + if (switchAccountUrl) { + const replacements = {path}; + const url = this._interpolateUrl(switchAccountUrl, replacements); + links.push({name: 'Switch account', url, external: true}); + } + links.push({name: 'Sign out', url: '/logout'}); + return links; + } + + _getTopContent(account) { + return [ + {text: this._accountName(account), bold: true}, + {text: account.email ? account.email : ''}, + ]; + } + + _handleLocationChange() { + this._path = + window.location.pathname + + window.location.search + + window.location.hash; + } + + _interpolateUrl(url, replacements) { + return url.replace( + INTERPOLATE_URL_PATTERN, + (match, p1) => replacements[p1] || ''); + } + + _accountName(account) { + return this.getUserName(this.config, account, true); + } +} + +customElements.define(GrAccountDropdown.is, GrAccountDropdown);
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js index 5152ef9..e22db65 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -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="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.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-dropdown/gr-dropdown.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-avatar/gr-avatar.html"> - -<dom-module id="gr-account-dropdown"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> gr-dropdown { padding: 0 var(--spacing-m); @@ -41,16 +33,9 @@ vertical-align: middle; } </style> - <gr-dropdown - link - items=[[links]] - top-content=[[topContent]] - horizontal-align="right"> - <span hidden$="[[_hasAvatars]]" hidden>[[_accountName(account)]]</span> - <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden - image-size="56" aria-label="Account avatar"></gr-avatar> + <gr-dropdown link="" items="[[links]]" top-content="[[topContent]]" horizontal-align="right"> + <span hidden\$="[[_hasAvatars]]" hidden="">[[_accountName(account)]]</span> + <gr-avatar account="[[account]]" hidden\$="[[!_hasAvatars]]" hidden="" image-size="56" aria-label="Account avatar"></gr-avatar> </gr-dropdown> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-account-dropdown.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html index 0a11df1..fa0c7a7 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-dropdown</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-account-dropdown.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-account-dropdown.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-dropdown.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,93 +40,95 @@ </template> </test-fixture> -<script> - suite('gr-account-dropdown tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-dropdown.js'; +suite('gr-account-dropdown tests', () => { + let element; - setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - }); - element = fixture('basic'); + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + }); + + test('account information', () => { + element.account = {name: 'John Doe', email: 'john@doe.com'}; + assert.deepEqual(element.topContent, + [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]); + }); + + test('test for account without a name', () => { + element.account = {id: '0001'}; + assert.deepEqual(element.topContent, + [{text: 'Anonymous', bold: true}, {text: ''}]); + }); + + test('test for account without a name but using config', () => { + element.config = { + user: { + anonymous_coward_name: 'WikiGerrit', + }, + }; + element.account = {id: '0001'}; + assert.deepEqual(element.topContent, + [{text: 'WikiGerrit', bold: true}, {text: ''}]); + }); + + test('test for account name as an email', () => { + element.config = { + user: { + anonymous_coward_name: 'WikiGerrit', + }, + }; + element.account = {email: 'john@doe.com'}; + assert.deepEqual(element.topContent, + [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]); + }); + + test('switch account', () => { + // Missing params. + assert.isUndefined(element._getLinks()); + assert.isUndefined(element._getLinks(null)); + + // No switch account link. + assert.equal(element._getLinks(null, '').length, 2); + + // Unparameterized switch account link. + let links = element._getLinks('/switch-account', ''); + assert.equal(links.length, 3); + assert.deepEqual(links[1], { + name: 'Switch account', + url: '/switch-account', + external: true, }); - test('account information', () => { - element.account = {name: 'John Doe', email: 'john@doe.com'}; - assert.deepEqual(element.topContent, - [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]); - }); - - test('test for account without a name', () => { - element.account = {id: '0001'}; - assert.deepEqual(element.topContent, - [{text: 'Anonymous', bold: true}, {text: ''}]); - }); - - test('test for account without a name but using config', () => { - element.config = { - user: { - anonymous_coward_name: 'WikiGerrit', - }, - }; - element.account = {id: '0001'}; - assert.deepEqual(element.topContent, - [{text: 'WikiGerrit', bold: true}, {text: ''}]); - }); - - test('test for account name as an email', () => { - element.config = { - user: { - anonymous_coward_name: 'WikiGerrit', - }, - }; - element.account = {email: 'john@doe.com'}; - assert.deepEqual(element.topContent, - [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]); - }); - - test('switch account', () => { - // Missing params. - assert.isUndefined(element._getLinks()); - assert.isUndefined(element._getLinks(null)); - - // No switch account link. - assert.equal(element._getLinks(null, '').length, 2); - - // Unparameterized switch account link. - let links = element._getLinks('/switch-account', ''); - assert.equal(links.length, 3); - assert.deepEqual(links[1], { - name: 'Switch account', - url: '/switch-account', - external: true, - }); - - // Parameterized switch account link. - links = element._getLinks('/switch-account${path}', '/c/123'); - assert.equal(links.length, 3); - assert.deepEqual(links[1], { - name: 'Switch account', - url: '/switch-account/c/123', - external: true, - }); - }); - - test('_interpolateUrl', () => { - const replacements = { - foo: 'bar', - test: 'TEST', - }; - const interpolate = function(url) { - return element._interpolateUrl(url, replacements); - }; - - assert.equal(interpolate('test'), 'test'); - assert.equal(interpolate('${test}'), 'TEST'); - assert.equal( - interpolate('${}, ${test}, ${TEST}, ${foo}'), - '${}, TEST, , bar'); + // Parameterized switch account link. + links = element._getLinks('/switch-account${path}', '/c/123'); + assert.equal(links.length, 3); + assert.deepEqual(links[1], { + name: 'Switch account', + url: '/switch-account/c/123', + external: true, }); }); + + test('_interpolateUrl', () => { + const replacements = { + foo: 'bar', + test: 'TEST', + }; + const interpolate = function(url) { + return element._interpolateUrl(url, replacements); + }; + + assert.equal(interpolate('test'), 'test'); + assert.equal(interpolate('${test}'), 'TEST'); + assert.equal( + interpolate('${}, ${test}, ${TEST}, ${foo}'), + '${}, TEST, , bar'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js index 63339c9..6814d89 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -14,44 +14,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrErrorDialog extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-error-dialog'; } - /** - * Fired when the dismiss button is pressed. - * - * @event dismiss - */ +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-error-dialog_html.js'; - static get properties() { - return { - text: String, - /** - * loginUrl to open on "sign in" button click - */ - loginUrl: { - type: String, - value: '/login', - }, - /** - * Show/hide "Sign In" button in dialog - */ - showSignInButton: { - type: Boolean, - value: false, - }, - }; - } +/** @extends Polymer.Element */ +class GrErrorDialog extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _handleConfirm() { - this.dispatchEvent(new CustomEvent('dismiss')); - } + static get is() { return 'gr-error-dialog'; } + /** + * Fired when the dismiss button is pressed. + * + * @event dismiss + */ + + static get properties() { + return { + text: String, + /** + * loginUrl to open on "sign in" button click + */ + loginUrl: { + type: String, + value: '/login', + }, + /** + * Show/hide "Sign In" button in dialog + */ + showSignInButton: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrErrorDialog.is, GrErrorDialog); -})(); + _handleConfirm() { + this.dispatchEvent(new CustomEvent('dismiss')); + } +} + +customElements.define(GrErrorDialog.is, GrErrorDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js index ffd7f896..e18d1bd 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
@@ -1,26 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -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-dialog/gr-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-error-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .main { max-height: 40em; @@ -38,23 +34,11 @@ text-decoration: none; } </style> - <gr-dialog - id="dialog" - cancel-label="" - on-confirm="_handleConfirm" - confirm-label="Dismiss" - confirm-on-enter> + <gr-dialog id="dialog" cancel-label="" on-confirm="_handleConfirm" confirm-label="Dismiss" confirm-on-enter=""> <div class="header" slot="header">An error occurred</div> <div class="main" slot="main">[[text]]</div> - <gr-button - id="signIn" - class$="signInLink" - hidden$="[[!showSignInButton]]" - link - slot="footer"> - <a href$="[[loginUrl]]" class="signInLink">Sign in</a> + <gr-button id="signIn" class\$="signInLink" hidden\$="[[!showSignInButton]]" link="" slot="footer"> + <a href\$="[[loginUrl]]" class="signInLink">Sign in</a> </gr-button> </gr-dialog> - </template> - <script src="gr-error-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html index c87f8bb..296c6f0 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-error-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-error-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-error-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-error-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,18 +40,20 @@ </template> </test-fixture> -<script> - suite('gr-error-dialog tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-error-dialog.js'; +suite('gr-error-dialog tests', () => { + let element; - setup(() => { - element = fixture('basic'); - }); - - test('dismiss tap fires event', done => { - element.addEventListener('dismiss', () => { done(); }); - MockInteractions.tap(element.$.dialog.$.confirm); - }); + setup(() => { + element = fixture('basic'); }); + + test('dismiss tap fires event', done => { + element.addEventListener('dismiss', () => { done(); }); + MockInteractions.tap(element.$.dialog.$.confirm); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js index b828774..e2284a9 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,384 +14,405 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +/* Import to get Gerrit interface */ +/* TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface */ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - const HIDE_ALERT_TIMEOUT_MS = 5000; - const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000; - const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000; - const SIGN_IN_WIDTH_PX = 690; - const SIGN_IN_HEIGHT_PX = 500; - const TOO_MANY_FILES = 'too many files to find conflicts'; - const AUTHENTICATION_REQUIRED = 'Authentication required\n'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../gr-error-dialog/gr-error-dialog.js'; +import '../gr-reporting/gr-reporting.js'; +import '../../shared/gr-alert/gr-alert.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-error-manager_html.js'; + +const HIDE_ALERT_TIMEOUT_MS = 5000; +const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000; +const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000; +const SIGN_IN_WIDTH_PX = 690; +const SIGN_IN_HEIGHT_PX = 500; +const TOO_MANY_FILES = 'too many files to find conflicts'; +const AUTHENTICATION_REQUIRED = 'Authentication required\n'; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrErrorManager extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-error-manager'; } + + static get properties() { + return { + /** + * The ID of the account that was logged in when the app was launched. If + * not set, then there was no account at launch. + */ + knownAccountId: Number, + + /** @type {?Object} */ + _alertElement: Object, + /** @type {?number} */ + _hideAlertHandle: Number, + _refreshingCredentials: { + type: Boolean, + value: false, + }, + + /** + * The time (in milliseconds) since the most recent credential check. + */ + _lastCredentialCheck: { + type: Number, + value() { return Date.now(); }, + }, + + loginUrl: { + type: String, + value: '/login', + }, + }; + } + + constructor() { + super(); + + /** @type {!Gerrit.Auth} */ + this._authService = Gerrit.Auth; + + /** @type {?Function} */ + this._authErrorHandlerDeregistrationHook; + } + + /** @override */ + attached() { + super.attached(); + this.listen(document, 'server-error', '_handleServerError'); + this.listen(document, 'network-error', '_handleNetworkError'); + this.listen(document, 'show-alert', '_handleShowAlert'); + this.listen(document, 'show-error', '_handleShowErrorDialog'); + this.listen(document, 'visibilitychange', '_handleVisibilityChange'); + this.listen(document, 'show-auth-required', '_handleAuthRequired'); + + this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error', + event => { + this._handleAuthError(event.message, event.action); + }); + } + + /** @override */ + detached() { + super.detached(); + this._clearHideAlertHandle(); + this.unlisten(document, 'server-error', '_handleServerError'); + this.unlisten(document, 'network-error', '_handleNetworkError'); + this.unlisten(document, 'show-auth-required', '_handleAuthRequired'); + this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); + this.unlisten(document, 'show-error', '_handleShowErrorDialog'); + + this._authErrorHandlerDeregistrationHook(); + } + + _shouldSuppressError(msg) { + return msg.includes(TOO_MANY_FILES); + } + + _handleAuthRequired() { + this._showAuthErrorAlert( + 'Log in is required to perform that action.', 'Log in.'); + } + + _handleAuthError(msg, action) { + this.$.noInteractionOverlay.open().then(() => { + this._showAuthErrorAlert(msg, action); + }); + } + + _handleServerError(e) { + const {request, response} = e.detail; + response.text().then(errorText => { + const url = request && (request.anonymizedUrl || request.url); + const {status, statusText} = response; + if (response.status === 403 + && !this._authService.isAuthed + && errorText === AUTHENTICATION_REQUIRED) { + // if not authed previously, this is trying to access auth required APIs + // show auth required alert + this._handleAuthRequired(); + } else if (response.status === 403 + && this._authService.isAuthed + && errorText === AUTHENTICATION_REQUIRED) { + // The app was logged at one point and is now getting auth errors. + // This indicates the auth token may no longer valid. + // Re-check on auth + this._authService.clearCache(); + this.$.restAPI.getLoggedIn(); + } else if (!this._shouldSuppressError(errorText)) { + const trace = + response.headers && response.headers.get('X-Gerrit-Trace'); + if (response.status === 404) { + this._showNotFoundMessageWithTip({ + status, + statusText, + errorText, + url, + trace, + }); + } else { + this._showErrorDialog(this._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + trace, + })); + } + } + console.log(`server error: ${errorText}`); + }); + } + + _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) { + this.$.restAPI.getLoggedIn().then(isLoggedIn => { + const tip = isLoggedIn ? + 'You might have not enough privileges.' : + 'You might have not enough privileges. Sign in and try again.'; + this._showErrorDialog(this._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + trace, + tip, + }), { + showSignInButton: !isLoggedIn, + }); + }); + return; + } + + _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) { + let err = ''; + if (tip) { + err += `${tip}\n\n`; + } + err += `Error ${status}`; + if (statusText) { err += ` (${statusText})`; } + if (errorText || url) { err += ': '; } + if (errorText) { err += errorText; } + if (url) { err += `\nEndpoint: ${url}`; } + if (trace) { err += `\nTrace Id: ${trace}`; } + return err; + } + + _handleShowAlert(e) { + this._showAlert(e.detail.message, e.detail.action, e.detail.callback, + e.detail.dismissOnNavigation); + } + + _handleNetworkError(e) { + this._showAlert('Server unavailable'); + console.error(e.detail.error.message); + } /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * @param {string} text + * @param {?string=} opt_actionText + * @param {?Function=} opt_actionCallback + * @param {?boolean=} opt_dismissOnNavigation */ - class GrErrorManager extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-error-manager'; } - - static get properties() { - return { - /** - * The ID of the account that was logged in when the app was launched. If - * not set, then there was no account at launch. - */ - knownAccountId: Number, - - /** @type {?Object} */ - _alertElement: Object, - /** @type {?number} */ - _hideAlertHandle: Number, - _refreshingCredentials: { - type: Boolean, - value: false, - }, - - /** - * The time (in milliseconds) since the most recent credential check. - */ - _lastCredentialCheck: { - type: Number, - value() { return Date.now(); }, - }, - - loginUrl: { - type: String, - value: '/login', - }, - }; - } - - constructor() { - super(); - - /** @type {!Gerrit.Auth} */ - this._authService = Gerrit.Auth; - - /** @type {?Function} */ - this._authErrorHandlerDeregistrationHook; - } - - /** @override */ - attached() { - super.attached(); - this.listen(document, 'server-error', '_handleServerError'); - this.listen(document, 'network-error', '_handleNetworkError'); - this.listen(document, 'show-alert', '_handleShowAlert'); - this.listen(document, 'show-error', '_handleShowErrorDialog'); - this.listen(document, 'visibilitychange', '_handleVisibilityChange'); - this.listen(document, 'show-auth-required', '_handleAuthRequired'); - - this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error', - event => { - this._handleAuthError(event.message, event.action); - }); - } - - /** @override */ - detached() { - super.detached(); - this._clearHideAlertHandle(); - this.unlisten(document, 'server-error', '_handleServerError'); - this.unlisten(document, 'network-error', '_handleNetworkError'); - this.unlisten(document, 'show-auth-required', '_handleAuthRequired'); - this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); - this.unlisten(document, 'show-error', '_handleShowErrorDialog'); - - this._authErrorHandlerDeregistrationHook(); - } - - _shouldSuppressError(msg) { - return msg.includes(TOO_MANY_FILES); - } - - _handleAuthRequired() { - this._showAuthErrorAlert( - 'Log in is required to perform that action.', 'Log in.'); - } - - _handleAuthError(msg, action) { - this.$.noInteractionOverlay.open().then(() => { - this._showAuthErrorAlert(msg, action); - }); - } - - _handleServerError(e) { - const {request, response} = e.detail; - response.text().then(errorText => { - const url = request && (request.anonymizedUrl || request.url); - const {status, statusText} = response; - if (response.status === 403 - && !this._authService.isAuthed - && errorText === AUTHENTICATION_REQUIRED) { - // if not authed previously, this is trying to access auth required APIs - // show auth required alert - this._handleAuthRequired(); - } else if (response.status === 403 - && this._authService.isAuthed - && errorText === AUTHENTICATION_REQUIRED) { - // The app was logged at one point and is now getting auth errors. - // This indicates the auth token may no longer valid. - // Re-check on auth - this._authService.clearCache(); - this.$.restAPI.getLoggedIn(); - } else if (!this._shouldSuppressError(errorText)) { - const trace = - response.headers && response.headers.get('X-Gerrit-Trace'); - if (response.status === 404) { - this._showNotFoundMessageWithTip({ - status, - statusText, - errorText, - url, - trace, - }); - } else { - this._showErrorDialog(this._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - trace, - })); - } - } - console.log(`server error: ${errorText}`); - }); - } - - _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) { - this.$.restAPI.getLoggedIn().then(isLoggedIn => { - const tip = isLoggedIn ? - 'You might have not enough privileges.' : - 'You might have not enough privileges. Sign in and try again.'; - this._showErrorDialog(this._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - trace, - tip, - }), { - showSignInButton: !isLoggedIn, - }); - }); - return; - } - - _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) { - let err = ''; - if (tip) { - err += `${tip}\n\n`; - } - err += `Error ${status}`; - if (statusText) { err += ` (${statusText})`; } - if (errorText || url) { err += ': '; } - if (errorText) { err += errorText; } - if (url) { err += `\nEndpoint: ${url}`; } - if (trace) { err += `\nTrace Id: ${trace}`; } - return err; - } - - _handleShowAlert(e) { - this._showAlert(e.detail.message, e.detail.action, e.detail.callback, - e.detail.dismissOnNavigation); - } - - _handleNetworkError(e) { - this._showAlert('Server unavailable'); - console.error(e.detail.error.message); - } - - /** - * @param {string} text - * @param {?string=} opt_actionText - * @param {?Function=} opt_actionCallback - * @param {?boolean=} opt_dismissOnNavigation - */ - _showAlert(text, opt_actionText, opt_actionCallback, - opt_dismissOnNavigation) { - if (this._alertElement) { - // do not override auth alerts - if (this._alertElement.type === 'AUTH') return; - this._hideAlert(); - } - - this._clearHideAlertHandle(); - if (opt_dismissOnNavigation) { - // Persist alert until navigation. - this.listen(document, 'location-change', '_hideAlert'); - } else { - this._hideAlertHandle = - this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS); - } - const el = this._createToastAlert(); - el.show(text, opt_actionText, opt_actionCallback); - this._alertElement = el; - } - - _hideAlert() { - if (!this._alertElement) { return; } - - this._alertElement.hide(); - this._alertElement = null; - - // Remove listener for page navigation, if it exists. - this.unlisten(document, 'location-change', '_hideAlert'); - } - - _clearHideAlertHandle() { - if (this._hideAlertHandle != null) { - this.cancelAsync(this._hideAlertHandle); - this._hideAlertHandle = null; - } - } - - _showAuthErrorAlert(errorText, actionText) { - // hide any existing alert like `reload` - // as auth error should have the highest priority - if (this._alertElement) { - this._alertElement.hide(); - } - - this._alertElement = this._createToastAlert(); - this._alertElement.type = 'AUTH'; - this._alertElement.show(errorText, actionText, - this._createLoginPopup.bind(this)); - - this._refreshingCredentials = true; - this._requestCheckLoggedIn(); - if (!document.hidden) { - this._handleVisibilityChange(); - } - } - - _createToastAlert() { - const el = document.createElement('gr-alert'); - el.toast = true; - return el; - } - - _handleVisibilityChange() { - // Ignore when the page is transitioning to hidden (or hidden is - // undefined). - if (document.hidden !== false) { return; } - - // If not currently refreshing credentials and the credentials are old, - // request them to confirm their validity or (display an auth toast if it - // fails). - const timeSinceLastCheck = Date.now() - this._lastCredentialCheck; - if (!this._refreshingCredentials && - this.knownAccountId !== undefined && - timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) { - this._lastCredentialCheck = Date.now(); - - // check auth status in case: - // - user signed out - // - user switched account - this._checkSignedIn(); - } - } - - _requestCheckLoggedIn() { - this.debounce( - 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS); - } - - _checkSignedIn() { - this._lastCredentialCheck = Date.now(); - - // force to refetch account info - this.$.restAPI.invalidateAccountsCache(); - this._authService.clearCache(); - - this.$.restAPI.getLoggedIn().then(isLoggedIn => { - // do nothing if its refreshing - if (!this._refreshingCredentials) return; - - if (!isLoggedIn) { - // check later - // 1. guest mode - // 2. or signed out - // in case #2, auth-error is taken care of separately - this._requestCheckLoggedIn(); - } else { - // check account - this.$.restAPI.getAccount().then(account => { - if (this._refreshingCredentials) { - // If the credentials were refreshed but the account is different - // then reload the page completely. - if (account._account_id !== this.knownAccountId) { - this._reloadPage(); - return; - } - - this._handleCredentialRefreshed(); - } - }); - } - }); - } - - _reloadPage() { - window.location.reload(); - } - - _createLoginPopup() { - const left = window.screenLeft + - (window.outerWidth - SIGN_IN_WIDTH_PX) / 2; - const top = window.screenTop + - (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2; - const options = [ - 'width=' + SIGN_IN_WIDTH_PX, - 'height=' + SIGN_IN_HEIGHT_PX, - 'left=' + left, - 'top=' + top, - ]; - window.open(this.getBaseUrl() + - '/login/%3FcloseAfterLogin', '_blank', options.join(',')); - this.listen(window, 'focus', '_handleWindowFocus'); - } - - _handleCredentialRefreshed() { - this.unlisten(window, 'focus', '_handleWindowFocus'); - this._refreshingCredentials = false; + _showAlert(text, opt_actionText, opt_actionCallback, + opt_dismissOnNavigation) { + if (this._alertElement) { + // do not override auth alerts + if (this._alertElement.type === 'AUTH') return; this._hideAlert(); - this._showAlert('Credentials refreshed.'); - this.$.noInteractionOverlay.close(); - - // Clear the cache for auth - this._authService.clearCache(); } - _handleWindowFocus() { - this.flushDebouncer('checkLoggedIn'); + this._clearHideAlertHandle(); + if (opt_dismissOnNavigation) { + // Persist alert until navigation. + this.listen(document, 'location-change', '_hideAlert'); + } else { + this._hideAlertHandle = + this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS); } + const el = this._createToastAlert(); + el.show(text, opt_actionText, opt_actionCallback); + this._alertElement = el; + } - _handleShowErrorDialog(e) { - this._showErrorDialog(e.detail.message); - } + _hideAlert() { + if (!this._alertElement) { return; } - _handleDismissErrorDialog() { - this.$.errorOverlay.close(); - } + this._alertElement.hide(); + this._alertElement = null; - _showErrorDialog(message, opt_options) { - this.$.reporting.reportErrorDialog(message); - this.$.errorDialog.text = message; - this.$.errorDialog.showSignInButton = - opt_options && opt_options.showSignInButton; - this.$.errorOverlay.open(); + // Remove listener for page navigation, if it exists. + this.unlisten(document, 'location-change', '_hideAlert'); + } + + _clearHideAlertHandle() { + if (this._hideAlertHandle != null) { + this.cancelAsync(this._hideAlertHandle); + this._hideAlertHandle = null; } } - customElements.define(GrErrorManager.is, GrErrorManager); -})(); + _showAuthErrorAlert(errorText, actionText) { + // hide any existing alert like `reload` + // as auth error should have the highest priority + if (this._alertElement) { + this._alertElement.hide(); + } + + this._alertElement = this._createToastAlert(); + this._alertElement.type = 'AUTH'; + this._alertElement.show(errorText, actionText, + this._createLoginPopup.bind(this)); + + this._refreshingCredentials = true; + this._requestCheckLoggedIn(); + if (!document.hidden) { + this._handleVisibilityChange(); + } + } + + _createToastAlert() { + const el = document.createElement('gr-alert'); + el.toast = true; + return el; + } + + _handleVisibilityChange() { + // Ignore when the page is transitioning to hidden (or hidden is + // undefined). + if (document.hidden !== false) { return; } + + // If not currently refreshing credentials and the credentials are old, + // request them to confirm their validity or (display an auth toast if it + // fails). + const timeSinceLastCheck = Date.now() - this._lastCredentialCheck; + if (!this._refreshingCredentials && + this.knownAccountId !== undefined && + timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) { + this._lastCredentialCheck = Date.now(); + + // check auth status in case: + // - user signed out + // - user switched account + this._checkSignedIn(); + } + } + + _requestCheckLoggedIn() { + this.debounce( + 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS); + } + + _checkSignedIn() { + this._lastCredentialCheck = Date.now(); + + // force to refetch account info + this.$.restAPI.invalidateAccountsCache(); + this._authService.clearCache(); + + this.$.restAPI.getLoggedIn().then(isLoggedIn => { + // do nothing if its refreshing + if (!this._refreshingCredentials) return; + + if (!isLoggedIn) { + // check later + // 1. guest mode + // 2. or signed out + // in case #2, auth-error is taken care of separately + this._requestCheckLoggedIn(); + } else { + // check account + this.$.restAPI.getAccount().then(account => { + if (this._refreshingCredentials) { + // If the credentials were refreshed but the account is different + // then reload the page completely. + if (account._account_id !== this.knownAccountId) { + this._reloadPage(); + return; + } + + this._handleCredentialRefreshed(); + } + }); + } + }); + } + + _reloadPage() { + window.location.reload(); + } + + _createLoginPopup() { + const left = window.screenLeft + + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2; + const top = window.screenTop + + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2; + const options = [ + 'width=' + SIGN_IN_WIDTH_PX, + 'height=' + SIGN_IN_HEIGHT_PX, + 'left=' + left, + 'top=' + top, + ]; + window.open(this.getBaseUrl() + + '/login/%3FcloseAfterLogin', '_blank', options.join(',')); + this.listen(window, 'focus', '_handleWindowFocus'); + } + + _handleCredentialRefreshed() { + this.unlisten(window, 'focus', '_handleWindowFocus'); + this._refreshingCredentials = false; + this._hideAlert(); + this._showAlert('Credentials refreshed.'); + this.$.noInteractionOverlay.close(); + + // Clear the cache for auth + this._authService.clearCache(); + } + + _handleWindowFocus() { + this.flushDebouncer('checkLoggedIn'); + } + + _handleShowErrorDialog(e) { + this._showErrorDialog(e.detail.message); + } + + _handleDismissErrorDialog() { + this.$.errorOverlay.close(); + } + + _showErrorDialog(message, opt_options) { + this.$.reporting.reportErrorDialog(message); + this.$.errorDialog.text = message; + this.$.errorDialog.showSignInButton = + opt_options && opt_options.showSignInButton; + this.$.errorOverlay.open(); + } +} + +customElements.define(GrErrorManager.is, GrErrorManager);
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js index 104d5b0..5661d1e 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
@@ -1,52 +1,27 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -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="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../core/gr-error-dialog/gr-error-dialog.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-alert/gr-alert.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"> -<!-- Import to get Gerrit interface --> -<!-- TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface --> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-error-manager"> - <template> - <gr-overlay with-backdrop id="errorOverlay"> - <gr-error-dialog - id="errorDialog" - on-dismiss="_handleDismissErrorDialog" - confirm-label="Dismiss" - confirm-on-enter - login-url="[[loginUrl]]" - ></gr-error-dialog> +export const htmlTemplate = html` + <gr-overlay with-backdrop="" id="errorOverlay"> + <gr-error-dialog id="errorDialog" on-dismiss="_handleDismissErrorDialog" confirm-label="Dismiss" confirm-on-enter="" login-url="[[loginUrl]]"></gr-error-dialog> </gr-overlay> - <gr-overlay - id="noInteractionOverlay" - with-backdrop - always-on-top - no-cancel-on-esc-key - no-cancel-on-outside-click> + <gr-overlay id="noInteractionOverlay" with-backdrop="" always-on-top="" no-cancel-on-esc-key="" no-cancel-on-outside-click=""> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-error-manager.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html index e984577..a4a9a7d 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -19,14 +19,18 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-error-manager</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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="gr-error-manager.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-error-manager.js"></script> -<script>void (0);</script> +<script type="module"> +import '../../../test/common-test-setup.js'; +import './gr-error-manager.js'; +void (0); +</script> <test-fixture id="basic"> <template> @@ -34,465 +38,467 @@ </template> </test-fixture> -<script> - suite('gr-error-manager tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/common-test-setup.js'; +import './gr-error-manager.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-error-manager tests', () => { + let element; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('when authed', () => { setup(() => { - sandbox = sinon.sandbox.create(); + sandbox.stub(window, 'fetch') + .returns(Promise.resolve({ok: true, status: 204})); + element = fixture('basic'); + element._authService.clearCache(); }); - teardown(() => { - sandbox.restore(); + test('does not show auth error on 403 by default', done => { + const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert'); + const responseText = Promise.resolve('server says no.'); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + flush(() => { + assert.isFalse(showAuthErrorStub.calledOnce); + done(); + }); }); - suite('when authed', () => { - setup(() => { - sandbox.stub(window, 'fetch') - .returns(Promise.resolve({ok: true, status: 204})); - element = fixture('basic'); - element._authService.clearCache(); - }); - - test('does not show auth error on 403 by default', done => { - const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert'); - const responseText = Promise.resolve('server says no.'); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - flush(() => { - assert.isFalse(showAuthErrorStub.calledOnce); - done(); - }); - }); - - test('show auth required for 403 with auth error and not authed before', - done => { - const showAuthErrorStub = sandbox.stub( - element, '_showAuthErrorAlert' - ); - const responseText = Promise.resolve('Authentication required\n'); - sinon.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - flush(() => { - assert.isTrue(showAuthErrorStub.calledOnce); - done(); - }); - }); - - test('recheck auth for 403 with auth error if authed before', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const responseText = Promise.resolve('Authentication required\n'); - sinon.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - flush(() => { - assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce); - done(); - }); - }); - - test('show logged in error', () => { - sandbox.stub(element, '_showAuthErrorAlert'); - element.fire('show-auth-required'); - assert.isTrue(element._showAuthErrorAlert.calledWithExactly( - 'Log in is required to perform that action.', 'Log in.')); - }); - - test('show normal Error', done => { - const showErrorStub = sandbox.stub(element, '_showErrorDialog'); - const textSpy = sandbox.spy(() => Promise.resolve('ZOMG')); - element.fire('server-error', {response: {status: 500, text: textSpy}}); - - assert.isTrue(textSpy.called); - flush(() => { - assert.isTrue(showErrorStub.calledOnce); - assert.isTrue(showErrorStub.lastCall.calledWithExactly( - 'Error 500: ZOMG')); - done(); - }); - }); - - test('_constructServerErrorMsg', () => { - const errorText = 'change conflicts'; - const status = 409; - const statusText = 'Conflict'; - const url = '/my/test/url'; - - assert.equal(element._constructServerErrorMsg({status}), - 'Error 409'); - assert.equal(element._constructServerErrorMsg({status, url}), - 'Error 409: \nEndpoint: /my/test/url'); - assert.equal(element. - _constructServerErrorMsg({status, statusText, url}), - 'Error 409 (Conflict): \nEndpoint: /my/test/url'); - assert.equal(element._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - }), 'Error 409 (Conflict): change conflicts' + - '\nEndpoint: /my/test/url'); - assert.equal(element._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - trace: 'xxxxx', - }), 'Error 409 (Conflict): change conflicts' + - '\nEndpoint: /my/test/url\nTrace Id: xxxxx'); - }); - - test('extract trace id from headers if exists', done => { - const textSpy = sandbox.spy( - () => Promise.resolve('500') - ); - const headers = new Headers(); - headers.set('X-Gerrit-Trace', 'xxxx'); - element.fire('server-error', { - response: { - headers, - status: 500, - text: textSpy, - }, - }); - flush(() => { - assert.equal( - element.$.errorDialog.text, - 'Error 500: 500\nTrace Id: xxxx' + test('show auth required for 403 with auth error and not authed before', + done => { + const showAuthErrorStub = sandbox.stub( + element, '_showAuthErrorAlert' ); - done(); - }); - }); - - test('suppress TOO_MANY_FILES error', done => { - const showAlertStub = sandbox.stub(element, '_showAlert'); - const textSpy = sandbox.spy( - () => Promise.resolve('too many files to find conflicts') - ); - element.fire('server-error', {response: {status: 500, text: textSpy}}); - - assert.isTrue(textSpy.called); - flush(() => { - assert.isFalse(showAlertStub.called); - done(); - }); - }); - - test('show network error', done => { - const consoleErrorStub = sandbox.stub(console, 'error'); - const showAlertStub = sandbox.stub(element, '_showAlert'); - element.fire('network-error', {error: new Error('ZOMG')}); - flush(() => { - assert.isTrue(showAlertStub.calledOnce); - assert.isTrue(showAlertStub.lastCall.calledWithExactly( - 'Server unavailable')); - assert.isTrue(consoleErrorStub.calledOnce); - assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG')); - done(); - }); - }); - - test('show auth refresh toast', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount', - () => Promise.resolve({})); - const toastSpy = sandbox.spy(element, '_createToastAlert'); - const windowOpen = sandbox.stub(window, 'open'); - const responseText = Promise.resolve('Authentication required\n'); - // fake failed auth - window.fetch.returns(Promise.resolve({status: 403})); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - assert.equal(window.fetch.callCount, 1); - flush(() => { - // here needs two flush as there are two chanined - // promises on server-error handler and flush only flushes one - assert.equal(window.fetch.callCount, 2); + const responseText = Promise.resolve('Authentication required\n'); + sinon.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); flush(() => { - // auth-error fired - assert.isTrue(toastSpy.called); + assert.isTrue(showAuthErrorStub.calledOnce); + done(); + }); + }); - // toast - let toast = toastSpy.lastCall.returnValue; + test('recheck auth for 403 with auth error if authed before', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const responseText = Promise.resolve('Authentication required\n'); + sinon.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + flush(() => { + assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce); + done(); + }); + }); + + test('show logged in error', () => { + sandbox.stub(element, '_showAuthErrorAlert'); + element.fire('show-auth-required'); + assert.isTrue(element._showAuthErrorAlert.calledWithExactly( + 'Log in is required to perform that action.', 'Log in.')); + }); + + test('show normal Error', done => { + const showErrorStub = sandbox.stub(element, '_showErrorDialog'); + const textSpy = sandbox.spy(() => Promise.resolve('ZOMG')); + element.fire('server-error', {response: {status: 500, text: textSpy}}); + + assert.isTrue(textSpy.called); + flush(() => { + assert.isTrue(showErrorStub.calledOnce); + assert.isTrue(showErrorStub.lastCall.calledWithExactly( + 'Error 500: ZOMG')); + done(); + }); + }); + + test('_constructServerErrorMsg', () => { + const errorText = 'change conflicts'; + const status = 409; + const statusText = 'Conflict'; + const url = '/my/test/url'; + + assert.equal(element._constructServerErrorMsg({status}), + 'Error 409'); + assert.equal(element._constructServerErrorMsg({status, url}), + 'Error 409: \nEndpoint: /my/test/url'); + assert.equal(element. + _constructServerErrorMsg({status, statusText, url}), + 'Error 409 (Conflict): \nEndpoint: /my/test/url'); + assert.equal(element._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + }), 'Error 409 (Conflict): change conflicts' + + '\nEndpoint: /my/test/url'); + assert.equal(element._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + trace: 'xxxxx', + }), 'Error 409 (Conflict): change conflicts' + + '\nEndpoint: /my/test/url\nTrace Id: xxxxx'); + }); + + test('extract trace id from headers if exists', done => { + const textSpy = sandbox.spy( + () => Promise.resolve('500') + ); + const headers = new Headers(); + headers.set('X-Gerrit-Trace', 'xxxx'); + element.fire('server-error', { + response: { + headers, + status: 500, + text: textSpy, + }, + }); + flush(() => { + assert.equal( + element.$.errorDialog.text, + 'Error 500: 500\nTrace Id: xxxx' + ); + done(); + }); + }); + + test('suppress TOO_MANY_FILES error', done => { + const showAlertStub = sandbox.stub(element, '_showAlert'); + const textSpy = sandbox.spy( + () => Promise.resolve('too many files to find conflicts') + ); + element.fire('server-error', {response: {status: 500, text: textSpy}}); + + assert.isTrue(textSpy.called); + flush(() => { + assert.isFalse(showAlertStub.called); + done(); + }); + }); + + test('show network error', done => { + const consoleErrorStub = sandbox.stub(console, 'error'); + const showAlertStub = sandbox.stub(element, '_showAlert'); + element.fire('network-error', {error: new Error('ZOMG')}); + flush(() => { + assert.isTrue(showAlertStub.calledOnce); + assert.isTrue(showAlertStub.lastCall.calledWithExactly( + 'Server unavailable')); + assert.isTrue(consoleErrorStub.calledOnce); + assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG')); + done(); + }); + }); + + test('show auth refresh toast', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount', + () => Promise.resolve({})); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + const windowOpen = sandbox.stub(window, 'open'); + const responseText = Promise.resolve('Authentication required\n'); + // fake failed auth + window.fetch.returns(Promise.resolve({status: 403})); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + assert.equal(window.fetch.callCount, 1); + flush(() => { + // here needs two flush as there are two chanined + // promises on server-error handler and flush only flushes one + assert.equal(window.fetch.callCount, 2); + flush(() => { + // auth-error fired + assert.isTrue(toastSpy.called); + + // toast + let toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + assert.include( + dom(toast.root).textContent, 'Refresh credentials'); + + // noInteractionOverlay + const noInteractionOverlay = element.$.noInteractionOverlay; + assert.isOk(noInteractionOverlay); + sinon.spy(noInteractionOverlay, 'close'); + assert.equal( + noInteractionOverlay.backdropElement.getAttribute('opened'), + ''); + assert.isFalse(windowOpen.called); + MockInteractions.tap(toast.shadowRoot + .querySelector('gr-button.action')); + assert.isTrue(windowOpen.called); + + // @see Issue 5822: noopener breaks closeAfterLogin + assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'), + -1); + + const hideToastSpy = sandbox.spy(toast, 'hide'); + + // now fake authed + window.fetch.returns(Promise.resolve({status: 204})); + element._handleWindowFocus(); + element.flushDebouncer('checkLoggedIn'); + flush(() => { + assert.isTrue(refreshStub.called); + assert.isTrue(hideToastSpy.called); + + // toast update + assert.notStrictEqual(toastSpy.lastCall.returnValue, toast); + toast = toastSpy.lastCall.returnValue; assert.isOk(toast); assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - assert.include( - Polymer.dom(toast.root).textContent, 'Refresh credentials'); + dom(toast.root).textContent, 'Credentials refreshed'); - // noInteractionOverlay - const noInteractionOverlay = element.$.noInteractionOverlay; - assert.isOk(noInteractionOverlay); - sinon.spy(noInteractionOverlay, 'close'); - assert.equal( - noInteractionOverlay.backdropElement.getAttribute('opened'), - ''); - assert.isFalse(windowOpen.called); - MockInteractions.tap(toast.shadowRoot - .querySelector('gr-button.action')); - assert.isTrue(windowOpen.called); - - // @see Issue 5822: noopener breaks closeAfterLogin - assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'), - -1); - - const hideToastSpy = sandbox.spy(toast, 'hide'); - - // now fake authed - window.fetch.returns(Promise.resolve({status: 204})); - element._handleWindowFocus(); - element.flushDebouncer('checkLoggedIn'); - flush(() => { - assert.isTrue(refreshStub.called); - assert.isTrue(hideToastSpy.called); - - // toast update - assert.notStrictEqual(toastSpy.lastCall.returnValue, toast); - toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials refreshed'); - - // close overlay - assert.isTrue(noInteractionOverlay.close.called); - done(); - }); - }); - }); - }); - - test('auth toast should dismiss existing toast', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const toastSpy = sandbox.spy(element, '_createToastAlert'); - const responseText = Promise.resolve('Authentication required\n'); - - // fake an alert - element.fire('show-alert', {message: 'test reload', action: 'reload'}); - const toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'test reload'); - - // fake auth - window.fetch.returns(Promise.resolve({status: 403})); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - assert.equal(window.fetch.callCount, 1); - flush(() => { - // here needs two flush as there are two chanined - // promises on server-error handler and flush only flushes one - assert.equal(window.fetch.callCount, 2); - flush(() => { - // toast - const toast = toastSpy.lastCall.returnValue; - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - assert.include( - Polymer.dom(toast.root).textContent, 'Refresh credentials'); + // close overlay + assert.isTrue(noInteractionOverlay.close.called); done(); }); }); }); + }); - test('regular toast should dismiss regular toast', () => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const toastSpy = sandbox.spy(element, '_createToastAlert'); + test('auth toast should dismiss existing toast', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + const responseText = Promise.resolve('Authentication required\n'); - // fake an alert - element.fire('show-alert', {message: 'test reload', action: 'reload'}); - let toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'test reload'); + // fake an alert + element.fire('show-alert', {message: 'test reload', action: 'reload'}); + const toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'test reload'); - // new alert - element.fire('show-alert', {message: 'second-test', action: 'reload'}); - - toast = toastSpy.lastCall.returnValue; - assert.include(Polymer.dom(toast.root).textContent, 'second-test'); - }); - - test('regular toast should not dismiss auth toast', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const toastSpy = sandbox.spy(element, '_createToastAlert'); - const responseText = Promise.resolve('Authentication required\n'); - - // fake auth - window.fetch.returns(Promise.resolve({status: 403})); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - assert.equal(window.fetch.callCount, 1); + // fake auth + window.fetch.returns(Promise.resolve({status: 403})); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + assert.equal(window.fetch.callCount, 1); + flush(() => { + // here needs two flush as there are two chanined + // promises on server-error handler and flush only flushes one + assert.equal(window.fetch.callCount, 2); flush(() => { - // here needs two flush as there are two chanined - // promises on server-error handler and flush only flushes one - assert.equal(window.fetch.callCount, 2); - flush(() => { - let toast = toastSpy.lastCall.returnValue; - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - assert.include( - Polymer.dom(toast.root).textContent, 'Refresh credentials'); + // toast + const toast = toastSpy.lastCall.returnValue; + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + assert.include( + dom(toast.root).textContent, 'Refresh credentials'); + done(); + }); + }); + }); - // fake an alert - element.fire('show-alert', { - message: 'test-alert', action: 'reload', - }); - flush(() => { - toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - done(); - }); + test('regular toast should dismiss regular toast', () => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + + // fake an alert + element.fire('show-alert', {message: 'test reload', action: 'reload'}); + let toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'test reload'); + + // new alert + element.fire('show-alert', {message: 'second-test', action: 'reload'}); + + toast = toastSpy.lastCall.returnValue; + assert.include(dom(toast.root).textContent, 'second-test'); + }); + + test('regular toast should not dismiss auth toast', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + const responseText = Promise.resolve('Authentication required\n'); + + // fake auth + window.fetch.returns(Promise.resolve({status: 403})); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + assert.equal(window.fetch.callCount, 1); + flush(() => { + // here needs two flush as there are two chanined + // promises on server-error handler and flush only flushes one + assert.equal(window.fetch.callCount, 2); + flush(() => { + let toast = toastSpy.lastCall.returnValue; + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + assert.include( + dom(toast.root).textContent, 'Refresh credentials'); + + // fake an alert + element.fire('show-alert', { + message: 'test-alert', action: 'reload', + }); + flush(() => { + toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + done(); }); }); }); + }); - test('show alert', () => { - const alertObj = {message: 'foo'}; - sandbox.stub(element, '_showAlert'); - element.fire('show-alert', alertObj); - assert.isTrue(element._showAlert.calledOnce); - assert.equal(element._showAlert.lastCall.args[0], 'foo'); - assert.isNotOk(element._showAlert.lastCall.args[1]); - assert.isNotOk(element._showAlert.lastCall.args[2]); - }); + test('show alert', () => { + const alertObj = {message: 'foo'}; + sandbox.stub(element, '_showAlert'); + element.fire('show-alert', alertObj); + assert.isTrue(element._showAlert.calledOnce); + assert.equal(element._showAlert.lastCall.args[0], 'foo'); + assert.isNotOk(element._showAlert.lastCall.args[1]); + assert.isNotOk(element._showAlert.lastCall.args[2]); + }); - test('checks stale credentials on visibility change', () => { - const refreshStub = sandbox.stub(element, - '_checkSignedIn'); - sandbox.stub(Date, 'now').returns(999999); - element._lastCredentialCheck = 0; - element._handleVisibilityChange(); + test('checks stale credentials on visibility change', () => { + const refreshStub = sandbox.stub(element, + '_checkSignedIn'); + sandbox.stub(Date, 'now').returns(999999); + element._lastCredentialCheck = 0; + element._handleVisibilityChange(); - // Since there is no known account, it should not test credentials. - assert.isFalse(refreshStub.called); - assert.equal(element._lastCredentialCheck, 0); + // Since there is no known account, it should not test credentials. + assert.isFalse(refreshStub.called); + assert.equal(element._lastCredentialCheck, 0); - element.knownAccountId = 123; - element._handleVisibilityChange(); + element.knownAccountId = 123; + element._handleVisibilityChange(); - // Should test credentials, since there is a known account. - assert.isTrue(refreshStub.called); - assert.equal(element._lastCredentialCheck, 999999); - }); + // Should test credentials, since there is a known account. + assert.isTrue(refreshStub.called); + assert.equal(element._lastCredentialCheck, 999999); + }); - test('refreshes with same credentials', done => { - const accountPromise = Promise.resolve({_account_id: 1234}); - sandbox.stub(element.$.restAPI, 'getAccount') - .returns(accountPromise); - const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); - const handleRefreshStub = sandbox.stub(element, - '_handleCredentialRefreshed'); - const reloadStub = sandbox.stub(element, '_reloadPage'); + test('refreshes with same credentials', done => { + const accountPromise = Promise.resolve({_account_id: 1234}); + sandbox.stub(element.$.restAPI, 'getAccount') + .returns(accountPromise); + const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, + '_handleCredentialRefreshed'); + const reloadStub = sandbox.stub(element, '_reloadPage'); - element.knownAccountId = 1234; - element._refreshingCredentials = true; - element._checkSignedIn(); + element.knownAccountId = 1234; + element._refreshingCredentials = true; + element._checkSignedIn(); - flush(() => { - assert.isFalse(requestCheckStub.called); - assert.isTrue(handleRefreshStub.called); - assert.isFalse(reloadStub.called); - done(); - }); - }); - - test('_showAlert hides existing alerts', () => { - element._alertElement = element._createToastAlert(); - const hideStub = sandbox.stub(element, '_hideAlert'); - element._showAlert(); - assert.isTrue(hideStub.calledOnce); - }); - - test('show-error', () => { - const openStub = sandbox.stub(element.$.errorOverlay, 'open'); - const closeStub = sandbox.stub(element.$.errorOverlay, 'close'); - const reportStub = sandbox.stub( - element.$.reporting, - 'reportErrorDialog' - ); - - const message = 'test message'; - element.fire('show-error', {message}); - flushAsynchronousOperations(); - - assert.isTrue(openStub.called); - assert.isTrue(reportStub.called); - assert.equal(element.$.errorDialog.text, message); - - element.$.errorDialog.fire('dismiss'); - flushAsynchronousOperations(); - - assert.isTrue(closeStub.called); - }); - - test('reloads when refreshed credentials differ', done => { - const accountPromise = Promise.resolve({_account_id: 1234}); - sandbox.stub(element.$.restAPI, 'getAccount') - .returns(accountPromise); - const requestCheckStub = sandbox.stub( - element, - '_requestCheckLoggedIn'); - const handleRefreshStub = sandbox.stub(element, - '_handleCredentialRefreshed'); - const reloadStub = sandbox.stub(element, '_reloadPage'); - - element.knownAccountId = 4321; // Different from 1234 - element._refreshingCredentials = true; - element._checkSignedIn(); - - flush(() => { - assert.isFalse(requestCheckStub.called); - assert.isFalse(handleRefreshStub.called); - assert.isTrue(reloadStub.called); - done(); - }); + flush(() => { + assert.isFalse(requestCheckStub.called); + assert.isTrue(handleRefreshStub.called); + assert.isFalse(reloadStub.called); + done(); }); }); - suite('when not authed', () => { - setup(() => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - }); - element = fixture('basic'); - }); + test('_showAlert hides existing alerts', () => { + element._alertElement = element._createToastAlert(); + const hideStub = sandbox.stub(element, '_hideAlert'); + element._showAlert(); + assert.isTrue(hideStub.calledOnce); + }); - test('refresh loop continues on credential fail', done => { - const requestCheckStub = sandbox.stub( - element, - '_requestCheckLoggedIn'); - const handleRefreshStub = sandbox.stub(element, - '_handleCredentialRefreshed'); - const reloadStub = sandbox.stub(element, '_reloadPage'); + test('show-error', () => { + const openStub = sandbox.stub(element.$.errorOverlay, 'open'); + const closeStub = sandbox.stub(element.$.errorOverlay, 'close'); + const reportStub = sandbox.stub( + element.$.reporting, + 'reportErrorDialog' + ); - element._refreshingCredentials = true; - element._checkSignedIn(); + const message = 'test message'; + element.fire('show-error', {message}); + flushAsynchronousOperations(); - flush(() => { - assert.isTrue(requestCheckStub.called); - assert.isFalse(handleRefreshStub.called); - assert.isFalse(reloadStub.called); - done(); - }); + assert.isTrue(openStub.called); + assert.isTrue(reportStub.called); + assert.equal(element.$.errorDialog.text, message); + + element.$.errorDialog.fire('dismiss'); + flushAsynchronousOperations(); + + assert.isTrue(closeStub.called); + }); + + test('reloads when refreshed credentials differ', done => { + const accountPromise = Promise.resolve({_account_id: 1234}); + sandbox.stub(element.$.restAPI, 'getAccount') + .returns(accountPromise); + const requestCheckStub = sandbox.stub( + element, + '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, + '_handleCredentialRefreshed'); + const reloadStub = sandbox.stub(element, '_reloadPage'); + + element.knownAccountId = 4321; // Different from 1234 + element._refreshingCredentials = true; + element._checkSignedIn(); + + flush(() => { + assert.isFalse(requestCheckStub.called); + assert.isFalse(handleRefreshStub.called); + assert.isTrue(reloadStub.called); + done(); }); }); }); + + suite('when not authed', () => { + setup(() => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + }); + + test('refresh loop continues on credential fail', done => { + const requestCheckStub = sandbox.stub( + element, + '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, + '_handleCredentialRefreshed'); + const reloadStub = sandbox.stub(element, '_reloadPage'); + + element._refreshingCredentials = true; + element._checkSignedIn(); + + flush(() => { + assert.isTrue(requestCheckStub.called); + assert.isFalse(handleRefreshStub.called); + assert.isFalse(reloadStub.called); + done(); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js index 3d424bc..5d7ec27 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -14,30 +14,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrKeyBindingDisplay extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-key-binding-display'; } +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-key-binding-display_html.js'; - static get properties() { - return { - /** @type {Array<string>} */ - binding: Array, - }; - } +/** @extends Polymer.Element */ +class GrKeyBindingDisplay extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _computeModifiers(binding) { - return binding.slice(0, binding.length - 1); - } + static get is() { return 'gr-key-binding-display'; } - _computeKey(binding) { - return binding[binding.length - 1]; - } + static get properties() { + return { + /** @type {Array<string>} */ + binding: Array, + }; } - customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay); -})(); + _computeModifiers(binding) { + return binding.slice(0, binding.length - 1); + } + + _computeKey(binding) { + return binding[binding.length - 1]; + } +} + +customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay);
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js index a863276..f98be3a 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -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="../../../styles/shared-styles.html"> - -<dom-module id="gr-key-binding-display"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .key { background-color: var(--chip-background-color); @@ -35,14 +32,9 @@ <template is="dom-if" if="[[index]]"> or </template> - <template - is="dom-repeat" - items="[[_computeModifiers(item)]]" - as="modifier"> + <template is="dom-repeat" items="[[_computeModifiers(item)]]" as="modifier"> <span class="key modifier">[[modifier]]</span> </template> <span class="key">[[_computeKey(item)]]</span> </template> - </template> - <script src="gr-key-binding-display.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html index f682f0a..bb449c3 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-key-binding-display</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-key-binding-display.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-key-binding-display.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-key-binding-display.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,37 +39,39 @@ </template> </test-fixture> -<script> - suite('gr-key-binding-display tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-key-binding-display.js'; +suite('gr-key-binding-display tests', () => { + let element; - setup(() => { - element = fixture('basic'); + setup(() => { + element = fixture('basic'); + }); + + suite('_computeKey', () => { + test('unmodified key', () => { + assert.strictEqual(element._computeKey(['x']), 'x'); }); - suite('_computeKey', () => { - test('unmodified key', () => { - assert.strictEqual(element._computeKey(['x']), 'x'); - }); - - test('key with modifiers', () => { - assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x'); - assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x'); - }); - }); - - suite('_computeModifiers', () => { - test('single unmodified key', () => { - assert.deepEqual(element._computeModifiers(['x']), []); - }); - - test('key with modifiers', () => { - assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']); - assert.deepEqual( - element._computeModifiers(['Shift', 'Meta', 'x']), - ['Shift', 'Meta']); - }); + test('key with modifiers', () => { + assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x'); + assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x'); }); }); + + suite('_computeModifiers', () => { + test('single unmodified key', () => { + assert.deepEqual(element._computeModifiers(['x']), []); + }); + + test('key with modifiers', () => { + assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']); + assert.deepEqual( + element._computeModifiers(['Shift', 'Meta', 'x']), + ['Shift', 'Meta']); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js index 4630ca7..371ba02 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -14,129 +14,140 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../shared/gr-button/gr-button.js'; +import '../gr-key-binding-display/gr-key-binding-display.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html.js'; +const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrKeyboardShortcutsDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-keyboard-shortcuts-dialog'; } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when the user presses the close button. + * + * @event close */ - class GrKeyboardShortcutsDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-keyboard-shortcuts-dialog'; } - /** - * Fired when the user presses the close button. - * - * @event close - */ - static get properties() { - return { - _left: Array, - _right: Array, + static get properties() { + return { + _left: Array, + _right: Array, - _propertyBySection: { - type: Object, - value() { - return { - [ShortcutSection.EVERYWHERE]: '_everywhere', - [ShortcutSection.NAVIGATION]: '_navigation', - [ShortcutSection.DASHBOARD]: '_dashboard', - [ShortcutSection.CHANGE_LIST]: '_changeList', - [ShortcutSection.ACTIONS]: '_actions', - [ShortcutSection.REPLY_DIALOG]: '_replyDialog', - [ShortcutSection.FILE_LIST]: '_fileList', - [ShortcutSection.DIFFS]: '_diffs', - }; - }, + _propertyBySection: { + type: Object, + value() { + return { + [ShortcutSection.EVERYWHERE]: '_everywhere', + [ShortcutSection.NAVIGATION]: '_navigation', + [ShortcutSection.DASHBOARD]: '_dashboard', + [ShortcutSection.CHANGE_LIST]: '_changeList', + [ShortcutSection.ACTIONS]: '_actions', + [ShortcutSection.REPLY_DIALOG]: '_replyDialog', + [ShortcutSection.FILE_LIST]: '_fileList', + [ShortcutSection.DIFFS]: '_diffs', + }; }, - }; - } - - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'dialog'); - } - - /** @override */ - attached() { - super.attached(); - this.addKeyboardShortcutDirectoryListener( - this._onDirectoryUpdated.bind(this)); - } - - /** @override */ - detached() { - super.detached(); - this.removeKeyboardShortcutDirectoryListener( - this._onDirectoryUpdated.bind(this)); - } - - _handleCloseTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('close', null, {bubbles: false}); - } - - _onDirectoryUpdated(directory) { - const left = []; - const right = []; - - if (directory.has(ShortcutSection.EVERYWHERE)) { - left.push({ - section: ShortcutSection.EVERYWHERE, - shortcuts: directory.get(ShortcutSection.EVERYWHERE), - }); - } - - if (directory.has(ShortcutSection.NAVIGATION)) { - left.push({ - section: ShortcutSection.NAVIGATION, - shortcuts: directory.get(ShortcutSection.NAVIGATION), - }); - } - - if (directory.has(ShortcutSection.ACTIONS)) { - right.push({ - section: ShortcutSection.ACTIONS, - shortcuts: directory.get(ShortcutSection.ACTIONS), - }); - } - - if (directory.has(ShortcutSection.REPLY_DIALOG)) { - right.push({ - section: ShortcutSection.REPLY_DIALOG, - shortcuts: directory.get(ShortcutSection.REPLY_DIALOG), - }); - } - - if (directory.has(ShortcutSection.FILE_LIST)) { - right.push({ - section: ShortcutSection.FILE_LIST, - shortcuts: directory.get(ShortcutSection.FILE_LIST), - }); - } - - if (directory.has(ShortcutSection.DIFFS)) { - right.push({ - section: ShortcutSection.DIFFS, - shortcuts: directory.get(ShortcutSection.DIFFS), - }); - } - - this.set('_left', left); - this.set('_right', right); - } + }, + }; } - customElements.define(GrKeyboardShortcutsDialog.is, - GrKeyboardShortcutsDialog); -})(); + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } + + /** @override */ + attached() { + super.attached(); + this.addKeyboardShortcutDirectoryListener( + this._onDirectoryUpdated.bind(this)); + } + + /** @override */ + detached() { + super.detached(); + this.removeKeyboardShortcutDirectoryListener( + this._onDirectoryUpdated.bind(this)); + } + + _handleCloseTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('close', null, {bubbles: false}); + } + + _onDirectoryUpdated(directory) { + const left = []; + const right = []; + + if (directory.has(ShortcutSection.EVERYWHERE)) { + left.push({ + section: ShortcutSection.EVERYWHERE, + shortcuts: directory.get(ShortcutSection.EVERYWHERE), + }); + } + + if (directory.has(ShortcutSection.NAVIGATION)) { + left.push({ + section: ShortcutSection.NAVIGATION, + shortcuts: directory.get(ShortcutSection.NAVIGATION), + }); + } + + if (directory.has(ShortcutSection.ACTIONS)) { + right.push({ + section: ShortcutSection.ACTIONS, + shortcuts: directory.get(ShortcutSection.ACTIONS), + }); + } + + if (directory.has(ShortcutSection.REPLY_DIALOG)) { + right.push({ + section: ShortcutSection.REPLY_DIALOG, + shortcuts: directory.get(ShortcutSection.REPLY_DIALOG), + }); + } + + if (directory.has(ShortcutSection.FILE_LIST)) { + right.push({ + section: ShortcutSection.FILE_LIST, + shortcuts: directory.get(ShortcutSection.FILE_LIST), + }); + } + + if (directory.has(ShortcutSection.DIFFS)) { + right.push({ + section: ShortcutSection.DIFFS, + shortcuts: directory.get(ShortcutSection.DIFFS), + }); + } + + this.set('_left', left); + this.set('_right', right); + } +} + +customElements.define(GrKeyboardShortcutsDialog.is, + GrKeyboardShortcutsDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js index a4424a2..380228f 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-keyboard-shortcuts-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -63,7 +56,7 @@ </style> <header> <h3>Keyboard shortcuts</h3> - <gr-button link on-click="_handleCloseTap">Close</gr-button> + <gr-button link="" on-click="_handleCloseTap">Close</gr-button> </header> <main> <table> @@ -106,6 +99,4 @@ </template> </main> <footer></footer> - </template> - <script src="gr-keyboard-shortcuts-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html index cc53db17..eedd166 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-key-binding-display</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-keyboard-shortcuts-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-keyboard-shortcuts-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-keyboard-shortcuts-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,150 +39,152 @@ </template> </test-fixture> -<script> - suite('gr-keyboard-shortcuts-dialog tests', async () => { - await readyToTest(); - const kb = window.Gerrit.KeyboardShortcutBinder; - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-keyboard-shortcuts-dialog.js'; +suite('gr-keyboard-shortcuts-dialog tests', () => { + const kb = window.Gerrit.KeyboardShortcutBinder; + let element; - setup(() => { - element = fixture('basic'); + setup(() => { + element = fixture('basic'); + }); + + function update(directory) { + element._onDirectoryUpdated(directory); + flushAsynchronousOperations(); + } + + suite('_left and _right contents', () => { + test('empty dialog', () => { + assert.strictEqual(element._left.length, 0); + assert.strictEqual(element._right.length, 0); }); - function update(directory) { - element._onDirectoryUpdated(directory); - flushAsynchronousOperations(); - } + test('everywhere goes on left', () => { + update(new Map([ + [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], + ])); + assert.deepEqual( + element._left, + [ + { + section: kb.ShortcutSection.EVERYWHERE, + shortcuts: ['everywhere shortcuts'], + }, + ]); + assert.strictEqual(element._right.length, 0); + }); - suite('_left and _right contents', () => { - test('empty dialog', () => { - assert.strictEqual(element._left.length, 0); - assert.strictEqual(element._right.length, 0); - }); + test('navigation goes on left', () => { + update(new Map([ + [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], + ])); + assert.deepEqual( + element._left, + [ + { + section: kb.ShortcutSection.NAVIGATION, + shortcuts: ['navigation shortcuts'], + }, + ]); + assert.strictEqual(element._right.length, 0); + }); - test('everywhere goes on left', () => { - update(new Map([ - [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], - ])); - assert.deepEqual( - element._left, - [ - { - section: kb.ShortcutSection.EVERYWHERE, - shortcuts: ['everywhere shortcuts'], - }, - ]); - assert.strictEqual(element._right.length, 0); - }); + test('actions go on right', () => { + update(new Map([ + [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.ACTIONS, + shortcuts: ['actions shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('navigation goes on left', () => { - update(new Map([ - [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], - ])); - assert.deepEqual( - element._left, - [ - { - section: kb.ShortcutSection.NAVIGATION, - shortcuts: ['navigation shortcuts'], - }, - ]); - assert.strictEqual(element._right.length, 0); - }); + test('reply dialog goes on right', () => { + update(new Map([ + [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.REPLY_DIALOG, + shortcuts: ['reply dialog shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('actions go on right', () => { - update(new Map([ - [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.ACTIONS, - shortcuts: ['actions shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); + test('file list goes on right', () => { + update(new Map([ + [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.FILE_LIST, + shortcuts: ['file list shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('reply dialog goes on right', () => { - update(new Map([ - [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.REPLY_DIALOG, - shortcuts: ['reply dialog shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); + test('diffs go on right', () => { + update(new Map([ + [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.DIFFS, + shortcuts: ['diffs shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('file list goes on right', () => { - update(new Map([ - [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.FILE_LIST, - shortcuts: ['file list shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); - - test('diffs go on right', () => { - update(new Map([ - [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.DIFFS, - shortcuts: ['diffs shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); - - test('multiple sections on each side', () => { - update(new Map([ - [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], - [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], - [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], - [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], - ])); - assert.deepEqual( - element._left, - [ - { - section: kb.ShortcutSection.EVERYWHERE, - shortcuts: ['everywhere shortcuts'], - }, - { - section: kb.ShortcutSection.NAVIGATION, - shortcuts: ['navigation shortcuts'], - }, - ]); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.ACTIONS, - shortcuts: ['actions shortcuts'], - }, - { - section: kb.ShortcutSection.DIFFS, - shortcuts: ['diffs shortcuts'], - }, - ]); - }); + test('multiple sections on each side', () => { + update(new Map([ + [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], + [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], + [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], + [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], + ])); + assert.deepEqual( + element._left, + [ + { + section: kb.ShortcutSection.EVERYWHERE, + shortcuts: ['everywhere shortcuts'], + }, + { + section: kb.ShortcutSection.NAVIGATION, + shortcuts: ['navigation shortcuts'], + }, + ]); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.ACTIONS, + shortcuts: ['actions shortcuts'], + }, + { + section: kb.ShortcutSection.DIFFS, + shortcuts: ['diffs shortcuts'], + }, + ]); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js index 05765fb..c8ed50c 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,332 +14,349 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const DEFAULT_LINKS = [{ - title: 'Changes', - links: [ - { - url: '/q/status:open', - name: 'Open', +import '../../../behaviors/docs-url-behavior/docs-url-behavior.js'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-account-dropdown/gr-account-dropdown.js'; +import '../gr-smart-search/gr-smart-search.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-main-header_html.js'; + +const DEFAULT_LINKS = [{ + title: 'Changes', + links: [ + { + url: '/q/status:open', + name: 'Open', + }, + { + url: '/q/status:merged', + name: 'Merged', + }, + { + url: '/q/status:abandoned', + name: 'Abandoned', + }, + ], +}]; + +const DOCUMENTATION_LINKS = [ + { + url: '/index.html', + name: 'Table of Contents', + }, + { + url: '/user-search.html', + name: 'Searching', + }, + { + url: '/user-upload.html', + name: 'Uploading', + }, + { + url: '/access-control.html', + name: 'Access Control', + }, + { + url: '/rest-api.html', + name: 'REST API', + }, + { + url: '/intro-project-owner.html', + name: 'Project Owner Guide', + }, +]; + +// Set of authentication methods that can provide custom registration page. +const AUTH_TYPES_WITH_REGISTER_URL = new Set([ + 'LDAP', + 'LDAP_BIND', + 'CUSTOM_EXTENSION', +]); + +/** + * @appliesMixin Gerrit.AdminNavMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.DocsUrlMixin + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrMainHeader extends mixinBehaviors( [ + Gerrit.AdminNavBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.DocsUrlBehavior, + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-main-header'; } + + static get properties() { + return { + searchQuery: { + type: String, + notify: true, }, - { - url: '/q/status:merged', - name: 'Merged', + loggedIn: { + type: Boolean, + reflectToAttribute: true, }, - { - url: '/q/status:abandoned', - name: 'Abandoned', + loading: { + type: Boolean, + reflectToAttribute: true, }, - ], - }]; - const DOCUMENTATION_LINKS = [ - { - url: '/index.html', - name: 'Table of Contents', - }, - { - url: '/user-search.html', - name: 'Searching', - }, - { - url: '/user-upload.html', - name: 'Uploading', - }, - { - url: '/access-control.html', - name: 'Access Control', - }, - { - url: '/rest-api.html', - name: 'REST API', - }, - { - url: '/intro-project-owner.html', - name: 'Project Owner Guide', - }, - ]; + /** @type {?Object} */ + _account: Object, + _adminLinks: { + type: Array, + value() { return []; }, + }, + _defaultLinks: { + type: Array, + value() { + return DEFAULT_LINKS; + }, + }, + _docBaseUrl: { + type: String, + value: null, + }, + _links: { + type: Array, + computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' + + '_topMenus, _docBaseUrl)', + }, + loginUrl: { + type: String, + value: '/login', + }, + _userLinks: { + type: Array, + value() { return []; }, + }, + _topMenus: { + type: Array, + value() { return []; }, + }, + _registerText: { + type: String, + value: 'Sign up', + }, + _registerURL: { + type: String, + value: null, + }, + }; + } - // Set of authentication methods that can provide custom registration page. - const AUTH_TYPES_WITH_REGISTER_URL = new Set([ - 'LDAP', - 'LDAP_BIND', - 'CUSTOM_EXTENSION', - ]); + static get observers() { + return [ + '_accountLoaded(_account)', + ]; + } - /** - * @appliesMixin Gerrit.AdminNavMixin - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.DocsUrlMixin - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrMainHeader extends Polymer.mixinBehaviors( [ - Gerrit.AdminNavBehavior, - Gerrit.BaseUrlBehavior, - Gerrit.DocsUrlBehavior, - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-main-header'; } + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'banner'); + } - static get properties() { + /** @override */ + attached() { + super.attached(); + this._loadAccount(); + this._loadConfig(); + } + + /** @override */ + detached() { + super.detached(); + } + + reload() { + this._loadAccount(); + } + + _computeRelativeURL(path) { + return '//' + window.location.host + this.getBaseUrl() + path; + } + + _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) { + // Polymer 2: check for undefined + if ([ + defaultLinks, + userLinks, + adminLinks, + topMenus, + docBaseUrl, + ].some(arg => arg === undefined)) { + return undefined; + } + + const links = defaultLinks.map(menu => { return { - searchQuery: { - type: String, - notify: true, - }, - loggedIn: { - type: Boolean, - reflectToAttribute: true, - }, - loading: { - type: Boolean, - reflectToAttribute: true, - }, - - /** @type {?Object} */ - _account: Object, - _adminLinks: { - type: Array, - value() { return []; }, - }, - _defaultLinks: { - type: Array, - value() { - return DEFAULT_LINKS; - }, - }, - _docBaseUrl: { - type: String, - value: null, - }, - _links: { - type: Array, - computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' + - '_topMenus, _docBaseUrl)', - }, - loginUrl: { - type: String, - value: '/login', - }, - _userLinks: { - type: Array, - value() { return []; }, - }, - _topMenus: { - type: Array, - value() { return []; }, - }, - _registerText: { - type: String, - value: 'Sign up', - }, - _registerURL: { - type: String, - value: null, - }, + title: menu.title, + links: menu.links.slice(), }; - } - - static get observers() { - return [ - '_accountLoaded(_account)', - ]; - } - - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'banner'); - } - - /** @override */ - attached() { - super.attached(); - this._loadAccount(); - this._loadConfig(); - } - - /** @override */ - detached() { - super.detached(); - } - - reload() { - this._loadAccount(); - } - - _computeRelativeURL(path) { - return '//' + window.location.host + this.getBaseUrl() + path; - } - - _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) { - // Polymer 2: check for undefined - if ([ - defaultLinks, - userLinks, - adminLinks, - topMenus, - docBaseUrl, - ].some(arg => arg === undefined)) { - return undefined; - } - - const links = defaultLinks.map(menu => { - return { - title: menu.title, - links: menu.links.slice(), - }; - }); - if (userLinks && userLinks.length > 0) { - links.push({ - title: 'Your', - links: userLinks.slice(), - }); - } - const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS); - if (docLinks.length) { - links.push({ - title: 'Documentation', - links: docLinks, - class: 'hideOnMobile', - }); - } + }); + if (userLinks && userLinks.length > 0) { links.push({ - title: 'Browse', - links: adminLinks.slice(), + title: 'Your', + links: userLinks.slice(), }); - const topMenuLinks = []; - links.forEach(link => { topMenuLinks[link.title] = link.links; }); - for (const m of topMenus) { - const items = m.items.map(this._fixCustomMenuItem).filter(link => - // Ignore GWT project links - !link.url.includes('${projectName}') - ); - if (m.name in topMenuLinks) { - items.forEach(link => { topMenuLinks[m.name].push(link); }); - } else { - links.push({ - title: m.name, - links: topMenuLinks[m.name] = items, + } + const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS); + if (docLinks.length) { + links.push({ + title: 'Documentation', + links: docLinks, + class: 'hideOnMobile', + }); + } + links.push({ + title: 'Browse', + links: adminLinks.slice(), + }); + const topMenuLinks = []; + links.forEach(link => { topMenuLinks[link.title] = link.links; }); + for (const m of topMenus) { + const items = m.items.map(this._fixCustomMenuItem).filter(link => + // Ignore GWT project links + !link.url.includes('${projectName}') + ); + if (m.name in topMenuLinks) { + items.forEach(link => { topMenuLinks[m.name].push(link); }); + } else { + links.push({ + title: m.name, + links: topMenuLinks[m.name] = items, + }); + } + } + return links; + } + + _getDocLinks(docBaseUrl, docLinks) { + if (!docBaseUrl || !docLinks) { + return []; + } + return docLinks.map(link => { + let url = docBaseUrl; + if (url && url[url.length - 1] === '/') { + url = url.substring(0, url.length - 1); + } + return { + url: url + link.url, + name: link.name, + target: '_blank', + }; + }); + } + + _loadAccount() { + this.loading = true; + const promises = [ + this.$.restAPI.getAccount(), + this.$.restAPI.getTopMenus(), + Gerrit.awaitPluginsLoaded(), + ]; + + return Promise.all(promises).then(result => { + const account = result[0]; + this._account = account; + this.loggedIn = !!account; + this.loading = false; + this._topMenus = result[1]; + + return this.getAdminLinks(account, + this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI), + this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI)) + .then(res => { + this._adminLinks = res.links; }); - } + }); + } + + _loadConfig() { + this.$.restAPI.getConfig() + .then(config => { + this._retrieveRegisterURL(config); + return this.getDocsBaseUrl(config, this.$.restAPI); + }) + .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; }); + } + + _accountLoaded(account) { + if (!account) { return; } + + this.$.restAPI.getPreferences().then(prefs => { + this._userLinks = prefs && prefs.my ? + prefs.my.map(this._fixCustomMenuItem) : []; + }); + } + + _retrieveRegisterURL(config) { + if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) { + this._registerURL = config.auth.register_url; + if (config.auth.register_text) { + this._registerText = config.auth.register_text; } - return links; - } - - _getDocLinks(docBaseUrl, docLinks) { - if (!docBaseUrl || !docLinks) { - return []; - } - return docLinks.map(link => { - let url = docBaseUrl; - if (url && url[url.length - 1] === '/') { - url = url.substring(0, url.length - 1); - } - return { - url: url + link.url, - name: link.name, - target: '_blank', - }; - }); - } - - _loadAccount() { - this.loading = true; - const promises = [ - this.$.restAPI.getAccount(), - this.$.restAPI.getTopMenus(), - Gerrit.awaitPluginsLoaded(), - ]; - - return Promise.all(promises).then(result => { - const account = result[0]; - this._account = account; - this.loggedIn = !!account; - this.loading = false; - this._topMenus = result[1]; - - return this.getAdminLinks(account, - this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI), - this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI)) - .then(res => { - this._adminLinks = res.links; - }); - }); - } - - _loadConfig() { - this.$.restAPI.getConfig() - .then(config => { - this._retrieveRegisterURL(config); - return this.getDocsBaseUrl(config, this.$.restAPI); - }) - .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; }); - } - - _accountLoaded(account) { - if (!account) { return; } - - this.$.restAPI.getPreferences().then(prefs => { - this._userLinks = prefs && prefs.my ? - prefs.my.map(this._fixCustomMenuItem) : []; - }); - } - - _retrieveRegisterURL(config) { - if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) { - this._registerURL = config.auth.register_url; - if (config.auth.register_text) { - this._registerText = config.auth.register_text; - } - } - } - - _computeIsInvisible(registerURL) { - return registerURL ? '' : 'invisible'; - } - - _fixCustomMenuItem(linkObj) { - // Normalize all urls to PolyGerrit style. - if (linkObj.url.startsWith('#')) { - linkObj.url = linkObj.url.slice(1); - } - - // Delete target property due to complications of - // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888 - // - // The server tries to guess whether URL is a view within the UI. - // If not, it sets target='_blank' on the menu item. The server - // makes assumptions that work for the GWT UI, but not PolyGerrit, - // so we'll just disable it altogether for now. - delete linkObj.target; - - return linkObj; - } - - _generateSettingsLink() { - return this.getBaseUrl() + '/settings/'; - } - - _onMobileSearchTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('mobile-search', null, {bubbles: false}); - } - - _computeLinkGroupClass(linkGroup) { - if (linkGroup && linkGroup.class) { - return linkGroup.class; - } - - return ''; } } - customElements.define(GrMainHeader.is, GrMainHeader); -})(); + _computeIsInvisible(registerURL) { + return registerURL ? '' : 'invisible'; + } + + _fixCustomMenuItem(linkObj) { + // Normalize all urls to PolyGerrit style. + if (linkObj.url.startsWith('#')) { + linkObj.url = linkObj.url.slice(1); + } + + // Delete target property due to complications of + // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888 + // + // The server tries to guess whether URL is a view within the UI. + // If not, it sets target='_blank' on the menu item. The server + // makes assumptions that work for the GWT UI, but not PolyGerrit, + // so we'll just disable it altogether for now. + delete linkObj.target; + + return linkObj; + } + + _generateSettingsLink() { + return this.getBaseUrl() + '/settings/'; + } + + _onMobileSearchTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('mobile-search', null, {bubbles: false}); + } + + _computeLinkGroupClass(linkGroup) { + if (linkGroup && linkGroup.class) { + return linkGroup.class; + } + + return ''; + } +} + +customElements.define(GrMainHeader.is, GrMainHeader);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js index 51717a8..307a081 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
@@ -1,35 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html"> -<link rel="import" href="../gr-smart-search/gr-smart-search.html"> - -<dom-module id="gr-main-header"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -183,19 +170,15 @@ } </style> <nav> - <a href$="[[_computeRelativeURL('/')]]" class="bigTitle"> + <a href\$="[[_computeRelativeURL('/')]]" class="bigTitle"> <gr-endpoint-decorator name="header-title"> <span class="titleText"></span> </gr-endpoint-decorator> </a> <ul class="links"> <template is="dom-repeat" items="[[_links]]" as="linkGroup"> - <li class$="[[_computeLinkGroupClass(linkGroup)]]"> - <gr-dropdown - link - down-arrow - items = [[linkGroup.links]] - horizontal-align="left"> + <li class\$="[[_computeLinkGroupClass(linkGroup)]]"> + <gr-dropdown link="" down-arrow="" items="[[linkGroup.links]]" horizontal-align="left"> <span class="linksTitle" id="[[linkGroup.title]]"> [[linkGroup.title]] </span> @@ -204,29 +187,18 @@ </template> </ul> <div class="rightItems"> - <gr-endpoint-decorator - class="hideOnMobile" - name="header-small-banner"></gr-endpoint-decorator> - <gr-smart-search - id="search" - search-query="{{searchQuery}}"></gr-smart-search> - <gr-endpoint-decorator - class="hideOnMobile" - name="header-browse-source"></gr-endpoint-decorator> + <gr-endpoint-decorator class="hideOnMobile" name="header-small-banner"></gr-endpoint-decorator> + <gr-smart-search id="search" search-query="{{searchQuery}}"></gr-smart-search> + <gr-endpoint-decorator class="hideOnMobile" name="header-browse-source"></gr-endpoint-decorator> <div class="accountContainer" id="accountContainer"> - <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap='_onMobileSearchTap'></iron-icon> - <div class$="[[_computeIsInvisible(_registerURL)]]"> - <a - class="registerButton" - href$="[[_registerURL]]"> + <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap="_onMobileSearchTap"></iron-icon> + <div class\$="[[_computeIsInvisible(_registerURL)]]"> + <a class="registerButton" href\$="[[_registerURL]]"> [[_registerText]] </a> </div> - <a class="loginButton" href$="[[loginUrl]]">Sign in</a> - <a - class="settingsButton" - href$="[[_generateSettingsLink()]]" - title="Settings"> + <a class="loginButton" href\$="[[loginUrl]]">Sign in</a> + <a class="settingsButton" href\$="[[_generateSettingsLink()]]" title="Settings"> <iron-icon icon="gr-icons:settings"></iron-icon> </a> <gr-account-dropdown account="[[_account]]"></gr-account-dropdown> @@ -235,6 +207,4 @@ </nav> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-main-header.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html index 8fe3fca..817643b 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-main-header</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-main-header.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-main-header.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-main-header.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,379 +40,381 @@ </template> </test-fixture> -<script> - suite('gr-main-header tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-main-header.js'; +suite('gr-main-header tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - probePath(path) { return Promise.resolve(false); }, - }); - stub('gr-main-header', { - _loadAccount() {}, - }); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + probePath(path) { return Promise.resolve(false); }, }); - - teardown(() => { - sandbox.restore(); + stub('gr-main-header', { + _loadAccount() {}, }); - - test('link visibility', () => { - element.loading = true; - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.accountContainer')).display, - 'none'); - element.loading = false; - element.loggedIn = false; - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.accountContainer')).display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.loginButton')).display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.registerButton')).display, - 'none'); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('gr-account-dropdown')).display, - 'none'); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.settingsButton')).display, - 'none'); - element.loggedIn = true; - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.loginButton')).display, - 'none'); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.registerButton')).display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('gr-account-dropdown')) - .display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.settingsButton')).display, - 'none'); - }); - - test('fix my menu item', () => { - assert.deepEqual([ - {url: 'https://awesometown.com/#hashyhash'}, - {url: 'url', target: '_blank'}, - ].map(element._fixCustomMenuItem), [ - {url: 'https://awesometown.com/#hashyhash'}, - {url: 'url'}, - ]); - }); - - test('user links', () => { - const defaultLinks = [{ - title: 'Faves', - links: [{ - name: 'Pinterest', - url: 'https://pinterest.com', - }], - }]; - const userLinks = [{ - name: 'Facebook', - url: 'https://facebook.com', - }]; - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - - // When no admin links are passed, it should use the default. - assert.deepEqual(element._computeLinks( - defaultLinks, - /* userLinks= */[], - adminLinks, - /* topMenus= */[], - /* docBaseUrl= */ '' - ), - defaultLinks.concat({ - title: 'Browse', - links: adminLinks, - })); - assert.deepEqual(element._computeLinks( - defaultLinks, - userLinks, - adminLinks, - /* topMenus= */[], - /* docBaseUrl= */ '' - ), - defaultLinks.concat([ - { - title: 'Your', - links: userLinks, - }, - { - title: 'Browse', - links: adminLinks, - }]) - ); - }); - - test('documentation links', () => { - const docLinks = [ - { - name: 'Table of Contents', - url: '/index.html', - }, - ]; - - assert.deepEqual(element._getDocLinks(null, docLinks), []); - assert.deepEqual(element._getDocLinks('', docLinks), []); - assert.deepEqual(element._getDocLinks('base', null), []); - assert.deepEqual(element._getDocLinks('base', []), []); - - assert.deepEqual(element._getDocLinks('base', docLinks), [{ - name: 'Table of Contents', - target: '_blank', - url: 'base/index.html', - }]); - - assert.deepEqual(element._getDocLinks('base/', docLinks), [{ - name: 'Table of Contents', - target: '_blank', - url: 'base/index.html', - }]); - }); - - test('top menus', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Plugins', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks, - }, - { - title: 'Plugins', - links: [{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]); - }); - - test('ignore top project menus', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Projects', - items: [{ - name: 'Project Settings', - target: '_blank', - url: '/plugins/myplugin/${projectName}', - }, { - name: 'Project List', - target: '_blank', - url: '/plugins/myplugin/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks, - }, - { - title: 'Projects', - links: [{ - name: 'Project List', - url: '/plugins/myplugin/index.html', - }], - }]); - }); - - test('merge top menus', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Plugins', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }, { - name: 'Plugins', - items: [{ - name: 'Create', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/create.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks, - }, { - title: 'Plugins', - links: [{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }, { - name: 'Create', - url: 'https://gerrit/plugins/plugin-manager/static/create.html', - }], - }]); - }); - - test('merge top menus in default links', () => { - const defaultLinks = [{ - title: 'Faves', - links: [{ - name: 'Pinterest', - url: 'https://pinterest.com', - }], - }]; - const topMenus = [{ - name: 'Faves', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - defaultLinks, - /* userLinks= */ [], - /* adminLinks= */ [], - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Faves', - links: defaultLinks[0].links.concat([{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }]), - }, { - title: 'Browse', - links: [], - }]); - }); - - test('merge top menus in user links', () => { - const userLinks = [{ - name: 'Facebook', - url: 'https://facebook.com', - }]; - const topMenus = [{ - name: 'Your', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - userLinks, - /* adminLinks= */ [], - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Your', - links: userLinks.concat([{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }]), - }, { - title: 'Browse', - links: [], - }]); - }); - - test('merge top menus in admin links', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Browse', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks.concat([{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }]), - }]); - }); - - test('register URL', () => { - const config = { - auth: { - auth_type: 'LDAP', - register_url: 'https//gerrit.example.com/register', - }, - }; - element._retrieveRegisterURL(config); - assert.equal(element._registerURL, config.auth.register_url); - assert.equal(element._registerText, 'Sign up'); - - config.auth.register_text = 'Create account'; - element._retrieveRegisterURL(config); - assert.equal(element._registerURL, config.auth.register_url); - assert.equal(element._registerText, config.auth.register_text); - }); - - test('register URL ignored for wrong auth type', () => { - const config = { - auth: { - auth_type: 'OPENID', - register_url: 'https//gerrit.example.com/register', - }, - }; - element._retrieveRegisterURL(config); - assert.equal(element._registerURL, null); - assert.equal(element._registerText, 'Sign up'); - }); + element = fixture('basic'); }); - </script> + + teardown(() => { + sandbox.restore(); + }); + + test('link visibility', () => { + element.loading = true; + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.accountContainer')).display, + 'none'); + element.loading = false; + element.loggedIn = false; + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.accountContainer')).display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.loginButton')).display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.registerButton')).display, + 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('gr-account-dropdown')).display, + 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.settingsButton')).display, + 'none'); + element.loggedIn = true; + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.loginButton')).display, + 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.registerButton')).display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('gr-account-dropdown')) + .display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.settingsButton')).display, + 'none'); + }); + + test('fix my menu item', () => { + assert.deepEqual([ + {url: 'https://awesometown.com/#hashyhash'}, + {url: 'url', target: '_blank'}, + ].map(element._fixCustomMenuItem), [ + {url: 'https://awesometown.com/#hashyhash'}, + {url: 'url'}, + ]); + }); + + test('user links', () => { + const defaultLinks = [{ + title: 'Faves', + links: [{ + name: 'Pinterest', + url: 'https://pinterest.com', + }], + }]; + const userLinks = [{ + name: 'Facebook', + url: 'https://facebook.com', + }]; + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + + // When no admin links are passed, it should use the default. + assert.deepEqual(element._computeLinks( + defaultLinks, + /* userLinks= */[], + adminLinks, + /* topMenus= */[], + /* docBaseUrl= */ '' + ), + defaultLinks.concat({ + title: 'Browse', + links: adminLinks, + })); + assert.deepEqual(element._computeLinks( + defaultLinks, + userLinks, + adminLinks, + /* topMenus= */[], + /* docBaseUrl= */ '' + ), + defaultLinks.concat([ + { + title: 'Your', + links: userLinks, + }, + { + title: 'Browse', + links: adminLinks, + }]) + ); + }); + + test('documentation links', () => { + const docLinks = [ + { + name: 'Table of Contents', + url: '/index.html', + }, + ]; + + assert.deepEqual(element._getDocLinks(null, docLinks), []); + assert.deepEqual(element._getDocLinks('', docLinks), []); + assert.deepEqual(element._getDocLinks('base', null), []); + assert.deepEqual(element._getDocLinks('base', []), []); + + assert.deepEqual(element._getDocLinks('base', docLinks), [{ + name: 'Table of Contents', + target: '_blank', + url: 'base/index.html', + }]); + + assert.deepEqual(element._getDocLinks('base/', docLinks), [{ + name: 'Table of Contents', + target: '_blank', + url: 'base/index.html', + }]); + }); + + test('top menus', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Plugins', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks, + }, + { + title: 'Plugins', + links: [{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]); + }); + + test('ignore top project menus', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Projects', + items: [{ + name: 'Project Settings', + target: '_blank', + url: '/plugins/myplugin/${projectName}', + }, { + name: 'Project List', + target: '_blank', + url: '/plugins/myplugin/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks, + }, + { + title: 'Projects', + links: [{ + name: 'Project List', + url: '/plugins/myplugin/index.html', + }], + }]); + }); + + test('merge top menus', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Plugins', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }, { + name: 'Plugins', + items: [{ + name: 'Create', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/create.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks, + }, { + title: 'Plugins', + links: [{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }, { + name: 'Create', + url: 'https://gerrit/plugins/plugin-manager/static/create.html', + }], + }]); + }); + + test('merge top menus in default links', () => { + const defaultLinks = [{ + title: 'Faves', + links: [{ + name: 'Pinterest', + url: 'https://pinterest.com', + }], + }]; + const topMenus = [{ + name: 'Faves', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + defaultLinks, + /* userLinks= */ [], + /* adminLinks= */ [], + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Faves', + links: defaultLinks[0].links.concat([{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }]), + }, { + title: 'Browse', + links: [], + }]); + }); + + test('merge top menus in user links', () => { + const userLinks = [{ + name: 'Facebook', + url: 'https://facebook.com', + }]; + const topMenus = [{ + name: 'Your', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + userLinks, + /* adminLinks= */ [], + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Your', + links: userLinks.concat([{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }]), + }, { + title: 'Browse', + links: [], + }]); + }); + + test('merge top menus in admin links', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Browse', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks.concat([{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }]), + }]); + }); + + test('register URL', () => { + const config = { + auth: { + auth_type: 'LDAP', + register_url: 'https//gerrit.example.com/register', + }, + }; + element._retrieveRegisterURL(config); + assert.equal(element._registerURL, config.auth.register_url); + assert.equal(element._registerText, 'Sign up'); + + config.auth.register_text = 'Create account'; + element._retrieveRegisterURL(config); + assert.equal(element._registerURL, config.auth.register_url); + assert.equal(element._registerText, config.auth.register_text); + }); + + test('register URL ignored for wrong auth type', () => { + const config = { + auth: { + auth_type: 'OPENID', + register_url: 'https//gerrit.example.com/register', + }, + }; + element._retrieveRegisterURL(config); + assert.equal(element._registerURL, null); + assert.equal(element._registerText, 'Sign up'); + }); +}); +</script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js index e79277a..c8724c3 100644 --- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -1,750 +1,748 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function(window) { + 'use strict'; -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 + // Navigation parameters object format: + // + // Each object has a `view` property with a value from Gerrit.Nav.View. The + // remaining properties depend on the value used for view. + // + // - Gerrit.Nav.View.CHANGE: + // - `changeNum`, required, String: the numeric ID of the change. + // - `project`, optional, String: the project name. + // - `patchNum`, optional, Number: the patch for the right-hand-side of + // the diff. + // - `basePatchNum`, optional, Number: the patch for the left-hand-side + // of the diff. If `basePatchNum` is provided, then `patchNum` must + // also be provided. + // - `edit`, optional, Boolean: whether or not to load the file list with + // edit controls. + // - `messageHash`, optional, String: the hash of the change message to + // scroll to. + // + // - Gerrit.Nav.View.SEARCH: + // - `query`, optional, String: the literal search query. If provided, + // the string will be used as the query, and all other params will be + // ignored. + // - `owner`, optional, String: the owner name. + // - `project`, optional, String: the project name. + // - `branch`, optional, String: the branch name. + // - `topic`, optional, String: the topic name. + // - `hashtag`, optional, String: the hashtag name. + // - `statuses`, optional, Array<String>: the list of change statuses to + // search for. If more than one is provided, the search will OR them + // together. + // - `offset`, optional, Number: the offset for the query. + // + // - Gerrit.Nav.View.DIFF: + // - `changeNum`, required, String: the numeric ID of the change. + // - `path`, required, String: the filepath of the diff. + // - `patchNum`, required, Number: the patch for the right-hand-side of + // the diff. + // - `basePatchNum`, optional, Number: the patch for the left-hand-side + // of the diff. If `basePatchNum` is provided, then `patchNum` must + // also be provided. + // - `lineNum`, optional, Number: the line number to be selected on load. + // - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value + // of true selects the line from base of the patch range. False by + // default. + // + // - Gerrit.Nav.View.GROUP: + // - `groupId`, required, String: the ID of the group. + // - `detail`, optional, String: the name of the group detail view. + // Takes any value from Gerrit.Nav.GroupDetailView. + // + // - Gerrit.Nav.View.REPO: + // - `repoName`, required, String: the name of the repo + // - `detail`, optional, String: the name of the repo detail view. + // Takes any value from Gerrit.Nav.RepoDetailView. + // + // - Gerrit.Nav.View.DASHBOARD + // - `repo`, optional, String. + // - `sections`, optional, Array of objects with `title` and `query` + // strings. + // - `user`, optional, String. + // + // - Gerrit.Nav.View.ROOT: + // - no possible parameters. -http://www.apache.org/licenses/LICENSE-2.0 + window.Gerrit = window.Gerrit || {}; -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. ---> -<script> - (function(window) { - 'use strict'; + // Prevent redefinition. + if (window.Gerrit.hasOwnProperty('Nav')) { return; } - // Navigation parameters object format: - // - // Each object has a `view` property with a value from Gerrit.Nav.View. The - // remaining properties depend on the value used for view. - // - // - Gerrit.Nav.View.CHANGE: - // - `changeNum`, required, String: the numeric ID of the change. - // - `project`, optional, String: the project name. - // - `patchNum`, optional, Number: the patch for the right-hand-side of - // the diff. - // - `basePatchNum`, optional, Number: the patch for the left-hand-side - // of the diff. If `basePatchNum` is provided, then `patchNum` must - // also be provided. - // - `edit`, optional, Boolean: whether or not to load the file list with - // edit controls. - // - `messageHash`, optional, String: the hash of the change message to - // scroll to. - // - // - Gerrit.Nav.View.SEARCH: - // - `query`, optional, String: the literal search query. If provided, - // the string will be used as the query, and all other params will be - // ignored. - // - `owner`, optional, String: the owner name. - // - `project`, optional, String: the project name. - // - `branch`, optional, String: the branch name. - // - `topic`, optional, String: the topic name. - // - `hashtag`, optional, String: the hashtag name. - // - `statuses`, optional, Array<String>: the list of change statuses to - // search for. If more than one is provided, the search will OR them - // together. - // - `offset`, optional, Number: the offset for the query. - // - // - Gerrit.Nav.View.DIFF: - // - `changeNum`, required, String: the numeric ID of the change. - // - `path`, required, String: the filepath of the diff. - // - `patchNum`, required, Number: the patch for the right-hand-side of - // the diff. - // - `basePatchNum`, optional, Number: the patch for the left-hand-side - // of the diff. If `basePatchNum` is provided, then `patchNum` must - // also be provided. - // - `lineNum`, optional, Number: the line number to be selected on load. - // - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value - // of true selects the line from base of the patch range. False by - // default. - // - // - Gerrit.Nav.View.GROUP: - // - `groupId`, required, String: the ID of the group. - // - `detail`, optional, String: the name of the group detail view. - // Takes any value from Gerrit.Nav.GroupDetailView. - // - // - Gerrit.Nav.View.REPO: - // - `repoName`, required, String: the name of the repo - // - `detail`, optional, String: the name of the repo detail view. - // Takes any value from Gerrit.Nav.RepoDetailView. - // - // - Gerrit.Nav.View.DASHBOARD - // - `repo`, optional, String. - // - `sections`, optional, Array of objects with `title` and `query` - // strings. - // - `user`, optional, String. - // - // - Gerrit.Nav.View.ROOT: - // - no possible parameters. + const uninitialized = () => { + console.warn('Use of uninitialized routing'); + }; - window.Gerrit = window.Gerrit || {}; + const EDIT_PATCHNUM = 'edit'; + const PARENT_PATCHNUM = 'PARENT'; - // Prevent redefinition. - if (window.Gerrit.hasOwnProperty('Nav')) { return; } + const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g; - const uninitialized = () => { - console.warn('Use of uninitialized routing'); - }; + // NOTE: These queries are tested in Java. Any changes made to definitions + // here require corresponding changes to: + // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java + const DEFAULT_SECTIONS = [ + { + // Changes with unpublished draft comments. This section is omitted when + // viewing other users, so we don't need to filter anything out. + name: 'Has draft comments', + query: 'has:draft', + selfOnly: true, + hideIfEmpty: true, + suffixForDashboard: 'limit:10', + }, + { + // Changes that are assigned to the viewed user. + name: 'Assigned reviews', + query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' + + 'is:open -is:ignored', + hideIfEmpty: true, + suffixForDashboard: 'limit:25', + }, + { + // WIP open changes owned by viewing user. This section is omitted when + // viewing other users, so we don't need to filter anything out. + name: 'Work in progress', + query: 'is:open owner:${user} is:wip', + selfOnly: true, + hideIfEmpty: true, + suffixForDashboard: 'limit:25', + }, + { + // Non-WIP open changes owned by viewed user. Filter out changes ignored + // by the viewing user. + name: 'Outgoing reviews', + query: 'is:open owner:${user} -is:wip -is:ignored', + isOutgoing: true, + suffixForDashboard: 'limit:25', + }, + { + // Non-WIP open changes not owned by the viewed user, that the viewed user + // is associated with (as either a reviewer or the assignee). Changes + // ignored by the viewing user are filtered out. + name: 'Incoming reviews', + query: 'is:open -owner:${user} -is:wip -is:ignored ' + + '(reviewer:${user} OR assignee:${user})', + suffixForDashboard: 'limit:25', + }, + { + // Open changes the viewed user is CCed on. Changes ignored by the viewing + // user are filtered out. + name: 'CCed on', + query: 'is:open -is:ignored cc:${user}', + suffixForDashboard: 'limit:10', + }, + { + name: 'Recently closed', + // Closed changes where viewed user is owner, reviewer, or assignee. + // Changes ignored by the viewing user are filtered out, and so are WIP + // changes not owned by the viewing user (the one instance of + // 'owner:self' is intentional and implements this logic). + query: 'is:closed -is:ignored (-is:wip OR owner:self) ' + + '(owner:${user} OR reviewer:${user} OR assignee:${user} ' + + 'OR cc:${user})', + suffixForDashboard: '-age:4w limit:10', + }, + ]; - const EDIT_PATCHNUM = 'edit'; - const PARENT_PATCHNUM = 'PARENT'; + window.Gerrit.Nav = { - const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g; + View: { + ADMIN: 'admin', + AGREEMENTS: 'agreements', + CHANGE: 'change', + DASHBOARD: 'dashboard', + DIFF: 'diff', + DOCUMENTATION_SEARCH: 'documentation-search', + EDIT: 'edit', + GROUP: 'group', + PLUGIN_SCREEN: 'plugin-screen', + REPO: 'repo', + ROOT: 'root', + SEARCH: 'search', + SETTINGS: 'settings', + }, - // NOTE: These queries are tested in Java. Any changes made to definitions - // here require corresponding changes to: - // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java - const DEFAULT_SECTIONS = [ - { - // Changes with unpublished draft comments. This section is omitted when - // viewing other users, so we don't need to filter anything out. - name: 'Has draft comments', - query: 'has:draft', - selfOnly: true, - hideIfEmpty: true, - suffixForDashboard: 'limit:10', - }, - { - // Changes that are assigned to the viewed user. - name: 'Assigned reviews', - query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' + - 'is:open -is:ignored', - hideIfEmpty: true, - suffixForDashboard: 'limit:25', - }, - { - // WIP open changes owned by viewing user. This section is omitted when - // viewing other users, so we don't need to filter anything out. - name: 'Work in progress', - query: 'is:open owner:${user} is:wip', - selfOnly: true, - hideIfEmpty: true, - suffixForDashboard: 'limit:25', - }, - { - // Non-WIP open changes owned by viewed user. Filter out changes ignored - // by the viewing user. - name: 'Outgoing reviews', - query: 'is:open owner:${user} -is:wip -is:ignored', - isOutgoing: true, - suffixForDashboard: 'limit:25', - }, - { - // Non-WIP open changes not owned by the viewed user, that the viewed user - // is associated with (as either a reviewer or the assignee). Changes - // ignored by the viewing user are filtered out. - name: 'Incoming reviews', - query: 'is:open -owner:${user} -is:wip -is:ignored ' + - '(reviewer:${user} OR assignee:${user})', - suffixForDashboard: 'limit:25', - }, - { - // Open changes the viewed user is CCed on. Changes ignored by the viewing - // user are filtered out. - name: 'CCed on', - query: 'is:open -is:ignored cc:${user}', - suffixForDashboard: 'limit:10', - }, - { - name: 'Recently closed', - // Closed changes where viewed user is owner, reviewer, or assignee. - // Changes ignored by the viewing user are filtered out, and so are WIP - // changes not owned by the viewing user (the one instance of - // 'owner:self' is intentional and implements this logic). - query: 'is:closed -is:ignored (-is:wip OR owner:self) ' + - '(owner:${user} OR reviewer:${user} OR assignee:${user} ' + - 'OR cc:${user})', - suffixForDashboard: '-age:4w limit:10', - }, - ]; + GroupDetailView: { + MEMBERS: 'members', + LOG: 'log', + }, - window.Gerrit.Nav = { + RepoDetailView: { + ACCESS: 'access', + BRANCHES: 'branches', + COMMANDS: 'commands', + DASHBOARDS: 'dashboards', + TAGS: 'tags', + }, - View: { - ADMIN: 'admin', - AGREEMENTS: 'agreements', - CHANGE: 'change', - DASHBOARD: 'dashboard', - DIFF: 'diff', - DOCUMENTATION_SEARCH: 'documentation-search', - EDIT: 'edit', - GROUP: 'group', - PLUGIN_SCREEN: 'plugin-screen', - REPO: 'repo', - ROOT: 'root', - SEARCH: 'search', - SETTINGS: 'settings', - }, + WeblinkType: { + CHANGE: 'change', + FILE: 'file', + PATCHSET: 'patchset', + }, - GroupDetailView: { - MEMBERS: 'members', - LOG: 'log', - }, + /** @type {Function} */ + _navigate: uninitialized, - RepoDetailView: { - ACCESS: 'access', - BRANCHES: 'branches', - COMMANDS: 'commands', - DASHBOARDS: 'dashboards', - TAGS: 'tags', - }, + /** @type {Function} */ + _generateUrl: uninitialized, - WeblinkType: { - CHANGE: 'change', - FILE: 'file', - PATCHSET: 'patchset', - }, + /** @type {Function} */ + _generateWeblinks: uninitialized, - /** @type {Function} */ - _navigate: uninitialized, + /** @type {Function} */ + mapCommentlinks: uninitialized, - /** @type {Function} */ - _generateUrl: uninitialized, + /** + * @param {number=} patchNum + * @param {number|string=} basePatchNum + */ + _checkPatchRange(patchNum, basePatchNum) { + if (basePatchNum && !patchNum) { + throw new Error('Cannot use base patch number without patch number.'); + } + }, - /** @type {Function} */ - _generateWeblinks: uninitialized, + /** + * Setup router implementation. + * + * @param {function(!string)} navigate the router-abstracted equivalent of + * `window.location.href = ...`. Takes a string. + * @param {function(!Object): string} generateUrl generates a URL given + * navigation parameters, detailed in the file header. + * @param {function(!Object): string} generateWeblinks weblinks generator + * function takes single payload parameter with type property that + * determines which + * part of the UI is the consumer of the weblinks. type property can + * be one of file, change, or patchset. + * - For file type, payload will also contain string properties: repo, + * commit, file. + * - For patchset type, payload will also contain string properties: + * repo, commit. + * - For change type, payload will also contain string properties: + * repo, commit. If server provides weblinks, those will be passed + * as options.weblinks property on the main payload object. + * @param {function(!Object): Object} mapCommentlinks provides an escape + * hatch to modify the commentlinks object, e.g. if it contains any + * relative URLs. + */ + setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) { + this._navigate = navigate; + this._generateUrl = generateUrl; + this._generateWeblinks = generateWeblinks; + this.mapCommentlinks = mapCommentlinks; + }, - /** @type {Function} */ - mapCommentlinks: uninitialized, + destroy() { + this._navigate = uninitialized; + this._generateUrl = uninitialized; + this._generateWeblinks = uninitialized; + this.mapCommentlinks = uninitialized; + }, - /** - * @param {number=} patchNum - * @param {number|string=} basePatchNum - */ - _checkPatchRange(patchNum, basePatchNum) { - if (basePatchNum && !patchNum) { - throw new Error('Cannot use base patch number without patch number.'); - } - }, + /** + * Generate a URL for the given route parameters. + * + * @param {Object} params + * @return {string} + */ + _getUrlFor(params) { + return this._generateUrl(params); + }, - /** - * Setup router implementation. - * - * @param {function(!string)} navigate the router-abstracted equivalent of - * `window.location.href = ...`. Takes a string. - * @param {function(!Object): string} generateUrl generates a URL given - * navigation parameters, detailed in the file header. - * @param {function(!Object): string} generateWeblinks weblinks generator - * function takes single payload parameter with type property that - * determines which - * part of the UI is the consumer of the weblinks. type property can - * be one of file, change, or patchset. - * - For file type, payload will also contain string properties: repo, - * commit, file. - * - For patchset type, payload will also contain string properties: - * repo, commit. - * - For change type, payload will also contain string properties: - * repo, commit. If server provides weblinks, those will be passed - * as options.weblinks property on the main payload object. - * @param {function(!Object): Object} mapCommentlinks provides an escape - * hatch to modify the commentlinks object, e.g. if it contains any - * relative URLs. - */ - setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) { - this._navigate = navigate; - this._generateUrl = generateUrl; - this._generateWeblinks = generateWeblinks; - this.mapCommentlinks = mapCommentlinks; - }, + getUrlForSearchQuery(query, opt_offset) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + query, + offset: opt_offset, + }); + }, - destroy() { - this._navigate = uninitialized; - this._generateUrl = uninitialized; - this._generateWeblinks = uninitialized; - this.mapCommentlinks = uninitialized; - }, + /** + * @param {!string} project The name of the project. + * @param {boolean=} opt_openOnly When true, only search open changes in + * the project. + * @param {string=} opt_host The host in which to search. + * @return {string} + */ + getUrlForProjectChanges(project, opt_openOnly, opt_host) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + project, + statuses: opt_openOnly ? ['open'] : [], + host: opt_host, + }); + }, - /** - * Generate a URL for the given route parameters. - * - * @param {Object} params - * @return {string} - */ - _getUrlFor(params) { - return this._generateUrl(params); - }, + /** + * @param {string} branch The name of the branch. + * @param {string} project The name of the project. + * @param {string=} opt_status The status to search. + * @param {string=} opt_host The host in which to search. + * @return {string} + */ + getUrlForBranch(branch, project, opt_status, opt_host) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + branch, + project, + statuses: opt_status ? [opt_status] : undefined, + host: opt_host, + }); + }, - getUrlForSearchQuery(query, opt_offset) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - query, - offset: opt_offset, - }); - }, + /** + * @param {string} topic The name of the topic. + * @param {string=} opt_host The host in which to search. + * @return {string} + */ + getUrlForTopic(topic, opt_host) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + topic, + statuses: ['open', 'merged'], + host: opt_host, + }); + }, - /** - * @param {!string} project The name of the project. - * @param {boolean=} opt_openOnly When true, only search open changes in - * the project. - * @param {string=} opt_host The host in which to search. - * @return {string} - */ - getUrlForProjectChanges(project, opt_openOnly, opt_host) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - project, - statuses: opt_openOnly ? ['open'] : [], - host: opt_host, - }); - }, + /** + * @param {string} hashtag The name of the hashtag. + * @return {string} + */ + getUrlForHashtag(hashtag) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + hashtag, + statuses: ['open', 'merged'], + }); + }, - /** - * @param {string} branch The name of the branch. - * @param {string} project The name of the project. - * @param {string=} opt_status The status to search. - * @param {string=} opt_host The host in which to search. - * @return {string} - */ - getUrlForBranch(branch, project, opt_status, opt_host) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - branch, - project, - statuses: opt_status ? [opt_status] : undefined, - host: opt_host, - }); - }, + /** + * Navigate to a search for changes with the given status. + * + * @param {string} status + */ + navigateToStatusSearch(status) { + this._navigate(this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + statuses: [status], + })); + }, - /** - * @param {string} topic The name of the topic. - * @param {string=} opt_host The host in which to search. - * @return {string} - */ - getUrlForTopic(topic, opt_host) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - topic, - statuses: ['open', 'merged'], - host: opt_host, - }); - }, + /** + * Navigate to a search query + * + * @param {string} query + * @param {number=} opt_offset + */ + navigateToSearchQuery(query, opt_offset) { + return this._navigate(this.getUrlForSearchQuery(query, opt_offset)); + }, - /** - * @param {string} hashtag The name of the hashtag. - * @return {string} - */ - getUrlForHashtag(hashtag) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - hashtag, - statuses: ['open', 'merged'], - }); - }, + /** + * Navigate to the user's dashboard + */ + navigateToUserDashboard() { + return this._navigate(this.getUrlForUserDashboard('self')); + }, - /** - * Navigate to a search for changes with the given status. - * - * @param {string} status - */ - navigateToStatusSearch(status) { - this._navigate(this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - statuses: [status], - })); - }, + /** + * @param {!Object} change The change object. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {boolean=} opt_isEdit + * @param {string=} opt_messageHash + * @return {string} + */ + getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit, + opt_messageHash) { + if (opt_basePatchNum === PARENT_PATCHNUM) { + opt_basePatchNum = undefined; + } - /** - * Navigate to a search query - * - * @param {string} query - * @param {number=} opt_offset - */ - navigateToSearchQuery(query, opt_offset) { - return this._navigate(this.getUrlForSearchQuery(query, opt_offset)); - }, + this._checkPatchRange(opt_patchNum, opt_basePatchNum); + return this._getUrlFor({ + view: Gerrit.Nav.View.CHANGE, + changeNum: change._number, + project: change.project, + patchNum: opt_patchNum, + basePatchNum: opt_basePatchNum, + edit: opt_isEdit, + host: change.internalHost || undefined, + messageHash: opt_messageHash, + }); + }, - /** - * Navigate to the user's dashboard - */ - navigateToUserDashboard() { - return this._navigate(this.getUrlForUserDashboard('self')); - }, + /** + * @param {number} changeNum + * @param {string} project The name of the project. + * @param {number=} opt_patchNum + * @return {string} + */ + getUrlForChangeById(changeNum, project, opt_patchNum) { + return this._getUrlFor({ + view: Gerrit.Nav.View.CHANGE, + changeNum, + project, + patchNum: opt_patchNum, + }); + }, - /** - * @param {!Object} change The change object. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {boolean=} opt_isEdit - * @param {string=} opt_messageHash - * @return {string} - */ - getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit, - opt_messageHash) { - if (opt_basePatchNum === PARENT_PATCHNUM) { - opt_basePatchNum = undefined; - } + /** + * @param {!Object} change The change object. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {boolean=} opt_isEdit + */ + navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) { + this._navigate(this.getUrlForChange(change, opt_patchNum, + opt_basePatchNum, opt_isEdit)); + }, - this._checkPatchRange(opt_patchNum, opt_basePatchNum); - return this._getUrlFor({ - view: Gerrit.Nav.View.CHANGE, - changeNum: change._number, - project: change.project, - patchNum: opt_patchNum, - basePatchNum: opt_basePatchNum, - edit: opt_isEdit, - host: change.internalHost || undefined, - messageHash: opt_messageHash, - }); - }, + /** + * @param {{ _number: number, project: string }} change The change object. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {number|string=} opt_lineNum + * @return {string} + */ + getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) { + return this.getUrlForDiffById(change._number, change.project, path, + opt_patchNum, opt_basePatchNum, opt_lineNum); + }, - /** - * @param {number} changeNum - * @param {string} project The name of the project. - * @param {number=} opt_patchNum - * @return {string} - */ - getUrlForChangeById(changeNum, project, opt_patchNum) { - return this._getUrlFor({ - view: Gerrit.Nav.View.CHANGE, - changeNum, - project, - patchNum: opt_patchNum, - }); - }, + /** + * @param {number} changeNum + * @param {string} project The name of the project. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {number=} opt_lineNum + * @param {boolean=} opt_leftSide + * @return {string} + */ + getUrlForDiffById(changeNum, project, path, opt_patchNum, + opt_basePatchNum, opt_lineNum, opt_leftSide) { + if (opt_basePatchNum === PARENT_PATCHNUM) { + opt_basePatchNum = undefined; + } - /** - * @param {!Object} change The change object. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {boolean=} opt_isEdit - */ - navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) { - this._navigate(this.getUrlForChange(change, opt_patchNum, - opt_basePatchNum, opt_isEdit)); - }, + this._checkPatchRange(opt_patchNum, opt_basePatchNum); + return this._getUrlFor({ + view: Gerrit.Nav.View.DIFF, + changeNum, + project, + path, + patchNum: opt_patchNum, + basePatchNum: opt_basePatchNum, + lineNum: opt_lineNum, + leftSide: opt_leftSide, + }); + }, - /** - * @param {{ _number: number, project: string }} change The change object. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {number|string=} opt_lineNum - * @return {string} - */ - getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) { - return this.getUrlForDiffById(change._number, change.project, path, - opt_patchNum, opt_basePatchNum, opt_lineNum); - }, + /** + * @param {{ _number: number, project: string }} change The change object. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @return {string} + */ + getEditUrlForDiff(change, path, opt_patchNum) { + return this.getEditUrlForDiffById(change._number, change.project, path, + opt_patchNum); + }, - /** - * @param {number} changeNum - * @param {string} project The name of the project. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {number=} opt_lineNum - * @param {boolean=} opt_leftSide - * @return {string} - */ - getUrlForDiffById(changeNum, project, path, opt_patchNum, - opt_basePatchNum, opt_lineNum, opt_leftSide) { - if (opt_basePatchNum === PARENT_PATCHNUM) { - opt_basePatchNum = undefined; - } + /** + * @param {number} changeNum + * @param {string} project The name of the project. + * @param {string} path The file path. + * @param {number|string=} opt_patchNum The patchNum the file content + * should be based on, or ${EDIT_PATCHNUM} if left undefined. + * @return {string} + */ + getEditUrlForDiffById(changeNum, project, path, opt_patchNum) { + return this._getUrlFor({ + view: Gerrit.Nav.View.EDIT, + changeNum, + project, + path, + patchNum: opt_patchNum || EDIT_PATCHNUM, + }); + }, - this._checkPatchRange(opt_patchNum, opt_basePatchNum); - return this._getUrlFor({ - view: Gerrit.Nav.View.DIFF, - changeNum, - project, - path, - patchNum: opt_patchNum, - basePatchNum: opt_basePatchNum, - lineNum: opt_lineNum, - leftSide: opt_leftSide, - }); - }, + /** + * @param {!Object} change The change object. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + */ + navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) { + this._navigate(this.getUrlForDiff(change, path, opt_patchNum, + opt_basePatchNum)); + }, - /** - * @param {{ _number: number, project: string }} change The change object. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @return {string} - */ - getEditUrlForDiff(change, path, opt_patchNum) { - return this.getEditUrlForDiffById(change._number, change.project, path, - opt_patchNum); - }, + /** + * @param {string} owner The name of the owner. + * @return {string} + */ + getUrlForOwner(owner) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + owner, + }); + }, - /** - * @param {number} changeNum - * @param {string} project The name of the project. - * @param {string} path The file path. - * @param {number|string=} opt_patchNum The patchNum the file content - * should be based on, or ${EDIT_PATCHNUM} if left undefined. - * @return {string} - */ - getEditUrlForDiffById(changeNum, project, path, opt_patchNum) { - return this._getUrlFor({ - view: Gerrit.Nav.View.EDIT, - changeNum, - project, - path, - patchNum: opt_patchNum || EDIT_PATCHNUM, - }); - }, + /** + * @param {string} user The name of the user. + * @return {string} + */ + getUrlForUserDashboard(user) { + return this._getUrlFor({ + view: Gerrit.Nav.View.DASHBOARD, + user, + }); + }, - /** - * @param {!Object} change The change object. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - */ - navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) { - this._navigate(this.getUrlForDiff(change, path, opt_patchNum, - opt_basePatchNum)); - }, + /** + * @return {string} + */ + getUrlForRoot() { + return this._getUrlFor({ + view: Gerrit.Nav.View.ROOT, + }); + }, - /** - * @param {string} owner The name of the owner. - * @return {string} - */ - getUrlForOwner(owner) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - owner, - }); - }, + /** + * @param {string} repo The name of the repo. + * @param {string} dashboard The ID of the dashboard, in the form of + * '<ref>:<path>'. + * @return {string} + */ + getUrlForRepoDashboard(repo, dashboard) { + return this._getUrlFor({ + view: Gerrit.Nav.View.DASHBOARD, + repo, + dashboard, + }); + }, - /** - * @param {string} user The name of the user. - * @return {string} - */ - getUrlForUserDashboard(user) { - return this._getUrlFor({ - view: Gerrit.Nav.View.DASHBOARD, - user, - }); - }, + /** + * Navigate to an arbitrary relative URL. + * + * @param {string} relativeUrl + */ + navigateToRelativeUrl(relativeUrl) { + if (!relativeUrl.startsWith('/')) { + throw new Error('navigateToRelativeUrl with non-relative URL'); + } + this._navigate(relativeUrl); + }, - /** - * @return {string} - */ - getUrlForRoot() { - return this._getUrlFor({ - view: Gerrit.Nav.View.ROOT, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepo(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + }); + }, - /** - * @param {string} repo The name of the repo. - * @param {string} dashboard The ID of the dashboard, in the form of - * '<ref>:<path>'. - * @return {string} - */ - getUrlForRepoDashboard(repo, dashboard) { - return this._getUrlFor({ - view: Gerrit.Nav.View.DASHBOARD, - repo, - dashboard, - }); - }, + /** + * Navigate to a repo settings page. + * + * @param {string} repoName + */ + navigateToRepo(repoName) { + this._navigate(this.getUrlForRepo(repoName)); + }, - /** - * Navigate to an arbitrary relative URL. - * - * @param {string} relativeUrl - */ - navigateToRelativeUrl(relativeUrl) { - if (!relativeUrl.startsWith('/')) { - throw new Error('navigateToRelativeUrl with non-relative URL'); - } - this._navigate(relativeUrl); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoTags(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.TAGS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepo(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoBranches(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + }); + }, - /** - * Navigate to a repo settings page. - * - * @param {string} repoName - */ - navigateToRepo(repoName) { - this._navigate(this.getUrlForRepo(repoName)); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoAccess(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoTags(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.TAGS, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoCommands(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.COMMANDS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoBranches(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoDashboards(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoAccess(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - }); - }, + /** + * @param {string} groupId + * @return {string} + */ + getUrlForGroup(groupId) { + return this._getUrlFor({ + view: Gerrit.Nav.View.GROUP, + groupId, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoCommands(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.COMMANDS, - }); - }, + /** + * @param {string} groupId + * @return {string} + */ + getUrlForGroupLog(groupId) { + return this._getUrlFor({ + view: Gerrit.Nav.View.GROUP, + groupId, + detail: Gerrit.Nav.GroupDetailView.LOG, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoDashboards(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, - }); - }, + /** + * @param {string} groupId + * @return {string} + */ + getUrlForGroupMembers(groupId) { + return this._getUrlFor({ + view: Gerrit.Nav.View.GROUP, + groupId, + detail: Gerrit.Nav.GroupDetailView.MEMBERS, + }); + }, - /** - * @param {string} groupId - * @return {string} - */ - getUrlForGroup(groupId) { - return this._getUrlFor({ - view: Gerrit.Nav.View.GROUP, - groupId, - }); - }, + getUrlForSettings() { + return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS}); + }, - /** - * @param {string} groupId - * @return {string} - */ - getUrlForGroupLog(groupId) { - return this._getUrlFor({ - view: Gerrit.Nav.View.GROUP, - groupId, - detail: Gerrit.Nav.GroupDetailView.LOG, - }); - }, + /** + * @param {string} repo + * @param {string} commit + * @param {string} file + * @param {Object=} opt_options + * @return { + * Array<{label: string, url: string}>| + * {label: string, url: string} + * } + */ + getFileWebLinks(repo, commit, file, opt_options) { + const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file}; + if (opt_options) { + params.options = opt_options; + } + return [].concat(this._generateWeblinks(params)); + }, - /** - * @param {string} groupId - * @return {string} - */ - getUrlForGroupMembers(groupId) { - return this._getUrlFor({ - view: Gerrit.Nav.View.GROUP, - groupId, - detail: Gerrit.Nav.GroupDetailView.MEMBERS, - }); - }, + /** + * @param {string} repo + * @param {string} commit + * @param {Object=} opt_options + * @return {{label: string, url: string}} + */ + getPatchSetWeblink(repo, commit, opt_options) { + const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit}; + if (opt_options) { + params.options = opt_options; + } + const result = this._generateWeblinks(params); + if (Array.isArray(result)) { + return result.pop(); + } else { + return result; + } + }, - getUrlForSettings() { - return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS}); - }, + /** + * @param {string} repo + * @param {string} commit + * @param {Object=} opt_options + * @return { + * Array<{label: string, url: string}>| + * {label: string, url: string} + * } + */ + getChangeWeblinks(repo, commit, opt_options) { + const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit}; + if (opt_options) { + params.options = opt_options; + } + return [].concat(this._generateWeblinks(params)); + }, - /** - * @param {string} repo - * @param {string} commit - * @param {string} file - * @param {Object=} opt_options - * @return { - * Array<{label: string, url: string}>| - * {label: string, url: string} - * } - */ - getFileWebLinks(repo, commit, file, opt_options) { - const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file}; - if (opt_options) { - params.options = opt_options; - } - return [].concat(this._generateWeblinks(params)); - }, - - /** - * @param {string} repo - * @param {string} commit - * @param {Object=} opt_options - * @return {{label: string, url: string}} - */ - getPatchSetWeblink(repo, commit, opt_options) { - const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit}; - if (opt_options) { - params.options = opt_options; - } - const result = this._generateWeblinks(params); - if (Array.isArray(result)) { - return result.pop(); - } else { - return result; - } - }, - - /** - * @param {string} repo - * @param {string} commit - * @param {Object=} opt_options - * @return { - * Array<{label: string, url: string}>| - * {label: string, url: string} - * } - */ - getChangeWeblinks(repo, commit, opt_options) { - const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit}; - if (opt_options) { - params.options = opt_options; - } - return [].concat(this._generateWeblinks(params)); - }, - - getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS, - title = '') { - sections = sections - .filter(section => (user === 'self' || !section.selfOnly)) - .map(section => Object.assign({}, section, { - name: section.name, - query: section.query.replace(USER_PLACEHOLDER_PATTERN, user), - })); - return {title, sections}; - }, - }; - })(window); -</script> + getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS, + title = '') { + sections = sections + .filter(section => (user === 'self' || !section.selfOnly)) + .map(section => Object.assign({}, section, { + name: section.name, + query: section.query.replace(USER_PLACEHOLDER_PATTERN, user), + })); + return {title, sections}; + }, + }; +})(window);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html index f58780c..8f3c623 100644 --- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -19,70 +19,71 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-navigation</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<script> - suite('gr-navigation tests', async () => { - await readyToTest(); - test('invalid patch ranges throw exceptions', () => { - assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12)); - assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12)); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +suite('gr-navigation tests', () => { + test('invalid patch ranges throw exceptions', () => { + assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12)); + assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12)); + }); + + suite('_getUserDashboard', () => { + const sections = [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2 for ${user}'}, + {name: 'section 3', query: 'self only query', selfOnly: true}, + {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'}, + ]; + + test('dashboard for self', () => { + const dashboard = + Gerrit.Nav.getUserDashboard('self', sections, 'title'); + assert.deepEqual( + dashboard, + { + title: 'title', + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2 for self'}, + { + name: 'section 3', + query: 'self only query', + selfOnly: true, + }, { + name: 'section 4', + query: 'query 4', + suffixForDashboard: 'suffix', + }, + ], + }); }); - suite('_getUserDashboard', () => { - const sections = [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2 for ${user}'}, - {name: 'section 3', query: 'self only query', selfOnly: true}, - {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'}, - ]; - - test('dashboard for self', () => { - const dashboard = - Gerrit.Nav.getUserDashboard('self', sections, 'title'); - assert.deepEqual( - dashboard, - { - title: 'title', - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2 for self'}, - { - name: 'section 3', - query: 'self only query', - selfOnly: true, - }, { - name: 'section 4', - query: 'query 4', - suffixForDashboard: 'suffix', - }, - ], - }); - }); - - test('dashboard for other user', () => { - const dashboard = - Gerrit.Nav.getUserDashboard('user', sections, 'title'); - assert.deepEqual( - dashboard, - { - title: 'title', - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2 for user'}, - { - name: 'section 4', - query: 'query 4', - suffixForDashboard: 'suffix', - }, - ], - }); - }); + test('dashboard for other user', () => { + const dashboard = + Gerrit.Nav.getUserDashboard('user', sections, 'title'); + assert.deepEqual( + dashboard, + { + title: 'title', + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2 for user'}, + { + name: 'section 4', + query: 'query 4', + suffixForDashboard: 'suffix', + }, + ], + }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html deleted file mode 100644 index 0ba8a22..0000000 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html +++ /dev/null
@@ -1,22 +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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-reporting"> - <script src="gr-reporting.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js index 106508c..55f8abd 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -14,566 +14,566 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // Latency reporting constants. - const TIMING = { - TYPE: 'timing-report', - CATEGORY_UI_LATENCY: 'UI Latency', - CATEGORY_RPC: 'RPC Timing', - // Reported events - alphabetize below. - APP_STARTED: 'App Started', - }; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; - // Plugin-related reporting constants. - const PLUGINS = { - TYPE: 'lifecycle', - // Reported events - alphabetize below. - INSTALLED: 'Plugins installed', - }; +// Latency reporting constants. +const TIMING = { + TYPE: 'timing-report', + CATEGORY_UI_LATENCY: 'UI Latency', + CATEGORY_RPC: 'RPC Timing', + // Reported events - alphabetize below. + APP_STARTED: 'App Started', +}; - // Chrome extension-related reporting constants. - const EXTENSION = { - TYPE: 'lifecycle', - // Reported events - alphabetize below. - DETECTED: 'Extension detected', - }; +// Plugin-related reporting constants. +const PLUGINS = { + TYPE: 'lifecycle', + // Reported events - alphabetize below. + INSTALLED: 'Plugins installed', +}; - // Navigation reporting constants. - const NAVIGATION = { - TYPE: 'nav-report', - CATEGORY: 'Location Changed', - PAGE: 'Page', - }; +// Chrome extension-related reporting constants. +const EXTENSION = { + TYPE: 'lifecycle', + // Reported events - alphabetize below. + DETECTED: 'Extension detected', +}; - const ERROR = { - TYPE: 'error', - CATEGORY: 'exception', - }; +// Navigation reporting constants. +const NAVIGATION = { + TYPE: 'nav-report', + CATEGORY: 'Location Changed', + PAGE: 'Page', +}; - const ERROR_DIALOG = { - TYPE: 'error', - CATEGORY: 'Error Dialog', - }; +const ERROR = { + TYPE: 'error', + CATEGORY: 'exception', +}; - const TIMER = { - CHANGE_DISPLAYED: 'ChangeDisplayed', - CHANGE_LOAD_FULL: 'ChangeFullyLoaded', - DASHBOARD_DISPLAYED: 'DashboardDisplayed', - DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent', - DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed', - DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded', - FILE_LIST_DISPLAYED: 'FileListDisplayed', - PLUGINS_LOADED: 'PluginsLoaded', - STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed', - STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded', - STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed', - STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent', - STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed', - STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded', - STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed', - WEB_COMPONENTS_READY: 'WebComponentsReady', - METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded', - }; +const ERROR_DIALOG = { + TYPE: 'error', + CATEGORY: 'Error Dialog', +}; - const STARTUP_TIMERS = {}; - STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0; - STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0; - STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0; - STARTUP_TIMERS[TIMING.APP_STARTED] = 0; - // WebComponentsReady timer is triggered from gr-router. - STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0; +const TIMER = { + CHANGE_DISPLAYED: 'ChangeDisplayed', + CHANGE_LOAD_FULL: 'ChangeFullyLoaded', + DASHBOARD_DISPLAYED: 'DashboardDisplayed', + DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent', + DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed', + DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded', + FILE_LIST_DISPLAYED: 'FileListDisplayed', + PLUGINS_LOADED: 'PluginsLoaded', + STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed', + STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded', + STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed', + STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent', + STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed', + STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded', + STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed', + WEB_COMPONENTS_READY: 'WebComponentsReady', + METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded', +}; - const INTERACTION_TYPE = 'interaction'; +const STARTUP_TIMERS = {}; +STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0; +STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0; +STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0; +STARTUP_TIMERS[TIMING.APP_STARTED] = 0; +// WebComponentsReady timer is triggered from gr-router. +STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0; - const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions'; - const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes. +const INTERACTION_TYPE = 'interaction'; - let pending = []; - let slowRpcList = []; - const SLOW_RPC_THRESHOLD = 500; +const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions'; +const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes. - // Variables that hold context info in global scope - let reportRepoName = undefined; +let pending = []; +let slowRpcList = []; +const SLOW_RPC_THRESHOLD = 500; - const onError = function(oldOnError, msg, url, line, column, error) { - if (oldOnError) { - oldOnError(msg, url, line, column, error); +// Variables that hold context info in global scope +let reportRepoName = undefined; + +const onError = function(oldOnError, msg, url, line, column, error) { + if (oldOnError) { + oldOnError(msg, url, line, column, error); + } + if (error) { + line = line || error.lineNumber; + column = column || error.columnNumber; + let shortenedErrorStack = msg; + if (error.stack) { + const errorStackLines = error.stack.split('\n'); + shortenedErrorStack = errorStackLines.slice(0, + Math.min(3, errorStackLines.length)).join('\n'); } - if (error) { - line = line || error.lineNumber; - column = column || error.columnNumber; - let shortenedErrorStack = msg; - if (error.stack) { - const errorStackLines = error.stack.split('\n'); - shortenedErrorStack = errorStackLines.slice(0, - Math.min(3, errorStackLines.length)).join('\n'); - } - msg = shortenedErrorStack || error.toString(); - } + msg = shortenedErrorStack || error.toString(); + } + const payload = { + url, + line, + column, + error, + }; + GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); + return true; +}; + +const catchErrors = function(opt_context) { + const context = opt_context || window; + context.onerror = onError.bind(null, context.onerror); + context.addEventListener('unhandledrejection', e => { + const msg = e.reason.message; const payload = { - url, - line, - column, - error, + error: e.reason, }; GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); - return true; - }; + }); +}; +catchErrors(); - const catchErrors = function(opt_context) { - const context = opt_context || window; - context.onerror = onError.bind(null, context.onerror); - context.addEventListener('unhandledrejection', e => { - const msg = e.reason.message; - const payload = { - error: e.reason, - }; - GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); +// PerformanceObserver interface is a browser API. +if (window.PerformanceObserver) { + const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || []; + // Safari doesn't support longtask yet + if (supportedEntryTypes.includes('longtask')) { + const catchLongJsTasks = new PerformanceObserver(list => { + for (const task of list.getEntries()) { + // We are interested in longtask longer than 200 ms (default is 50 ms) + if (task.duration > 200) { + GrReporting.prototype.reporter(TIMING.TYPE, + TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`, + Math.round(task.duration), {}, false); + } + } }); - }; - catchErrors(); - - // PerformanceObserver interface is a browser API. - if (window.PerformanceObserver) { - const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || []; - // Safari doesn't support longtask yet - if (supportedEntryTypes.includes('longtask')) { - const catchLongJsTasks = new PerformanceObserver(list => { - for (const task of list.getEntries()) { - // We are interested in longtask longer than 200 ms (default is 50 ms) - if (task.duration > 200) { - GrReporting.prototype.reporter(TIMING.TYPE, - TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`, - Math.round(task.duration), {}, false); - } - } - }); - catchLongJsTasks.observe({entryTypes: ['longtask']}); - } + catchLongJsTasks.observe({entryTypes: ['longtask']}); } +} - document.addEventListener('visibilitychange', () => { - const eventName = `Visibility changed to ${document.visibilityState}`; - GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName, - undefined, {}, true); - }); +document.addEventListener('visibilitychange', () => { + const eventName = `Visibility changed to ${document.visibilityState}`; + GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName, + undefined, {}, true); +}); - // The Polymer pass of JSCompiler requires this to be reassignable - // eslint-disable-next-line prefer-const - let GrReporting = Polymer({ - is: 'gr-reporting', +// The Polymer pass of JSCompiler requires this to be reassignable +// eslint-disable-next-line prefer-const +let GrReporting = Polymer({ + is: 'gr-reporting', - properties: { - category: String, + properties: { + category: String, - _baselines: { - type: Object, - value: STARTUP_TIMERS, // Shared across all instances. + _baselines: { + type: Object, + value: STARTUP_TIMERS, // Shared across all instances. + }, + + _timers: { + type: Object, + value: {timeBetweenDraftActions: null}, // Shared across all instances. + }, + }, + + get performanceTiming() { + return window.performance.timing; + }, + + get slowRpcSnapshot() { + return slowRpcList.slice(); + }, + + now() { + return Math.round(window.performance.now()); + }, + + _arePluginsLoaded() { + return this._baselines && + !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED); + }, + + _isMetricsPluginLoaded() { + return this._arePluginsLoaded() || this._baselines && + !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED); + }, + + /** + * Reporter reports events. Events will be queued if metrics plugin is not + * yet installed. + * + * @param {string} type + * @param {string} category + * @param {string} eventName + * @param {string|number} eventValue + * @param {Object} eventDetails + * @param {boolean|undefined} opt_noLog If true, the event will not be + * logged to the JS console. + */ + reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) { + const eventInfo = this._createEventInfo(type, category, + eventName, eventValue, eventDetails); + if (type === ERROR.TYPE && category === ERROR.CATEGORY) { + console.error(eventValue && eventValue.error || eventName); + } + + // We report events immediately when metrics plugin is loaded + if (this._isMetricsPluginLoaded() && !pending.length) { + this._reportEvent(eventInfo, opt_noLog); + } else { + // We cache until metrics plugin is loaded + pending.push([eventInfo, opt_noLog]); + if (this._isMetricsPluginLoaded()) { + pending.forEach(([eventInfo, opt_noLog]) => { + this._reportEvent(eventInfo, opt_noLog); + }); + pending = []; + } + } + }, + + _reportEvent(eventInfo, opt_noLog) { + const {type, value, name} = eventInfo; + document.dispatchEvent(new CustomEvent(type, {detail: eventInfo})); + if (opt_noLog) { return; } + if (type !== ERROR.TYPE) { + if (value !== undefined) { + console.log(`Reporting: ${name}: ${value}`); + } else { + console.log(`Reporting: ${name}`); + } + } + }, + + _createEventInfo(type, category, name, value, eventDetails) { + const eventInfo = { + type, + category, + name, + value, + eventStart: this.now(), + }; + + if (typeof(eventDetails) === 'object' && + Object.entries(eventDetails).length !== 0) { + eventInfo.eventDetails = JSON.stringify(eventDetails); + } + if (reportRepoName) { + eventInfo.repoName = reportRepoName; + } + const isInBackgroundTab = document.visibilityState === 'hidden'; + if (isInBackgroundTab !== undefined) { + eventInfo.inBackgroundTab = isInBackgroundTab; + } + + return eventInfo; + }, + + /** + * User-perceived app start time, should be reported when the app is ready. + */ + appStarted() { + this.timeEnd(TIMING.APP_STARTED); + this.pageLoaded(); + }, + + /** + * Page load time and other metrics, should be reported at any time + * after navigation. + */ + pageLoaded() { + if (this.performanceTiming.loadEventEnd === 0) { + console.error('pageLoaded should be called after window.onload'); + this.async(this.pageLoaded, 100); + } else { + const perfEvents = Object.keys(this.performanceTiming.toJSON()); + perfEvents.forEach( + eventName => this._reportPerformanceTiming(eventName) + ); + } + }, + + _reportPerformanceTiming(eventName, eventDetails) { + const eventTiming = this.performanceTiming[eventName]; + if (eventTiming > 0) { + const elapsedTime = eventTiming - + this.performanceTiming.navigationStart; + // NavResTime - Navigation and resource timings. + this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, + `NavResTime - ${eventName}`, elapsedTime, eventDetails, true); + } + }, + + beforeLocationChanged() { + for (const prop of Object.keys(this._baselines)) { + delete this._baselines[prop]; + } + this.time(TIMER.CHANGE_DISPLAYED); + this.time(TIMER.CHANGE_LOAD_FULL); + this.time(TIMER.DASHBOARD_DISPLAYED); + this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); + this.time(TIMER.DIFF_VIEW_DISPLAYED); + this.time(TIMER.DIFF_VIEW_LOAD_FULL); + this.time(TIMER.FILE_LIST_DISPLAYED); + reportRepoName = undefined; + // reset slow rpc list since here start page loads which report these rpcs + slowRpcList = []; + }, + + locationChanged(page) { + this.reporter( + NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page); + }, + + dashboardDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } else { + this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } + }, + + changeDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } else { + this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } + }, + + changeFullyLoaded() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) { + this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL); + } else { + this.timeEnd(TIMER.CHANGE_LOAD_FULL); + } + }, + + diffViewDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } else { + this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } + }, + + diffViewFullyLoaded() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) { + this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL); + } else { + this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL); + } + }, + + diffViewContentDisplayed() { + if (this._baselines.hasOwnProperty( + TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED); + } else { + this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); + } + }, + + fileListDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED); + } else { + this.timeEnd(TIMER.FILE_LIST_DISPLAYED); + } + }, + + reportExtension(name) { + this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name); + }, + + pluginLoaded(name) { + if (name.startsWith('metrics-')) { + this.timeEnd(TIMER.METRICS_PLUGIN_LOADED); + } + }, + + pluginsLoaded(pluginsList) { + this.timeEnd(TIMER.PLUGINS_LOADED); + this.reporter( + PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined, + {pluginsList: pluginsList || []}, true); + }, + + /** + * Reset named timer. + */ + time(name) { + this._baselines[name] = this.now(); + window.performance.mark(`${name}-start`); + }, + + /** + * Finish named timer and report it to server. + */ + timeEnd(name, eventDetails) { + if (!this._baselines.hasOwnProperty(name)) { return; } + const baseTime = this._baselines[name]; + delete this._baselines[name]; + this._reportTiming(name, this.now() - baseTime, eventDetails); + + // Finalize the interval. Either from a registered start mark or + // the navigation start time (if baseTime is 0). + if (baseTime !== 0) { + window.performance.measure(name, `${name}-start`); + } else { + // Microsft Edge does not handle the 2nd param correctly + // (if undefined). + window.performance.measure(name); + } + }, + + /** + * Reports just line timeEnd, but additionally reports an average given a + * denominator and a separate reporiting name for the average. + * + * @param {string} name Timing name. + * @param {string} averageName Average timing name. + * @param {number} denominator Number by which to divide the total to + * compute the average. + */ + timeEndWithAverage(name, averageName, denominator) { + if (!this._baselines.hasOwnProperty(name)) { return; } + const baseTime = this._baselines[name]; + this.timeEnd(name); + + // Guard against division by zero. + if (!denominator) { return; } + const time = this.now() - baseTime; + this._reportTiming(averageName, time / denominator); + }, + + /** + * Send a timing report with an arbitrary time value. + * + * @param {string} name Timing name. + * @param {number} time The time to report as an integer of milliseconds. + * @param {Object} eventDetails non sensitive details + */ + _reportTiming(name, time, eventDetails) { + this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time, + eventDetails); + }, + + /** + * Get a timer object to for reporing a user timing. The start time will be + * the time that the object has been created, and the end time will be the + * time that the "end" method is called on the object. + * + * @param {string} name Timing name. + * @returns {!Object} The timer object. + */ + getTimer(name) { + let called = false; + let start; + let max = null; + + const timer = { + + // Clear the timer and reset the start time. + reset: () => { + called = false; + start = this.now(); + return timer; }, - _timers: { - type: Object, - value: {timeBetweenDraftActions: null}, // Shared across all instances. + // Stop the timer and report the intervening time. + end: () => { + if (called) { + throw new Error(`Timer for "${name}" already ended.`); + } + called = true; + const time = this.now() - start; + + // If a maximum is specified and the time exceeds it, do not report. + if (max && time > max) { return timer; } + + this._reportTiming(name, time); + return timer; }, - }, - get performanceTiming() { - return window.performance.timing; - }, + // Set a maximum reportable time. If a maximum is set and the timer is + // ended after the specified amount of time, the value is not reported. + withMaximum(maximum) { + max = maximum; + return timer; + }, + }; - get slowRpcSnapshot() { - return slowRpcList.slice(); - }, + // The timer is initialized to its creation time. + return timer.reset(); + }, - now() { - return Math.round(window.performance.now()); - }, + /** + * Log timing information for an RPC. + * + * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated. + * @param {number} elapsed The time elapsed of the RPC. + */ + reportRpcTiming(anonymizedUrl, elapsed) { + this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl, + elapsed, {}, true); + if (elapsed >= SLOW_RPC_THRESHOLD) { + slowRpcList.push({anonymizedUrl, elapsed}); + } + }, - _arePluginsLoaded() { - return this._baselines && - !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED); - }, + reportInteraction(eventName, details) { + this.reporter(INTERACTION_TYPE, this.category, eventName, undefined, + details, true); + }, - _isMetricsPluginLoaded() { - return this._arePluginsLoaded() || this._baselines && - !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED); - }, + /** + * A draft interaction was started. Update the time-betweeen-draft-actions + * timer. + */ + recordDraftInteraction() { + // If there is no timer defined, then this is the first interaction. + // Set up the timer so that it's ready to record the intervening time when + // called again. + const timer = this._timers.timeBetweenDraftActions; + if (!timer) { + // Create a timer with a maximum length. + this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER) + .withMaximum(DRAFT_ACTION_TIMER_MAX); + return; + } - /** - * Reporter reports events. Events will be queued if metrics plugin is not - * yet installed. - * - * @param {string} type - * @param {string} category - * @param {string} eventName - * @param {string|number} eventValue - * @param {Object} eventDetails - * @param {boolean|undefined} opt_noLog If true, the event will not be - * logged to the JS console. - */ - reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) { - const eventInfo = this._createEventInfo(type, category, - eventName, eventValue, eventDetails); - if (type === ERROR.TYPE && category === ERROR.CATEGORY) { - console.error(eventValue && eventValue.error || eventName); - } + // Mark the time and reinitialize the timer. + timer.end().reset(); + }, - // We report events immediately when metrics plugin is loaded - if (this._isMetricsPluginLoaded() && !pending.length) { - this._reportEvent(eventInfo, opt_noLog); - } else { - // We cache until metrics plugin is loaded - pending.push([eventInfo, opt_noLog]); - if (this._isMetricsPluginLoaded()) { - pending.forEach(([eventInfo, opt_noLog]) => { - this._reportEvent(eventInfo, opt_noLog); - }); - pending = []; - } - } - }, + reportErrorDialog(message) { + this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY, + 'ErrorDialog: ' + message, {error: new Error(message)}); + }, - _reportEvent(eventInfo, opt_noLog) { - const {type, value, name} = eventInfo; - document.dispatchEvent(new CustomEvent(type, {detail: eventInfo})); - if (opt_noLog) { return; } - if (type !== ERROR.TYPE) { - if (value !== undefined) { - console.log(`Reporting: ${name}: ${value}`); - } else { - console.log(`Reporting: ${name}`); - } - } - }, + setRepoName(repoName) { + reportRepoName = repoName; + }, +}); - _createEventInfo(type, category, name, value, eventDetails) { - const eventInfo = { - type, - category, - name, - value, - eventStart: this.now(), - }; - - if (typeof(eventDetails) === 'object' && - Object.entries(eventDetails).length !== 0) { - eventInfo.eventDetails = JSON.stringify(eventDetails); - } - if (reportRepoName) { - eventInfo.repoName = reportRepoName; - } - const isInBackgroundTab = document.visibilityState === 'hidden'; - if (isInBackgroundTab !== undefined) { - eventInfo.inBackgroundTab = isInBackgroundTab; - } - - return eventInfo; - }, - - /** - * User-perceived app start time, should be reported when the app is ready. - */ - appStarted() { - this.timeEnd(TIMING.APP_STARTED); - this.pageLoaded(); - }, - - /** - * Page load time and other metrics, should be reported at any time - * after navigation. - */ - pageLoaded() { - if (this.performanceTiming.loadEventEnd === 0) { - console.error('pageLoaded should be called after window.onload'); - this.async(this.pageLoaded, 100); - } else { - const perfEvents = Object.keys(this.performanceTiming.toJSON()); - perfEvents.forEach( - eventName => this._reportPerformanceTiming(eventName) - ); - } - }, - - _reportPerformanceTiming(eventName, eventDetails) { - const eventTiming = this.performanceTiming[eventName]; - if (eventTiming > 0) { - const elapsedTime = eventTiming - - this.performanceTiming.navigationStart; - // NavResTime - Navigation and resource timings. - this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, - `NavResTime - ${eventName}`, elapsedTime, eventDetails, true); - } - }, - - beforeLocationChanged() { - for (const prop of Object.keys(this._baselines)) { - delete this._baselines[prop]; - } - this.time(TIMER.CHANGE_DISPLAYED); - this.time(TIMER.CHANGE_LOAD_FULL); - this.time(TIMER.DASHBOARD_DISPLAYED); - this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); - this.time(TIMER.DIFF_VIEW_DISPLAYED); - this.time(TIMER.DIFF_VIEW_LOAD_FULL); - this.time(TIMER.FILE_LIST_DISPLAYED); - reportRepoName = undefined; - // reset slow rpc list since here start page loads which report these rpcs - slowRpcList = []; - }, - - locationChanged(page) { - this.reporter( - NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page); - }, - - dashboardDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } else { - this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } - }, - - changeDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } else { - this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } - }, - - changeFullyLoaded() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) { - this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL); - } else { - this.timeEnd(TIMER.CHANGE_LOAD_FULL); - } - }, - - diffViewDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } else { - this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } - }, - - diffViewFullyLoaded() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) { - this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL); - } else { - this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL); - } - }, - - diffViewContentDisplayed() { - if (this._baselines.hasOwnProperty( - TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED); - } else { - this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); - } - }, - - fileListDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED); - } else { - this.timeEnd(TIMER.FILE_LIST_DISPLAYED); - } - }, - - reportExtension(name) { - this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name); - }, - - pluginLoaded(name) { - if (name.startsWith('metrics-')) { - this.timeEnd(TIMER.METRICS_PLUGIN_LOADED); - } - }, - - pluginsLoaded(pluginsList) { - this.timeEnd(TIMER.PLUGINS_LOADED); - this.reporter( - PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined, - {pluginsList: pluginsList || []}, true); - }, - - /** - * Reset named timer. - */ - time(name) { - this._baselines[name] = this.now(); - window.performance.mark(`${name}-start`); - }, - - /** - * Finish named timer and report it to server. - */ - timeEnd(name, eventDetails) { - if (!this._baselines.hasOwnProperty(name)) { return; } - const baseTime = this._baselines[name]; - delete this._baselines[name]; - this._reportTiming(name, this.now() - baseTime, eventDetails); - - // Finalize the interval. Either from a registered start mark or - // the navigation start time (if baseTime is 0). - if (baseTime !== 0) { - window.performance.measure(name, `${name}-start`); - } else { - // Microsft Edge does not handle the 2nd param correctly - // (if undefined). - window.performance.measure(name); - } - }, - - /** - * Reports just line timeEnd, but additionally reports an average given a - * denominator and a separate reporiting name for the average. - * - * @param {string} name Timing name. - * @param {string} averageName Average timing name. - * @param {number} denominator Number by which to divide the total to - * compute the average. - */ - timeEndWithAverage(name, averageName, denominator) { - if (!this._baselines.hasOwnProperty(name)) { return; } - const baseTime = this._baselines[name]; - this.timeEnd(name); - - // Guard against division by zero. - if (!denominator) { return; } - const time = this.now() - baseTime; - this._reportTiming(averageName, time / denominator); - }, - - /** - * Send a timing report with an arbitrary time value. - * - * @param {string} name Timing name. - * @param {number} time The time to report as an integer of milliseconds. - * @param {Object} eventDetails non sensitive details - */ - _reportTiming(name, time, eventDetails) { - this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time, - eventDetails); - }, - - /** - * Get a timer object to for reporing a user timing. The start time will be - * the time that the object has been created, and the end time will be the - * time that the "end" method is called on the object. - * - * @param {string} name Timing name. - * @returns {!Object} The timer object. - */ - getTimer(name) { - let called = false; - let start; - let max = null; - - const timer = { - - // Clear the timer and reset the start time. - reset: () => { - called = false; - start = this.now(); - return timer; - }, - - // Stop the timer and report the intervening time. - end: () => { - if (called) { - throw new Error(`Timer for "${name}" already ended.`); - } - called = true; - const time = this.now() - start; - - // If a maximum is specified and the time exceeds it, do not report. - if (max && time > max) { return timer; } - - this._reportTiming(name, time); - return timer; - }, - - // Set a maximum reportable time. If a maximum is set and the timer is - // ended after the specified amount of time, the value is not reported. - withMaximum(maximum) { - max = maximum; - return timer; - }, - }; - - // The timer is initialized to its creation time. - return timer.reset(); - }, - - /** - * Log timing information for an RPC. - * - * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated. - * @param {number} elapsed The time elapsed of the RPC. - */ - reportRpcTiming(anonymizedUrl, elapsed) { - this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl, - elapsed, {}, true); - if (elapsed >= SLOW_RPC_THRESHOLD) { - slowRpcList.push({anonymizedUrl, elapsed}); - } - }, - - reportInteraction(eventName, details) { - this.reporter(INTERACTION_TYPE, this.category, eventName, undefined, - details, true); - }, - - /** - * A draft interaction was started. Update the time-betweeen-draft-actions - * timer. - */ - recordDraftInteraction() { - // If there is no timer defined, then this is the first interaction. - // Set up the timer so that it's ready to record the intervening time when - // called again. - const timer = this._timers.timeBetweenDraftActions; - if (!timer) { - // Create a timer with a maximum length. - this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER) - .withMaximum(DRAFT_ACTION_TIMER_MAX); - return; - } - - // Mark the time and reinitialize the timer. - timer.end().reset(); - }, - - reportErrorDialog(message) { - this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY, - 'ErrorDialog: ' + message, {error: new Error(message)}); - }, - - setRepoName(repoName) { - reportRepoName = repoName; - }, - }); - - window.GrReporting = GrReporting; - // Expose onerror installation so it would be accessible from tests. - window.GrReporting._catchErrors = catchErrors; - window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS); -})(); +window.GrReporting = GrReporting; +// Expose onerror installation so it would be accessible from tests. +window.GrReporting._catchErrors = catchErrors; +window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html index 19e4b74..ad9903e 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reporting</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-reporting.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-reporting.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-reporting.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,407 +40,409 @@ </template> </test-fixture> -<script> - suite('gr-reporting tests', async () => { - await readyToTest(); - let element; - let sandbox; - let clock; - let fakePerformance; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-reporting.js'; +suite('gr-reporting tests', () => { + let element; + let sandbox; + let clock; + let fakePerformance; - const NOW_TIME = 100; + const NOW_TIME = 100; + setup(() => { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(NOW_TIME); + element = fixture('basic'); + element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS); + fakePerformance = { + navigationStart: 1, + loadEventEnd: 2, + }; + fakePerformance.toJSON = () => fakePerformance; + sinon.stub(element, 'performanceTiming', + {get() { return fakePerformance; }}); + sandbox.stub(element, 'reporter'); + }); + + teardown(() => { + sandbox.restore(); + clock.restore(); + }); + + test('appStarted', () => { + sandbox.stub(element, 'now').returns(42); + element.appStarted(); + assert.isTrue( + element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'App Started', 42 + )); + }); + + test('WebComponentsReady', () => { + sandbox.stub(element, 'now').returns(42); + element.timeEnd('WebComponentsReady'); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'WebComponentsReady', 42 + )); + }); + + test('pageLoaded', () => { + element.pageLoaded(); + assert.isTrue( + element.reporter.calledWithExactly( + 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd', + fakePerformance.loadEventEnd - fakePerformance.navigationStart, + undefined, true) + ); + }); + + test('beforeLocationChanged', () => { + element._baselines['garbage'] = 'monster'; + sandbox.stub(element, 'time'); + element.beforeLocationChanged(); + assert.isTrue(element.time.calledWithExactly('DashboardDisplayed')); + assert.isTrue(element.time.calledWithExactly('ChangeDisplayed')); + assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded')); + assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed')); + assert.isTrue(element.time.calledWithExactly('FileListDisplayed')); + assert.isFalse(element._baselines.hasOwnProperty('garbage')); + }); + + test('changeDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.changeDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []})); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupChangeDisplayed', + {rpcList: []})); + element.changeDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed', + {rpcList: []})); + }); + + test('changeFullyLoaded', () => { + sandbox.spy(element, 'timeEnd'); + element.changeFullyLoaded(); + assert.isFalse( + element.timeEnd.calledWithExactly('ChangeFullyLoaded')); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupChangeFullyLoaded')); + element.changeFullyLoaded(); + assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded')); + }); + + test('diffViewDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.diffViewDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('DiffViewDisplayed', + {rpcList: []})); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupDiffViewDisplayed', + {rpcList: []})); + element.diffViewDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed', + {rpcList: []})); + }); + + test('fileListDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.fileListDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('FileListDisplayed')); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupFileListDisplayed')); + element.fileListDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed')); + }); + + test('dashboardDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.dashboardDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('DashboardDisplayed', + {rpcList: []})); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupDashboardDisplayed', + {rpcList: []})); + element.dashboardDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed', + {rpcList: []})); + }); + + test('dashboardDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.reportRpcTiming('/changes/*~*/comments', 500); + element.dashboardDisplayed(); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupDashboardDisplayed', + {rpcList: [ + { + anonymizedUrl: '/changes/*~*/comments', + elapsed: 500, + }, + ]} + )); + }); + + test('time and timeEnd', () => { + const nowStub = sandbox.stub(element, 'now').returns(0); + element.time('foo'); + nowStub.returns(1); + element.time('bar'); + nowStub.returns(2); + element.timeEnd('bar'); + nowStub.returns(3); + element.timeEnd('foo'); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'foo', 3 + )); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'bar', 1 + )); + }); + + test('timer object', () => { + const nowStub = sandbox.stub(element, 'now').returns(100); + const timer = element.getTimer('foo-bar'); + nowStub.returns(150); + timer.end(); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'foo-bar', 50)); + }); + + test('timer object double call', () => { + const timer = element.getTimer('foo-bar'); + timer.end(); + assert.isTrue(element.reporter.calledOnce); + assert.throws(() => { + timer.end(); + }, 'Timer for "foo-bar" already ended.'); + }); + + test('timer object maximum', () => { + const nowStub = sandbox.stub(element, 'now').returns(100); + const timer = element.getTimer('foo-bar').withMaximum(100); + nowStub.returns(150); + timer.end(); + assert.isTrue(element.reporter.calledOnce); + + timer.reset(); + nowStub.returns(260); + timer.end(); + assert.isTrue(element.reporter.calledOnce); + }); + + test('recordDraftInteraction', () => { + const key = 'TimeBetweenDraftActions'; + const nowStub = sandbox.stub(element, 'now').returns(100); + const timingStub = sandbox.stub(element, '_reportTiming'); + element.recordDraftInteraction(); + assert.isFalse(timingStub.called); + + nowStub.returns(200); + element.recordDraftInteraction(); + assert.isTrue(timingStub.calledOnce); + assert.equal(timingStub.lastCall.args[0], key); + assert.equal(timingStub.lastCall.args[1], 100); + + nowStub.returns(350); + element.recordDraftInteraction(); + assert.isTrue(timingStub.calledTwice); + assert.equal(timingStub.lastCall.args[0], key); + assert.equal(timingStub.lastCall.args[1], 150); + + nowStub.returns(370 + 2 * 60 * 1000); + element.recordDraftInteraction(); + assert.isFalse(timingStub.calledThrice); + }); + + test('timeEndWithAverage', () => { + const nowStub = sandbox.stub(element, 'now').returns(0); + nowStub.returns(1000); + element.time('foo'); + nowStub.returns(1100); + element.timeEndWithAverage('foo', 'bar', 10); + assert.isTrue(element.reporter.calledTwice); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'foo', 100)); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'bar', 10)); + }); + + test('reportExtension', () => { + element.reportExtension('foo'); + assert.isTrue(element.reporter.calledWithExactly( + 'lifecycle', 'Extension detected', 'foo' + )); + }); + + test('reportInteraction', () => { + element.reporter.restore(); + sandbox.spy(element, '_reportEvent'); + element.pluginsLoaded(); // so we don't cache + element.reportInteraction('button-click', {name: 'sendReply'}); + assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + { + type: 'interaction', + name: 'button-click', + eventDetails: JSON.stringify({name: 'sendReply'}), + } + )); + }); + + test('report start time', () => { + element.reporter.restore(); + sandbox.stub(element, 'now').returns(42); + sandbox.spy(element, '_reportEvent'); + const dispatchStub = sandbox.spy(document, 'dispatchEvent'); + element.pluginsLoaded(); + element.time('timeAction'); + element.timeEnd('timeAction'); + assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + { + type: 'timing-report', + category: 'UI Latency', + name: 'timeAction', + value: 0, + eventStart: 42, + } + )); + assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42); + }); + + suite('plugins', () => { setup(() => { - sandbox = sinon.sandbox.create(); - clock = sinon.useFakeTimers(NOW_TIME); - element = fixture('basic'); - element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS); - fakePerformance = { - navigationStart: 1, - loadEventEnd: 2, - }; - fakePerformance.toJSON = () => fakePerformance; - sinon.stub(element, 'performanceTiming', - {get() { return fakePerformance; }}); - sandbox.stub(element, 'reporter'); - }); - - teardown(() => { - sandbox.restore(); - clock.restore(); - }); - - test('appStarted', () => { - sandbox.stub(element, 'now').returns(42); - element.appStarted(); - assert.isTrue( - element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'App Started', 42 - )); - }); - - test('WebComponentsReady', () => { - sandbox.stub(element, 'now').returns(42); - element.timeEnd('WebComponentsReady'); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'WebComponentsReady', 42 - )); - }); - - test('pageLoaded', () => { - element.pageLoaded(); - assert.isTrue( - element.reporter.calledWithExactly( - 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd', - fakePerformance.loadEventEnd - fakePerformance.navigationStart, - undefined, true) - ); - }); - - test('beforeLocationChanged', () => { - element._baselines['garbage'] = 'monster'; - sandbox.stub(element, 'time'); - element.beforeLocationChanged(); - assert.isTrue(element.time.calledWithExactly('DashboardDisplayed')); - assert.isTrue(element.time.calledWithExactly('ChangeDisplayed')); - assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded')); - assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed')); - assert.isTrue(element.time.calledWithExactly('FileListDisplayed')); - assert.isFalse(element._baselines.hasOwnProperty('garbage')); - }); - - test('changeDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.changeDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []})); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupChangeDisplayed', - {rpcList: []})); - element.changeDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed', - {rpcList: []})); - }); - - test('changeFullyLoaded', () => { - sandbox.spy(element, 'timeEnd'); - element.changeFullyLoaded(); - assert.isFalse( - element.timeEnd.calledWithExactly('ChangeFullyLoaded')); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupChangeFullyLoaded')); - element.changeFullyLoaded(); - assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded')); - }); - - test('diffViewDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.diffViewDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('DiffViewDisplayed', - {rpcList: []})); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupDiffViewDisplayed', - {rpcList: []})); - element.diffViewDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed', - {rpcList: []})); - }); - - test('fileListDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.fileListDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('FileListDisplayed')); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupFileListDisplayed')); - element.fileListDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed')); - }); - - test('dashboardDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.dashboardDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('DashboardDisplayed', - {rpcList: []})); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupDashboardDisplayed', - {rpcList: []})); - element.dashboardDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed', - {rpcList: []})); - }); - - test('dashboardDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.reportRpcTiming('/changes/*~*/comments', 500); - element.dashboardDisplayed(); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupDashboardDisplayed', - {rpcList: [ - { - anonymizedUrl: '/changes/*~*/comments', - elapsed: 500, - }, - ]} - )); - }); - - test('time and timeEnd', () => { - const nowStub = sandbox.stub(element, 'now').returns(0); - element.time('foo'); - nowStub.returns(1); - element.time('bar'); - nowStub.returns(2); - element.timeEnd('bar'); - nowStub.returns(3); - element.timeEnd('foo'); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'foo', 3 - )); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'bar', 1 - )); - }); - - test('timer object', () => { - const nowStub = sandbox.stub(element, 'now').returns(100); - const timer = element.getTimer('foo-bar'); - nowStub.returns(150); - timer.end(); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'foo-bar', 50)); - }); - - test('timer object double call', () => { - const timer = element.getTimer('foo-bar'); - timer.end(); - assert.isTrue(element.reporter.calledOnce); - assert.throws(() => { - timer.end(); - }, 'Timer for "foo-bar" already ended.'); - }); - - test('timer object maximum', () => { - const nowStub = sandbox.stub(element, 'now').returns(100); - const timer = element.getTimer('foo-bar').withMaximum(100); - nowStub.returns(150); - timer.end(); - assert.isTrue(element.reporter.calledOnce); - - timer.reset(); - nowStub.returns(260); - timer.end(); - assert.isTrue(element.reporter.calledOnce); - }); - - test('recordDraftInteraction', () => { - const key = 'TimeBetweenDraftActions'; - const nowStub = sandbox.stub(element, 'now').returns(100); - const timingStub = sandbox.stub(element, '_reportTiming'); - element.recordDraftInteraction(); - assert.isFalse(timingStub.called); - - nowStub.returns(200); - element.recordDraftInteraction(); - assert.isTrue(timingStub.calledOnce); - assert.equal(timingStub.lastCall.args[0], key); - assert.equal(timingStub.lastCall.args[1], 100); - - nowStub.returns(350); - element.recordDraftInteraction(); - assert.isTrue(timingStub.calledTwice); - assert.equal(timingStub.lastCall.args[0], key); - assert.equal(timingStub.lastCall.args[1], 150); - - nowStub.returns(370 + 2 * 60 * 1000); - element.recordDraftInteraction(); - assert.isFalse(timingStub.calledThrice); - }); - - test('timeEndWithAverage', () => { - const nowStub = sandbox.stub(element, 'now').returns(0); - nowStub.returns(1000); - element.time('foo'); - nowStub.returns(1100); - element.timeEndWithAverage('foo', 'bar', 10); - assert.isTrue(element.reporter.calledTwice); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'foo', 100)); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'bar', 10)); - }); - - test('reportExtension', () => { - element.reportExtension('foo'); - assert.isTrue(element.reporter.calledWithExactly( - 'lifecycle', 'Extension detected', 'foo' - )); - }); - - test('reportInteraction', () => { element.reporter.restore(); - sandbox.spy(element, '_reportEvent'); - element.pluginsLoaded(); // so we don't cache - element.reportInteraction('button-click', {name: 'sendReply'}); - assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( - { - type: 'interaction', - name: 'button-click', - eventDetails: JSON.stringify({name: 'sendReply'}), - } - )); + sandbox.stub(element, '_reportEvent'); }); - test('report start time', () => { - element.reporter.restore(); + test('pluginsLoaded reports time', () => { sandbox.stub(element, 'now').returns(42); - sandbox.spy(element, '_reportEvent'); - const dispatchStub = sandbox.spy(document, 'dispatchEvent'); element.pluginsLoaded(); - element.time('timeAction'); - element.timeEnd('timeAction'); - assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + assert.isTrue(element._reportEvent.calledWithMatch( { type: 'timing-report', category: 'UI Latency', - name: 'timeAction', - value: 0, - eventStart: 42, + name: 'PluginsLoaded', + value: 42, } )); - assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42); }); - suite('plugins', () => { - setup(() => { - element.reporter.restore(); - sandbox.stub(element, '_reportEvent'); - }); - - test('pluginsLoaded reports time', () => { - sandbox.stub(element, 'now').returns(42); - element.pluginsLoaded(); - assert.isTrue(element._reportEvent.calledWithMatch( - { - type: 'timing-report', - category: 'UI Latency', - name: 'PluginsLoaded', - value: 42, - } - )); - }); - - test('pluginsLoaded reports plugins', () => { - element.pluginsLoaded(['foo', 'bar']); - assert.isTrue(element._reportEvent.calledWithMatch( - { - type: 'lifecycle', - category: 'Plugins installed', - eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}), - } - )); - }); - - test('caches reports if plugins are not loaded', () => { - element.timeEnd('foo'); - assert.isFalse(element._reportEvent.called); - }); - - test('reports if plugins are loaded', () => { - element.pluginsLoaded(); - assert.isTrue(element._reportEvent.called); - }); - - test('reports if metrics plugin xyz is loaded', () => { - element.pluginLoaded('metrics-xyz'); - assert.isTrue(element._reportEvent.called); - }); - - test('reports cached events preserving order', () => { - element.time('foo'); - element.time('bar'); - element.timeEnd('foo'); - element.pluginsLoaded(); - element.timeEnd('bar'); - assert.isTrue(element._reportEvent.getCall(0).calledWithMatch( - {type: 'timing-report', category: 'UI Latency', name: 'foo'} - )); - assert.isTrue(element._reportEvent.getCall(1).calledWithMatch( - {type: 'timing-report', category: 'UI Latency', - name: 'PluginsLoaded'} - )); - assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( - {type: 'lifecycle', category: 'Plugins installed'} - )); - assert.isTrue(element._reportEvent.getCall(3).calledWithMatch( - {type: 'timing-report', category: 'UI Latency', name: 'bar'} - )); - }); + test('pluginsLoaded reports plugins', () => { + element.pluginsLoaded(['foo', 'bar']); + assert.isTrue(element._reportEvent.calledWithMatch( + { + type: 'lifecycle', + category: 'Plugins installed', + eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}), + } + )); }); - test('search', () => { - element.locationChanged('_handleSomeRoute'); - assert.isTrue(element.reporter.calledWithExactly( - 'nav-report', 'Location Changed', 'Page', '_handleSomeRoute')); + test('caches reports if plugins are not loaded', () => { + element.timeEnd('foo'); + assert.isFalse(element._reportEvent.called); }); - suite('exception logging', () => { - let fakeWindow; - let reporter; + test('reports if plugins are loaded', () => { + element.pluginsLoaded(); + assert.isTrue(element._reportEvent.called); + }); - const emulateThrow = function(msg, url, line, column, error) { - return fakeWindow.onerror(msg, url, line, column, error); - }; + test('reports if metrics plugin xyz is loaded', () => { + element.pluginLoaded('metrics-xyz'); + assert.isTrue(element._reportEvent.called); + }); - setup(() => { - reporter = sandbox.stub(GrReporting.prototype, 'reporter'); - fakeWindow = { - handlers: {}, - addEventListener(type, handler) { - this.handlers[type] = handler; - }, - }; - sandbox.stub(console, 'error'); - window.GrReporting._catchErrors(fakeWindow); - }); - - test('is reported', () => { - const error = new Error('bar'); - error.stack = undefined; - emulateThrow('bar', 'http://url', 4, 2, error); - assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); - const payload = reporter.lastCall.args[3]; - assert.deepEqual(payload, { - url: 'http://url', - line: 4, - column: 2, - error, - }); - }); - - test('is reported with 3 lines of stack', () => { - const error = new Error('bar'); - emulateThrow('bar', 'http://url', 4, 2, error); - const expectedStack = error.stack.split('\n').slice(0, 3) - .join('\n'); - assert.isTrue(reporter.calledWith('error', 'exception', - expectedStack)); - }); - - test('prevent default event handler', () => { - assert.isTrue(emulateThrow()); - }); - - test('unhandled rejection', () => { - fakeWindow.handlers['unhandledrejection']({ - reason: { - message: 'bar', - }, - }); - assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); - }); + test('reports cached events preserving order', () => { + element.time('foo'); + element.time('bar'); + element.timeEnd('foo'); + element.pluginsLoaded(); + element.timeEnd('bar'); + assert.isTrue(element._reportEvent.getCall(0).calledWithMatch( + {type: 'timing-report', category: 'UI Latency', name: 'foo'} + )); + assert.isTrue(element._reportEvent.getCall(1).calledWithMatch( + {type: 'timing-report', category: 'UI Latency', + name: 'PluginsLoaded'} + )); + assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + {type: 'lifecycle', category: 'Plugins installed'} + )); + assert.isTrue(element._reportEvent.getCall(3).calledWithMatch( + {type: 'timing-report', category: 'UI Latency', name: 'bar'} + )); }); }); + + test('search', () => { + element.locationChanged('_handleSomeRoute'); + assert.isTrue(element.reporter.calledWithExactly( + 'nav-report', 'Location Changed', 'Page', '_handleSomeRoute')); + }); + + suite('exception logging', () => { + let fakeWindow; + let reporter; + + const emulateThrow = function(msg, url, line, column, error) { + return fakeWindow.onerror(msg, url, line, column, error); + }; + + setup(() => { + reporter = sandbox.stub(GrReporting.prototype, 'reporter'); + fakeWindow = { + handlers: {}, + addEventListener(type, handler) { + this.handlers[type] = handler; + }, + }; + sandbox.stub(console, 'error'); + window.GrReporting._catchErrors(fakeWindow); + }); + + test('is reported', () => { + const error = new Error('bar'); + error.stack = undefined; + emulateThrow('bar', 'http://url', 4, 2, error); + assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); + const payload = reporter.lastCall.args[3]; + assert.deepEqual(payload, { + url: 'http://url', + line: 4, + column: 2, + error, + }); + }); + + test('is reported with 3 lines of stack', () => { + const error = new Error('bar'); + emulateThrow('bar', 'http://url', 4, 2, error); + const expectedStack = error.stack.split('\n').slice(0, 3) + .join('\n'); + assert.isTrue(reporter.calledWith('error', 'exception', + expectedStack)); + }); + + test('prevent default event handler', () => { + assert.isTrue(emulateThrow()); + }); + + test('unhandled rejection', () => { + fakeWindow.handlers['unhandledrejection']({ + reason: { + message: 'bar', + }, + }); + assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js index ebac1e1..e461d1d 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -14,1520 +14,1535 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const RoutePattern = { - ROOT: '/', +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-reporting/gr-reporting.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import page from 'page/page.mjs'; +self.page = page; +import {htmlTemplate} from './gr-router_html.js'; - DASHBOARD: /^\/dashboard\/(.+)$/, - CUSTOM_DASHBOARD: /^\/dashboard\/?$/, - PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/, +const RoutePattern = { + ROOT: '/', - AGREEMENTS: /^\/settings\/agreements\/?/, - NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/, - REGISTER: /^\/register(\/.*)?$/, + DASHBOARD: /^\/dashboard\/(.+)$/, + CUSTOM_DASHBOARD: /^\/dashboard\/?$/, + PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/, - // Pattern for login and logout URLs intended to be passed-through. May - // include a return URL. - LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/, + AGREEMENTS: /^\/settings\/agreements\/?/, + NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/, + REGISTER: /^\/register(\/.*)?$/, - // Pattern for a catchall route when no other pattern is matched. - DEFAULT: /.*/, + // Pattern for login and logout URLs intended to be passed-through. May + // include a return URL. + LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/, - // Matches /admin/groups/[uuid-]<group> - GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/, + // Pattern for a catchall route when no other pattern is matched. + DEFAULT: /.*/, - // Redirects /groups/self to /settings/#Groups for GWT compatibility - GROUP_SELF: /^\/groups\/self/, + // Matches /admin/groups/[uuid-]<group> + GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/, - // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui) - // Redirects to /admin/groups/[uuid-]<group> - GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/, + // Redirects /groups/self to /settings/#Groups for GWT compatibility + GROUP_SELF: /^\/groups\/self/, - // Matches /admin/groups/<group>,audit-log - GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/, + // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui) + // Redirects to /admin/groups/[uuid-]<group> + GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/, - // Matches /admin/groups/[uuid-]<group>,members - GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/, + // Matches /admin/groups/<group>,audit-log + GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/, - // Matches /admin/groups[,<offset>][/]. - GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/, - GROUP_LIST_FILTER: '/admin/groups/q/filter::filter', - GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset', + // Matches /admin/groups/[uuid-]<group>,members + GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/, - // Matches /admin/create-project - LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/, + // Matches /admin/groups[,<offset>][/]. + GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/, + GROUP_LIST_FILTER: '/admin/groups/q/filter::filter', + GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset', - // Matches /admin/create-project - LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/, + // Matches /admin/create-project + LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/, - PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/, + // Matches /admin/create-project + LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/, - // Matches /admin/repos/<repo> - REPO: /^\/admin\/repos\/([^,]+)$/, + PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/, - // Matches /admin/repos/<repo>,commands. - REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/, + // Matches /admin/repos/<repo> + REPO: /^\/admin\/repos\/([^,]+)$/, - // Matches /admin/repos/<repos>,access. - REPO_ACCESS: /^\/admin\/repos\/(.+),access$/, + // Matches /admin/repos/<repo>,commands. + REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/, - // Matches /admin/repos/<repos>,access. - REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/, + // Matches /admin/repos/<repos>,access. + REPO_ACCESS: /^\/admin\/repos\/(.+),access$/, - // Matches /admin/repos[,<offset>][/]. - REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/, - REPO_LIST_FILTER: '/admin/repos/q/filter::filter', - REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset', + // Matches /admin/repos/<repos>,access. + REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/, - // Matches /admin/repos/<repo>,branches[,<offset>]. - BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/, - BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter', - BRANCH_LIST_FILTER_OFFSET: - '/admin/repos/:repo,branches/q/filter::filter,:offset', + // Matches /admin/repos[,<offset>][/]. + REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/, + REPO_LIST_FILTER: '/admin/repos/q/filter::filter', + REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset', - // Matches /admin/repos/<repo>,tags[,<offset>]. - TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/, - TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter', - TAG_LIST_FILTER_OFFSET: - '/admin/repos/:repo,tags/q/filter::filter,:offset', + // Matches /admin/repos/<repo>,branches[,<offset>]. + BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/, + BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter', + BRANCH_LIST_FILTER_OFFSET: + '/admin/repos/:repo,branches/q/filter::filter,:offset', - PLUGINS: /^\/plugins\/(.+)$/, + // Matches /admin/repos/<repo>,tags[,<offset>]. + TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/, + TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter', + TAG_LIST_FILTER_OFFSET: + '/admin/repos/:repo,tags/q/filter::filter,:offset', - PLUGIN_LIST: /^\/admin\/plugins(\/)?$/, + PLUGINS: /^\/plugins\/(.+)$/, - // Matches /admin/plugins[,<offset>][/]. - PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/, - PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter', - PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset', + PLUGIN_LIST: /^\/admin\/plugins(\/)?$/, - QUERY: /^\/q\/([^,]+)(,(\d+))?$/, + // Matches /admin/plugins[,<offset>][/]. + PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/, + PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter', + PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset', - /** - * Support vestigial params from GWT UI. - * - * @see Issue 7673. - * @type {!RegExp} - */ - QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/, - - // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/]. - CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, - CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/, - - // Matches - // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>]. - // TODO(kaspern): Migrate completely to project based URLs, with backwards - // compatibility for change-only. - CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, - - // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit - CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/, - - // Matches - // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>. - // TODO(kaspern): Migrate completely to project based URLs, with backwards - // compatibility for change-only. - // eslint-disable-next-line max-len - DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/, - - // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum] - DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/, - - // Matches non-project-relative - // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>. - DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/, - - // Matches diff routes using @\d+ to specify a file name (whether or not - // the project name is included). - // eslint-disable-next-line max-len - DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/, - - SETTINGS: /^\/settings\/?/, - SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/, - - // Matches /c/<changeNum>/ /<URL tail> - // Catches improperly encoded URLs (context: Issue 7100) - IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/, - - PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/, - - DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter', - DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/, - DOCUMENTATION: /^\/Documentation(\/)?(.+)?/, - }; + QUERY: /^\/q\/([^,]+)(,(\d+))?$/, /** - * Pattern to recognize and parse the diff line locations as they appear in - * the hash of diff URLs. In this format, a number on its own indicates that - * line number in the revision of the diff. A number prefixed by either an 'a' - * or a 'b' indicates that line number of the base of the diff. + * Support vestigial params from GWT UI. * - * @type {RegExp} + * @see Issue 7673. + * @type {!RegExp} */ - const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/; + QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/, - /** - * Pattern to recognize '+' in url-encoded strings for replacement with ' '. - */ - const PLUS_PATTERN = /\+/g; + // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/]. + CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, + CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/, - /** - * Pattern to recognize leading '?' in window.location.search, for stripping. - */ - const QUESTION_PATTERN = /^\?*/; + // Matches + // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>]. + // TODO(kaspern): Migrate completely to project based URLs, with backwards + // compatibility for change-only. + CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, - /** - * GWT UI would use @\d+ at the end of a path to indicate linenum. - */ - const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/; + // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit + CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/, - const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/; + // Matches + // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>. + // TODO(kaspern): Migrate completely to project based URLs, with backwards + // compatibility for change-only. + // eslint-disable-next-line max-len + DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/, - const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g; + // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum] + DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/, - // Polymer makes `app` intrinsically defined on the window by virtue of the - // custom element having the id "app", but it is made explicit here. - const app = document.querySelector('#app'); - if (!app) { - console.log('No gr-app found (running tests)'); + // Matches non-project-relative + // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>. + DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/, + + // Matches diff routes using @\d+ to specify a file name (whether or not + // the project name is included). + // eslint-disable-next-line max-len + DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/, + + SETTINGS: /^\/settings\/?/, + SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/, + + // Matches /c/<changeNum>/ /<URL tail> + // Catches improperly encoded URLs (context: Issue 7100) + IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/, + + PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/, + + DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter', + DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/, + DOCUMENTATION: /^\/Documentation(\/)?(.+)?/, +}; + +/** + * Pattern to recognize and parse the diff line locations as they appear in + * the hash of diff URLs. In this format, a number on its own indicates that + * line number in the revision of the diff. A number prefixed by either an 'a' + * or a 'b' indicates that line number of the base of the diff. + * + * @type {RegExp} + */ +const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/; + +/** + * Pattern to recognize '+' in url-encoded strings for replacement with ' '. + */ +const PLUS_PATTERN = /\+/g; + +/** + * Pattern to recognize leading '?' in window.location.search, for stripping. + */ +const QUESTION_PATTERN = /^\?*/; + +/** + * GWT UI would use @\d+ at the end of a path to indicate linenum. + */ +const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/; + +const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/; + +const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g; + +// Polymer makes `app` intrinsically defined on the window by virtue of the +// custom element having the id "app", but it is made explicit here. +const app = document.querySelector('#app'); +if (!app) { + console.log('No gr-app found (running tests)'); +} + +// Setup listeners outside of the router component initialization. +(function() { + const reporting = document.createElement('gr-reporting'); + + window.addEventListener('WebComponentsReady', () => { + reporting.timeEnd('WebComponentsReady'); + }); +})(); + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrRouter extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-router'; } + + static get properties() { + return { + _app: { + type: Object, + value: app, + }, + _isRedirecting: Boolean, + // This variable is to differentiate between internal navigation (false) + // and for first navigation in app after loaded from server (true). + _isInitialLoad: { + type: Boolean, + value: true, + }, + }; } - // Setup listeners outside of the router component initialization. - (function() { - const reporting = document.createElement('gr-reporting'); + start() { + if (!this._app) { return; } + this._startRouter(); + } - window.addEventListener('WebComponentsReady', () => { - reporting.timeEnd('WebComponentsReady'); - }); - })(); + _setParams(params) { + this._appElement().params = params; + } + + _appElement() { + // In Polymer2 you have to reach through the shadow root of the app + // element. This obviously breaks encapsulation. + // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element + // explicitly in app, or by delegating to it. + return document.getElementById('app-element') || + document.getElementById('app').shadowRoot.getElementById( + 'app-element'); + } + + _redirect(url) { + this._isRedirecting = true; + page.redirect(url); + } /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element + * @param {!Object} params + * @return {string} */ - class GrRouter extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-router'; } + _generateUrl(params) { + const base = this.getBaseUrl(); + let url = ''; + const Views = Gerrit.Nav.View; - static get properties() { - return { - _app: { - type: Object, - value: app, - }, - _isRedirecting: Boolean, - // This variable is to differentiate between internal navigation (false) - // and for first navigation in app after loaded from server (true). - _isInitialLoad: { - type: Boolean, - value: true, - }, - }; + if (params.view === Views.SEARCH) { + url = this._generateSearchUrl(params); + } else if (params.view === Views.CHANGE) { + url = this._generateChangeUrl(params); + } else if (params.view === Views.DASHBOARD) { + url = this._generateDashboardUrl(params); + } else if (params.view === Views.DIFF || params.view === Views.EDIT) { + url = this._generateDiffOrEditUrl(params); + } else if (params.view === Views.GROUP) { + url = this._generateGroupUrl(params); + } else if (params.view === Views.REPO) { + url = this._generateRepoUrl(params); + } else if (params.view === Views.ROOT) { + url = '/'; + } else if (params.view === Views.SETTINGS) { + url = this._generateSettingsUrl(params); + } else { + throw new Error('Can\'t generate'); } - start() { - if (!this._app) { return; } - this._startRouter(); + return base + url; + } + + _generateWeblinks(params) { + const type = params.type; + switch (type) { + case Gerrit.Nav.WeblinkType.FILE: + return this._getFileWebLinks(params); + case Gerrit.Nav.WeblinkType.CHANGE: + return this._getChangeWeblinks(params); + case Gerrit.Nav.WeblinkType.PATCHSET: + return this._getPatchSetWeblink(params); + default: + console.warn(`Unsupported weblink ${type}!`); + } + } + + _getPatchSetWeblink(params) { + const {commit, options} = params; + const {weblinks, config} = options || {}; + const name = commit && commit.slice(0, 7); + const weblink = this._getBrowseCommitWeblink(weblinks, config); + if (!weblink || !weblink.url) { + return {name}; + } else { + return {name, url: weblink.url}; + } + } + + _firstCodeBrowserWeblink(weblinks) { + // This is an ordered whitelist of web link types that provide direct + // links to the commit in the url property. + const codeBrowserLinks = ['gitiles', 'browse', 'gitweb']; + for (let i = 0; i < codeBrowserLinks.length; i++) { + const weblink = + weblinks.find(weblink => weblink.name === codeBrowserLinks[i]); + if (weblink) { return weblink; } + } + return null; + } + + _getBrowseCommitWeblink(weblinks, config) { + if (!weblinks) { return null; } + let weblink; + // Use primary weblink if configured and exists. + if (config && config.gerrit && config.gerrit.primary_weblink_name) { + weblink = weblinks.find( + weblink => weblink.name === config.gerrit.primary_weblink_name + ); + } + if (!weblink) { + weblink = this._firstCodeBrowserWeblink(weblinks); + } + if (!weblink) { return null; } + return weblink; + } + + _getChangeWeblinks({repo, commit, options: {weblinks, config}}) { + if (!weblinks || !weblinks.length) return []; + const commitWeblink = this._getBrowseCommitWeblink(weblinks, config); + return weblinks.filter(weblink => + !commitWeblink || + !commitWeblink.name || + weblink.name !== commitWeblink.name); + } + + _getFileWebLinks({repo, commit, file, options: {weblinks}}) { + return weblinks; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateSearchUrl(params) { + let offsetExpr = ''; + if (params.offset && params.offset > 0) { + offsetExpr = ',' + params.offset; } - _setParams(params) { - this._appElement().params = params; + if (params.query) { + return '/q/' + this.encodeURL(params.query, true) + offsetExpr; } - _appElement() { - // In Polymer2 you have to reach through the shadow root of the app - // element. This obviously breaks encapsulation. - // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element - // explicitly in app, or by delegating to it. - return document.getElementById('app-element') || - document.getElementById('app').shadowRoot.getElementById( - 'app-element'); + const operators = []; + if (params.owner) { + operators.push('owner:' + this.encodeURL(params.owner, false)); + } + if (params.project) { + operators.push('project:' + this.encodeURL(params.project, false)); + } + if (params.branch) { + operators.push('branch:' + this.encodeURL(params.branch, false)); + } + if (params.topic) { + operators.push('topic:"' + this.encodeURL(params.topic, false) + '"'); + } + if (params.hashtag) { + operators.push('hashtag:"' + + this.encodeURL(params.hashtag.toLowerCase(), false) + '"'); + } + if (params.statuses) { + if (params.statuses.length === 1) { + operators.push( + 'status:' + this.encodeURL(params.statuses[0], false)); + } else if (params.statuses.length > 1) { + operators.push( + '(' + + params.statuses.map(s => `status:${this.encodeURL(s, false)}`) + .join(' OR ') + + ')'); + } } - _redirect(url) { - this._isRedirecting = true; - page.redirect(url); + return '/q/' + operators.join('+') + offsetExpr; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateChangeUrl(params) { + let range = this._getPatchRangeExpression(params); + if (range.length) { range = '/' + range; } + let suffix = `${range}`; + if (params.querystring) { + suffix += '?' + params.querystring; + } else if (params.edit) { + suffix += ',edit'; + } + if (params.messageHash) { + suffix += params.messageHash; + } + if (params.project) { + const encodedProject = this.encodeURL(params.project, true); + return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; + } else { + return `/c/${params.changeNum}${suffix}`; + } + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateDashboardUrl(params) { + const repoName = params.repo || params.project || null; + if (params.sections) { + // Custom dashboard. + const queryParams = this._sectionsToEncodedParams(params.sections, + repoName); + if (params.title) { + queryParams.push('title=' + encodeURIComponent(params.title)); + } + const user = params.user ? params.user : ''; + return `/dashboard/${user}?${queryParams.join('&')}`; + } else if (repoName) { + // Project dashboard. + const encodedRepo = this.encodeURL(repoName, true); + return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`; + } else { + // User dashboard. + return `/dashboard/${params.user || 'self'}`; + } + } + + /** + * @param {!Array<!{name: string, query: string}>} sections + * @param {string=} opt_repoName + * @return {!Array<string>} + */ + _sectionsToEncodedParams(sections, opt_repoName) { + return sections.map(section => { + // If there is a repo name provided, make sure to substitute it into the + // ${repo} (or legacy ${project}) query tokens. + const query = opt_repoName ? + section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) : + section.query; + return encodeURIComponent(section.name) + '=' + + encodeURIComponent(query); + }); + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateDiffOrEditUrl(params) { + let range = this._getPatchRangeExpression(params); + if (range.length) { range = '/' + range; } + + let suffix = `${range}/${this.encodeURL(params.path, true)}`; + + if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; } + + if (params.lineNum) { + suffix += '#'; + if (params.leftSide) { suffix += 'b'; } + suffix += params.lineNum; } - /** - * @param {!Object} params - * @return {string} - */ - _generateUrl(params) { - const base = this.getBaseUrl(); - let url = ''; - const Views = Gerrit.Nav.View; + if (params.project) { + const encodedProject = this.encodeURL(params.project, true); + return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; + } else { + return `/c/${params.changeNum}${suffix}`; + } + } - if (params.view === Views.SEARCH) { - url = this._generateSearchUrl(params); - } else if (params.view === Views.CHANGE) { - url = this._generateChangeUrl(params); - } else if (params.view === Views.DASHBOARD) { - url = this._generateDashboardUrl(params); - } else if (params.view === Views.DIFF || params.view === Views.EDIT) { - url = this._generateDiffOrEditUrl(params); - } else if (params.view === Views.GROUP) { - url = this._generateGroupUrl(params); - } else if (params.view === Views.REPO) { - url = this._generateRepoUrl(params); - } else if (params.view === Views.ROOT) { - url = '/'; - } else if (params.view === Views.SETTINGS) { - url = this._generateSettingsUrl(params); + /** + * @param {!Object} params + * @return {string} + */ + _generateGroupUrl(params) { + let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`; + if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) { + url += ',members'; + } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) { + url += ',audit-log'; + } + return url; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateRepoUrl(params) { + let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`; + if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) { + url += ',access'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) { + url += ',branches'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) { + url += ',tags'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) { + url += ',commands'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) { + url += ',dashboards'; + } + return url; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateSettingsUrl(params) { + return '/settings'; + } + + /** + * Given an object of parameters, potentially including a `patchNum` or a + * `basePatchNum` or both, return a string representation of that range. If + * no range is indicated in the params, the empty string is returned. + * + * @param {!Object} params + * @return {string} + */ + _getPatchRangeExpression(params) { + let range = ''; + if (params.patchNum) { range = '' + params.patchNum; } + if (params.basePatchNum) { range = params.basePatchNum + '..' + range; } + return range; + } + + /** + * Given a set of params without a project, gets the project from the rest + * API project lookup and then sets the app params. + * + * @param {?Object} params + */ + _normalizeLegacyRouteParams(params) { + if (!params.changeNum) { return Promise.resolve(); } + + return this.$.restAPI.getFromProjectLookup(params.changeNum) + .then(project => { + // Show a 404 and terminate if the lookup request failed. Attempting + // to redirect after failing to get the project loops infinitely. + if (!project) { + this._show404(); + return; + } + + params.project = project; + this._normalizePatchRangeParams(params); + this._redirect(this._generateUrl(params)); + }); + } + + /** + * Normalizes the params object, and determines if the URL needs to be + * modified to fit the proper schema. + * + * @param {*} params + * @return {boolean} whether or not the URL needs to be upgraded. + */ + _normalizePatchRangeParams(params) { + const hasBasePatchNum = params.basePatchNum !== null && + params.basePatchNum !== undefined; + const hasPatchNum = params.patchNum !== null && + params.patchNum !== undefined; + let needsRedirect = false; + + // Diffing a patch against itself is invalid, so if the base and revision + // patches are equal clear the base. + if (hasBasePatchNum && + this.patchNumEquals(params.basePatchNum, params.patchNum)) { + needsRedirect = true; + params.basePatchNum = null; + } else if (hasBasePatchNum && !hasPatchNum) { + // Regexes set basePatchNum instead of patchNum when only one is + // specified. Redirect is not needed in this case. + params.patchNum = params.basePatchNum; + params.basePatchNum = null; + } + return needsRedirect; + } + + /** + * Redirect the user to login using the given return-URL for redirection + * after authentication success. + * + * @param {string} returnUrl + */ + _redirectToLogin(returnUrl) { + const basePath = this.getBaseUrl() || ''; + page( + '/login/' + encodeURIComponent(returnUrl.substring(basePath.length))); + } + + /** + * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c" + * is parsed to have a hash of "b" rather than "b#c". Instead, this method + * parses hashes correctly. Will return an empty string if there is no hash. + * + * @param {!string} canonicalPath + * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c"). + */ + _getHashFromCanonicalPath(canonicalPath) { + return canonicalPath.split('#').slice(1) + .join('#'); + } + + _parseLineAddress(hash) { + const match = hash.match(LINE_ADDRESS_PATTERN); + if (!match) { return null; } + return { + leftSide: !!match[1], + lineNum: parseInt(match[2], 10), + }; + } + + /** + * Check to see if the user is logged in and return a promise that only + * resolves if the user is logged in. If the user us not logged in, the + * promise is rejected and the page is redirected to the login flow. + * + * @param {!Object} data The parsed route data. + * @return {!Promise<!Object>} A promise yielding the original route data + * (if it resolves). + */ + _redirectIfNotLoggedIn(data) { + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + return Promise.resolve(); } else { - throw new Error('Can\'t generate'); + this._redirectToLogin(data.canonicalPath); + return Promise.reject(new Error()); } + }); + } - return base + url; + /** Page.js middleware that warms the REST API's logged-in cache line. */ + _loadUserMiddleware(ctx, next) { + this.$.restAPI.getLoggedIn().then(() => { next(); }); + } + + /** + * Map a route to a method on the router. + * + * @param {!string|!RegExp} pattern The page.js pattern for the route. + * @param {!string} handlerName The method name for the handler. If the + * route is matched, the handler will be executed with `this` referring + * to the component. Its return value will be discarded so that it does + * not interfere with page.js. + * @param {?boolean=} opt_authRedirect If true, then auth is checked before + * executing the handler. If the user is not logged in, it will redirect + * to the login flow and the handler will not be executed. The login + * redirect specifies the matched URL to be used after successfull auth. + */ + _mapRoute(pattern, handlerName, opt_authRedirect) { + if (!this[handlerName]) { + console.error('Attempted to map route to unknown method: ', + handlerName); + return; + } + page(pattern, this._loadUserMiddleware.bind(this), data => { + this.$.reporting.locationChanged(handlerName); + const promise = opt_authRedirect ? + this._redirectIfNotLoggedIn(data) : Promise.resolve(); + promise.then(() => { this[handlerName](data); }); + }); + } + + _startRouter() { + const base = this.getBaseUrl(); + if (base) { + page.base(base); } - _generateWeblinks(params) { - const type = params.type; - switch (type) { - case Gerrit.Nav.WeblinkType.FILE: - return this._getFileWebLinks(params); - case Gerrit.Nav.WeblinkType.CHANGE: - return this._getChangeWeblinks(params); - case Gerrit.Nav.WeblinkType.PATCHSET: - return this._getPatchSetWeblink(params); - default: - console.warn(`Unsupported weblink ${type}!`); + Gerrit.Nav.setup( + url => { page.show(url); }, + this._generateUrl.bind(this), + params => this._generateWeblinks(params), + x => x + ); + + page.exit('*', (ctx, next) => { + if (!this._isRedirecting) { + this.$.reporting.beforeLocationChanged(); } - } + this._isRedirecting = false; + this._isInitialLoad = false; + next(); + }); - _getPatchSetWeblink(params) { - const {commit, options} = params; - const {weblinks, config} = options || {}; - const name = commit && commit.slice(0, 7); - const weblink = this._getBrowseCommitWeblink(weblinks, config); - if (!weblink || !weblink.url) { - return {name}; - } else { - return {name, url: weblink.url}; - } - } + // Middleware + page((ctx, next) => { + document.body.scrollTop = 0; - _firstCodeBrowserWeblink(weblinks) { - // This is an ordered whitelist of web link types that provide direct - // links to the commit in the url property. - const codeBrowserLinks = ['gitiles', 'browse', 'gitweb']; - for (let i = 0; i < codeBrowserLinks.length; i++) { - const weblink = - weblinks.find(weblink => weblink.name === codeBrowserLinks[i]); - if (weblink) { return weblink; } - } - return null; - } - - _getBrowseCommitWeblink(weblinks, config) { - if (!weblinks) { return null; } - let weblink; - // Use primary weblink if configured and exists. - if (config && config.gerrit && config.gerrit.primary_weblink_name) { - weblink = weblinks.find( - weblink => weblink.name === config.gerrit.primary_weblink_name - ); - } - if (!weblink) { - weblink = this._firstCodeBrowserWeblink(weblinks); - } - if (!weblink) { return null; } - return weblink; - } - - _getChangeWeblinks({repo, commit, options: {weblinks, config}}) { - if (!weblinks || !weblinks.length) return []; - const commitWeblink = this._getBrowseCommitWeblink(weblinks, config); - return weblinks.filter(weblink => - !commitWeblink || - !commitWeblink.name || - weblink.name !== commitWeblink.name); - } - - _getFileWebLinks({repo, commit, file, options: {weblinks}}) { - return weblinks; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateSearchUrl(params) { - let offsetExpr = ''; - if (params.offset && params.offset > 0) { - offsetExpr = ',' + params.offset; - } - - if (params.query) { - return '/q/' + this.encodeURL(params.query, true) + offsetExpr; - } - - const operators = []; - if (params.owner) { - operators.push('owner:' + this.encodeURL(params.owner, false)); - } - if (params.project) { - operators.push('project:' + this.encodeURL(params.project, false)); - } - if (params.branch) { - operators.push('branch:' + this.encodeURL(params.branch, false)); - } - if (params.topic) { - operators.push('topic:"' + this.encodeURL(params.topic, false) + '"'); - } - if (params.hashtag) { - operators.push('hashtag:"' + - this.encodeURL(params.hashtag.toLowerCase(), false) + '"'); - } - if (params.statuses) { - if (params.statuses.length === 1) { - operators.push( - 'status:' + this.encodeURL(params.statuses[0], false)); - } else if (params.statuses.length > 1) { - operators.push( - '(' + - params.statuses.map(s => `status:${this.encodeURL(s, false)}`) - .join(' OR ') + - ')'); - } - } - - return '/q/' + operators.join('+') + offsetExpr; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateChangeUrl(params) { - let range = this._getPatchRangeExpression(params); - if (range.length) { range = '/' + range; } - let suffix = `${range}`; - if (params.querystring) { - suffix += '?' + params.querystring; - } else if (params.edit) { - suffix += ',edit'; - } - if (params.messageHash) { - suffix += params.messageHash; - } - if (params.project) { - const encodedProject = this.encodeURL(params.project, true); - return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; - } else { - return `/c/${params.changeNum}${suffix}`; - } - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateDashboardUrl(params) { - const repoName = params.repo || params.project || null; - if (params.sections) { - // Custom dashboard. - const queryParams = this._sectionsToEncodedParams(params.sections, - repoName); - if (params.title) { - queryParams.push('title=' + encodeURIComponent(params.title)); - } - const user = params.user ? params.user : ''; - return `/dashboard/${user}?${queryParams.join('&')}`; - } else if (repoName) { - // Project dashboard. - const encodedRepo = this.encodeURL(repoName, true); - return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`; - } else { - // User dashboard. - return `/dashboard/${params.user || 'self'}`; - } - } - - /** - * @param {!Array<!{name: string, query: string}>} sections - * @param {string=} opt_repoName - * @return {!Array<string>} - */ - _sectionsToEncodedParams(sections, opt_repoName) { - return sections.map(section => { - // If there is a repo name provided, make sure to substitute it into the - // ${repo} (or legacy ${project}) query tokens. - const query = opt_repoName ? - section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) : - section.query; - return encodeURIComponent(section.name) + '=' + - encodeURIComponent(query); - }); - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateDiffOrEditUrl(params) { - let range = this._getPatchRangeExpression(params); - if (range.length) { range = '/' + range; } - - let suffix = `${range}/${this.encodeURL(params.path, true)}`; - - if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; } - - if (params.lineNum) { - suffix += '#'; - if (params.leftSide) { suffix += 'b'; } - suffix += params.lineNum; - } - - if (params.project) { - const encodedProject = this.encodeURL(params.project, true); - return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; - } else { - return `/c/${params.changeNum}${suffix}`; - } - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateGroupUrl(params) { - let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`; - if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) { - url += ',members'; - } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) { - url += ',audit-log'; - } - return url; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateRepoUrl(params) { - let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`; - if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) { - url += ',access'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) { - url += ',branches'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) { - url += ',tags'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) { - url += ',commands'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) { - url += ',dashboards'; - } - return url; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateSettingsUrl(params) { - return '/settings'; - } - - /** - * Given an object of parameters, potentially including a `patchNum` or a - * `basePatchNum` or both, return a string representation of that range. If - * no range is indicated in the params, the empty string is returned. - * - * @param {!Object} params - * @return {string} - */ - _getPatchRangeExpression(params) { - let range = ''; - if (params.patchNum) { range = '' + params.patchNum; } - if (params.basePatchNum) { range = params.basePatchNum + '..' + range; } - return range; - } - - /** - * Given a set of params without a project, gets the project from the rest - * API project lookup and then sets the app params. - * - * @param {?Object} params - */ - _normalizeLegacyRouteParams(params) { - if (!params.changeNum) { return Promise.resolve(); } - - return this.$.restAPI.getFromProjectLookup(params.changeNum) - .then(project => { - // Show a 404 and terminate if the lookup request failed. Attempting - // to redirect after failing to get the project loops infinitely. - if (!project) { - this._show404(); - return; - } - - params.project = project; - this._normalizePatchRangeParams(params); - this._redirect(this._generateUrl(params)); - }); - } - - /** - * Normalizes the params object, and determines if the URL needs to be - * modified to fit the proper schema. - * - * @param {*} params - * @return {boolean} whether or not the URL needs to be upgraded. - */ - _normalizePatchRangeParams(params) { - const hasBasePatchNum = params.basePatchNum !== null && - params.basePatchNum !== undefined; - const hasPatchNum = params.patchNum !== null && - params.patchNum !== undefined; - let needsRedirect = false; - - // Diffing a patch against itself is invalid, so if the base and revision - // patches are equal clear the base. - if (hasBasePatchNum && - this.patchNumEquals(params.basePatchNum, params.patchNum)) { - needsRedirect = true; - params.basePatchNum = null; - } else if (hasBasePatchNum && !hasPatchNum) { - // Regexes set basePatchNum instead of patchNum when only one is - // specified. Redirect is not needed in this case. - params.patchNum = params.basePatchNum; - params.basePatchNum = null; - } - return needsRedirect; - } - - /** - * Redirect the user to login using the given return-URL for redirection - * after authentication success. - * - * @param {string} returnUrl - */ - _redirectToLogin(returnUrl) { - const basePath = this.getBaseUrl() || ''; - page( - '/login/' + encodeURIComponent(returnUrl.substring(basePath.length))); - } - - /** - * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c" - * is parsed to have a hash of "b" rather than "b#c". Instead, this method - * parses hashes correctly. Will return an empty string if there is no hash. - * - * @param {!string} canonicalPath - * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c"). - */ - _getHashFromCanonicalPath(canonicalPath) { - return canonicalPath.split('#').slice(1) - .join('#'); - } - - _parseLineAddress(hash) { - const match = hash.match(LINE_ADDRESS_PATTERN); - if (!match) { return null; } - return { - leftSide: !!match[1], - lineNum: parseInt(match[2], 10), - }; - } - - /** - * Check to see if the user is logged in and return a promise that only - * resolves if the user is logged in. If the user us not logged in, the - * promise is rejected and the page is redirected to the login flow. - * - * @param {!Object} data The parsed route data. - * @return {!Promise<!Object>} A promise yielding the original route data - * (if it resolves). - */ - _redirectIfNotLoggedIn(data) { - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (loggedIn) { - return Promise.resolve(); - } else { - this._redirectToLogin(data.canonicalPath); - return Promise.reject(new Error()); - } - }); - } - - /** Page.js middleware that warms the REST API's logged-in cache line. */ - _loadUserMiddleware(ctx, next) { - this.$.restAPI.getLoggedIn().then(() => { next(); }); - } - - /** - * Map a route to a method on the router. - * - * @param {!string|!RegExp} pattern The page.js pattern for the route. - * @param {!string} handlerName The method name for the handler. If the - * route is matched, the handler will be executed with `this` referring - * to the component. Its return value will be discarded so that it does - * not interfere with page.js. - * @param {?boolean=} opt_authRedirect If true, then auth is checked before - * executing the handler. If the user is not logged in, it will redirect - * to the login flow and the handler will not be executed. The login - * redirect specifies the matched URL to be used after successfull auth. - */ - _mapRoute(pattern, handlerName, opt_authRedirect) { - if (!this[handlerName]) { - console.error('Attempted to map route to unknown method: ', - handlerName); + if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) { + // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen + // This is needed to allow plugins to add basic #/x/ screen links to + // any location. + this._redirect(ctx.hash); return; } - page(pattern, this._loadUserMiddleware.bind(this), data => { - this.$.reporting.locationChanged(handlerName); - const promise = opt_authRedirect ? - this._redirectIfNotLoggedIn(data) : Promise.resolve(); - promise.then(() => { this[handlerName](data); }); - }); - } - _startRouter() { + // Fire asynchronously so that the URL is changed by the time the event + // is processed. + this.async(() => { + this.fire('location-change', { + hash: window.location.hash, + pathname: window.location.pathname, + }); + }, 1); + next(); + }); + + this._mapRoute(RoutePattern.ROOT, '_handleRootRoute'); + + this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute'); + + this._mapRoute(RoutePattern.CUSTOM_DASHBOARD, + '_handleCustomDashboardRoute'); + + this._mapRoute(RoutePattern.PROJECT_DASHBOARD, + '_handleProjectDashboardRoute'); + + this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true); + + this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute', + true); + + this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute', + true); + + this._mapRoute(RoutePattern.GROUP_LIST_OFFSET, + '_handleGroupListOffsetRoute', true); + + this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET, + '_handleGroupListFilterOffsetRoute', true); + + this._mapRoute(RoutePattern.GROUP_LIST_FILTER, + '_handleGroupListFilterRoute', true); + + this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute', + true); + + this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true); + + this._mapRoute(RoutePattern.PROJECT_OLD, + '_handleProjectsOldRoute'); + + this._mapRoute(RoutePattern.REPO_COMMANDS, + '_handleRepoCommandsRoute', true); + + this._mapRoute(RoutePattern.REPO_ACCESS, + '_handleRepoAccessRoute'); + + this._mapRoute(RoutePattern.REPO_DASHBOARDS, + '_handleRepoDashboardsRoute'); + + this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET, + '_handleBranchListOffsetRoute'); + + this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET, + '_handleBranchListFilterOffsetRoute'); + + this._mapRoute(RoutePattern.BRANCH_LIST_FILTER, + '_handleBranchListFilterRoute'); + + this._mapRoute(RoutePattern.TAG_LIST_OFFSET, + '_handleTagListOffsetRoute'); + + this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET, + '_handleTagListFilterOffsetRoute'); + + this._mapRoute(RoutePattern.TAG_LIST_FILTER, + '_handleTagListFilterRoute'); + + this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP, + '_handleCreateGroupRoute', true); + + this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT, + '_handleCreateProjectRoute', true); + + this._mapRoute(RoutePattern.REPO_LIST_OFFSET, + '_handleRepoListOffsetRoute'); + + this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET, + '_handleRepoListFilterOffsetRoute'); + + this._mapRoute(RoutePattern.REPO_LIST_FILTER, + '_handleRepoListFilterRoute'); + + this._mapRoute(RoutePattern.REPO, '_handleRepoRoute'); + + this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute'); + + this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET, + '_handlePluginListOffsetRoute', true); + + this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET, + '_handlePluginListFilterOffsetRoute', true); + + this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER, + '_handlePluginListFilterRoute', true); + + this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true); + + this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX, + '_handleQueryLegacySuffixRoute'); + + this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute'); + + this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum'); + + this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY, + '_handleChangeNumberLegacyRoute'); + + this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true); + + this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true); + + this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute'); + + this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute'); + + this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute'); + + this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute'); + + this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true); + + this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute', + true); + + this._mapRoute(RoutePattern.SETTINGS_LEGACY, + '_handleSettingsLegacyRoute', true); + + this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true); + + this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute'); + + this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute'); + + this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS, + '_handleImproperlyEncodedPlusRoute'); + + this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen'); + + this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER, + '_handleDocumentationSearchRoute'); + + // redirects /Documentation/q/* to /Documentation/q/filter:* + this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH, + '_handleDocumentationSearchRedirectRoute'); + + // Makes sure /Documentation/* links work (doin't return 404) + this._mapRoute(RoutePattern.DOCUMENTATION, + '_handleDocumentationRedirectRoute'); + + // Note: this route should appear last so it only catches URLs unmatched + // by other patterns. + this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute'); + + page.start(); + } + + /** + * @param {!Object} data + * @return {Promise|null} if handling the route involves asynchrony, then a + * promise is returned. Otherwise, synchronous handling returns null. + */ + _handleRootRoute(data) { + if (data.querystring.match(/^closeAfterLogin/)) { + // Close child window on redirect after login. + window.close(); + return null; + } + let hash = this._getHashFromCanonicalPath(data.canonicalPath); + // For backward compatibility with GWT links. + if (hash) { + // In certain login flows the server may redirect to a hash without + // a leading slash, which page.js doesn't handle correctly. + if (hash[0] !== '/') { + hash = '/' + hash; + } + if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) { + // Path decodes all '+' to ' ' -- this breaks project-based URLs. + // See Issue 6888. + hash = hash.replace('/ /', '/+/'); + } const base = this.getBaseUrl(); - if (base) { - page.base(base); + let newUrl = base + hash; + if (hash.startsWith('/VE/')) { + newUrl = base + '/settings' + hash; } - - Gerrit.Nav.setup( - url => { page.show(url); }, - this._generateUrl.bind(this), - params => this._generateWeblinks(params), - x => x - ); - - page.exit('*', (ctx, next) => { - if (!this._isRedirecting) { - this.$.reporting.beforeLocationChanged(); - } - this._isRedirecting = false; - this._isInitialLoad = false; - next(); - }); - - // Middleware - page((ctx, next) => { - document.body.scrollTop = 0; - - if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) { - // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen - // This is needed to allow plugins to add basic #/x/ screen links to - // any location. - this._redirect(ctx.hash); - return; - } - - // Fire asynchronously so that the URL is changed by the time the event - // is processed. - this.async(() => { - this.fire('location-change', { - hash: window.location.hash, - pathname: window.location.pathname, - }); - }, 1); - next(); - }); - - this._mapRoute(RoutePattern.ROOT, '_handleRootRoute'); - - this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute'); - - this._mapRoute(RoutePattern.CUSTOM_DASHBOARD, - '_handleCustomDashboardRoute'); - - this._mapRoute(RoutePattern.PROJECT_DASHBOARD, - '_handleProjectDashboardRoute'); - - this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true); - - this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute', - true); - - this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute', - true); - - this._mapRoute(RoutePattern.GROUP_LIST_OFFSET, - '_handleGroupListOffsetRoute', true); - - this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET, - '_handleGroupListFilterOffsetRoute', true); - - this._mapRoute(RoutePattern.GROUP_LIST_FILTER, - '_handleGroupListFilterRoute', true); - - this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute', - true); - - this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true); - - this._mapRoute(RoutePattern.PROJECT_OLD, - '_handleProjectsOldRoute'); - - this._mapRoute(RoutePattern.REPO_COMMANDS, - '_handleRepoCommandsRoute', true); - - this._mapRoute(RoutePattern.REPO_ACCESS, - '_handleRepoAccessRoute'); - - this._mapRoute(RoutePattern.REPO_DASHBOARDS, - '_handleRepoDashboardsRoute'); - - this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET, - '_handleBranchListOffsetRoute'); - - this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET, - '_handleBranchListFilterOffsetRoute'); - - this._mapRoute(RoutePattern.BRANCH_LIST_FILTER, - '_handleBranchListFilterRoute'); - - this._mapRoute(RoutePattern.TAG_LIST_OFFSET, - '_handleTagListOffsetRoute'); - - this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET, - '_handleTagListFilterOffsetRoute'); - - this._mapRoute(RoutePattern.TAG_LIST_FILTER, - '_handleTagListFilterRoute'); - - this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP, - '_handleCreateGroupRoute', true); - - this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT, - '_handleCreateProjectRoute', true); - - this._mapRoute(RoutePattern.REPO_LIST_OFFSET, - '_handleRepoListOffsetRoute'); - - this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET, - '_handleRepoListFilterOffsetRoute'); - - this._mapRoute(RoutePattern.REPO_LIST_FILTER, - '_handleRepoListFilterRoute'); - - this._mapRoute(RoutePattern.REPO, '_handleRepoRoute'); - - this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute'); - - this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET, - '_handlePluginListOffsetRoute', true); - - this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET, - '_handlePluginListFilterOffsetRoute', true); - - this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER, - '_handlePluginListFilterRoute', true); - - this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true); - - this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX, - '_handleQueryLegacySuffixRoute'); - - this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute'); - - this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum'); - - this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY, - '_handleChangeNumberLegacyRoute'); - - this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true); - - this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true); - - this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute'); - - this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute'); - - this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute'); - - this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute'); - - this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true); - - this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute', - true); - - this._mapRoute(RoutePattern.SETTINGS_LEGACY, - '_handleSettingsLegacyRoute', true); - - this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true); - - this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute'); - - this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute'); - - this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS, - '_handleImproperlyEncodedPlusRoute'); - - this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen'); - - this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER, - '_handleDocumentationSearchRoute'); - - // redirects /Documentation/q/* to /Documentation/q/filter:* - this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH, - '_handleDocumentationSearchRedirectRoute'); - - // Makes sure /Documentation/* links work (doin't return 404) - this._mapRoute(RoutePattern.DOCUMENTATION, - '_handleDocumentationRedirectRoute'); - - // Note: this route should appear last so it only catches URLs unmatched - // by other patterns. - this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute'); - - page.start(); + this._redirect(newUrl); + return null; } + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + this._redirect('/dashboard/self'); + } else { + this._redirect('/q/status:open'); + } + }); + } - /** - * @param {!Object} data - * @return {Promise|null} if handling the route involves asynchrony, then a - * promise is returned. Otherwise, synchronous handling returns null. - */ - _handleRootRoute(data) { - if (data.querystring.match(/^closeAfterLogin/)) { - // Close child window on redirect after login. - window.close(); - return null; + /** + * Decode an application/x-www-form-urlencoded string. + * + * @param {string} qs The application/x-www-form-urlencoded string. + * @return {string} The decoded string. + */ + _decodeQueryString(qs) { + return decodeURIComponent(qs.replace(PLUS_PATTERN, ' ')); + } + + /** + * Parse a query string (e.g. window.location.search) into an array of + * name/value pairs. + * + * @param {string} qs The application/x-www-form-urlencoded query string. + * @return {!Array<!Array<string>>} An array of name/value pairs, where each + * element is a 2-element array. + */ + _parseQueryString(qs) { + qs = qs.replace(QUESTION_PATTERN, ''); + if (!qs) { + return []; + } + const params = []; + qs.split('&').forEach(param => { + const idx = param.indexOf('='); + let name; + let value; + if (idx < 0) { + name = this._decodeQueryString(param); + value = ''; + } else { + name = this._decodeQueryString(param.substring(0, idx)); + value = this._decodeQueryString(param.substring(idx + 1)); } - let hash = this._getHashFromCanonicalPath(data.canonicalPath); - // For backward compatibility with GWT links. - if (hash) { - // In certain login flows the server may redirect to a hash without - // a leading slash, which page.js doesn't handle correctly. - if (hash[0] !== '/') { - hash = '/' + hash; - } - if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) { - // Path decodes all '+' to ' ' -- this breaks project-based URLs. - // See Issue 6888. - hash = hash.replace('/ /', '/+/'); - } - const base = this.getBaseUrl(); - let newUrl = base + hash; - if (hash.startsWith('/VE/')) { - newUrl = base + '/settings' + hash; - } - this._redirect(newUrl); - return null; + if (name) { + params.push([name, value]); } - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (loggedIn) { - this._redirect('/dashboard/self'); + }); + return params; + } + + /** + * Handle dashboard routes. These may be user, or project dashboards. + * + * @param {!Object} data The parsed route data. + */ + _handleDashboardRoute(data) { + // User dashboard. We require viewing user to be logged in, else we + // redirect to login for self dashboard or simple owner search for + // other user dashboard. + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (!loggedIn) { + if (data.params[0].toLowerCase() === 'self') { + this._redirectToLogin(data.canonicalPath); } else { - this._redirect('/q/status:open'); + this._redirect('/q/owner:' + encodeURIComponent(data.params[0])); } - }); - } - - /** - * Decode an application/x-www-form-urlencoded string. - * - * @param {string} qs The application/x-www-form-urlencoded string. - * @return {string} The decoded string. - */ - _decodeQueryString(qs) { - return decodeURIComponent(qs.replace(PLUS_PATTERN, ' ')); - } - - /** - * Parse a query string (e.g. window.location.search) into an array of - * name/value pairs. - * - * @param {string} qs The application/x-www-form-urlencoded query string. - * @return {!Array<!Array<string>>} An array of name/value pairs, where each - * element is a 2-element array. - */ - _parseQueryString(qs) { - qs = qs.replace(QUESTION_PATTERN, ''); - if (!qs) { - return []; - } - const params = []; - qs.split('&').forEach(param => { - const idx = param.indexOf('='); - let name; - let value; - if (idx < 0) { - name = this._decodeQueryString(param); - value = ''; - } else { - name = this._decodeQueryString(param.substring(0, idx)); - value = this._decodeQueryString(param.substring(idx + 1)); - } - if (name) { - params.push([name, value]); - } - }); - return params; - } - - /** - * Handle dashboard routes. These may be user, or project dashboards. - * - * @param {!Object} data The parsed route data. - */ - _handleDashboardRoute(data) { - // User dashboard. We require viewing user to be logged in, else we - // redirect to login for self dashboard or simple owner search for - // other user dashboard. - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (!loggedIn) { - if (data.params[0].toLowerCase() === 'self') { - this._redirectToLogin(data.canonicalPath); - } else { - this._redirect('/q/owner:' + encodeURIComponent(data.params[0])); - } - } else { - this._setParams({ - view: Gerrit.Nav.View.DASHBOARD, - user: data.params[0], - }); - } - }); - } - - /** - * Handle custom dashboard routes. - * - * @param {!Object} data The parsed route data. - * @param {string=} opt_qs Optional query string associated with the route. - * If not given, window.location.search is used. (Used by tests). - */ - _handleCustomDashboardRoute(data, opt_qs) { - // opt_qs may be provided by a test, and it may have a falsy value - const qs = opt_qs !== undefined ? opt_qs : window.location.search; - const queryParams = this._parseQueryString(qs); - let title = 'Custom Dashboard'; - const titleParam = queryParams.find( - elem => elem[0].toLowerCase() === 'title'); - if (titleParam) { - title = titleParam[1]; - } - // Dashboards support a foreach param which adds a base query to any - // additional query. - const forEachParam = queryParams.find( - elem => elem[0].toLowerCase() === 'foreach'); - let forEachQuery = null; - if (forEachParam) { - forEachQuery = forEachParam[1]; - } - const sectionParams = queryParams.filter( - elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' && - elem[0].toLowerCase() !== 'foreach'); - const sections = sectionParams.map(elem => { - const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1]; - return { - name: elem[0], - query, - }; - }); - - if (sections.length > 0) { - // Custom dashboard view. + } else { this._setParams({ view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections, - title, + user: data.params[0], }); - return Promise.resolve(); } + }); + } - // Redirect /dashboard/ -> /dashboard/self. - this._redirect('/dashboard/self'); + /** + * Handle custom dashboard routes. + * + * @param {!Object} data The parsed route data. + * @param {string=} opt_qs Optional query string associated with the route. + * If not given, window.location.search is used. (Used by tests). + */ + _handleCustomDashboardRoute(data, opt_qs) { + // opt_qs may be provided by a test, and it may have a falsy value + const qs = opt_qs !== undefined ? opt_qs : window.location.search; + const queryParams = this._parseQueryString(qs); + let title = 'Custom Dashboard'; + const titleParam = queryParams.find( + elem => elem[0].toLowerCase() === 'title'); + if (titleParam) { + title = titleParam[1]; + } + // Dashboards support a foreach param which adds a base query to any + // additional query. + const forEachParam = queryParams.find( + elem => elem[0].toLowerCase() === 'foreach'); + let forEachQuery = null; + if (forEachParam) { + forEachQuery = forEachParam[1]; + } + const sectionParams = queryParams.filter( + elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' && + elem[0].toLowerCase() !== 'foreach'); + const sections = sectionParams.map(elem => { + const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1]; + return { + name: elem[0], + query, + }; + }); + + if (sections.length > 0) { + // Custom dashboard view. + this._setParams({ + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections, + title, + }); return Promise.resolve(); } - _handleProjectDashboardRoute(data) { - const project = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.DASHBOARD, - project, - dashboard: decodeURIComponent(data.params[1]), - }); - this.$.reporting.setRepoName(project); - } + // Redirect /dashboard/ -> /dashboard/self. + this._redirect('/dashboard/self'); + return Promise.resolve(); + } - _handleGroupInfoRoute(data) { - this._redirect('/admin/groups/' + encodeURIComponent(data.params[0])); - } + _handleProjectDashboardRoute(data) { + const project = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.DASHBOARD, + project, + dashboard: decodeURIComponent(data.params[1]), + }); + this.$.reporting.setRepoName(project); + } - _handleGroupSelfRedirectRoute(data) { - this._redirect('/settings/#Groups'); - } + _handleGroupInfoRoute(data) { + this._redirect('/admin/groups/' + encodeURIComponent(data.params[0])); + } - _handleGroupRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.GROUP, - groupId: data.params[0], - }); - } + _handleGroupSelfRedirectRoute(data) { + this._redirect('/settings/#Groups'); + } - _handleGroupAuditLogRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.GROUP, - detail: Gerrit.Nav.GroupDetailView.LOG, - groupId: data.params[0], - }); - } + _handleGroupRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.GROUP, + groupId: data.params[0], + }); + } - _handleGroupMembersRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.GROUP, - detail: Gerrit.Nav.GroupDetailView.MEMBERS, - groupId: data.params[0], - }); - } + _handleGroupAuditLogRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.GROUP, + detail: Gerrit.Nav.GroupDetailView.LOG, + groupId: data.params[0], + }); + } - _handleGroupListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - offset: data.params[1] || 0, - filter: null, - openCreateModal: data.hash === 'create', - }); - } + _handleGroupMembersRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.GROUP, + detail: Gerrit.Nav.GroupDetailView.MEMBERS, + groupId: data.params[0], + }); + } - _handleGroupListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - offset: data.params.offset, - filter: data.params.filter, - }); - } + _handleGroupListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: data.params[1] || 0, + filter: null, + openCreateModal: data.hash === 'create', + }); + } - _handleGroupListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - filter: data.params.filter || null, - }); - } + _handleGroupListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: data.params.offset, + filter: data.params.filter, + }); + } - _handleProjectsOldRoute(data) { - let params = ''; - if (data.params[1]) { - params = encodeURIComponent(data.params[1]); - if (data.params[1].includes(',')) { - params = - encodeURIComponent(data.params[1]).replace('%2C', ','); - } - } + _handleGroupListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + filter: data.params.filter || null, + }); + } - this._redirect(`/admin/repos/${params}`); - } - - _handleRepoCommandsRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.COMMANDS, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handleRepoAccessRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handleRepoDashboardsRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handleBranchListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: data.params[0], - offset: data.params[2] || 0, - filter: null, - }); - } - - _handleBranchListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: data.params.repo, - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handleBranchListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: data.params.repo, - filter: data.params.filter || null, - }); - } - - _handleTagListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: data.params[0], - offset: data.params[2] || 0, - filter: null, - }); - } - - _handleTagListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: data.params.repo, - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handleTagListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: data.params.repo, - filter: data.params.filter || null, - }); - } - - _handleRepoListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: data.params[1] || 0, - filter: null, - openCreateModal: data.hash === 'create', - }); - } - - _handleRepoListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handleRepoListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - filter: data.params.filter || null, - }); - } - - _handleCreateProjectRoute(data) { - // Redirects the legacy route to the new route, which displays the project - // list with a hash 'create'. - this._redirect('/admin/repos#create'); - } - - _handleCreateGroupRoute(data) { - // Redirects the legacy route to the new route, which displays the group - // list with a hash 'create'. - this._redirect('/admin/groups#create'); - } - - _handleRepoRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handlePluginListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: data.params[1] || 0, - filter: null, - }); - } - - _handlePluginListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handlePluginListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - filter: data.params.filter || null, - }); - } - - _handlePluginListRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - }); - } - - _handleQueryRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.SEARCH, - query: data.params[0], - offset: data.params[2], - }); - } - - _handleQueryLegacySuffixRoute(ctx) { - this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, '')); - } - - _handleChangeNumberLegacyRoute(ctx) { - this._redirect('/c/' + encodeURIComponent(ctx.params[0])); - } - - _handleChangeRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - project: ctx.params[0], - changeNum: ctx.params[1], - basePatchNum: ctx.params[4], - patchNum: ctx.params[6], - view: Gerrit.Nav.View.CHANGE, - }; - - this.$.reporting.setRepoName(params.project); - this._redirectOrNavigate(params); - } - - _handleDiffRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - project: ctx.params[0], - changeNum: ctx.params[1], - basePatchNum: ctx.params[4], - patchNum: ctx.params[6], - path: ctx.params[8], - view: Gerrit.Nav.View.DIFF, - }; - - const address = this._parseLineAddress(ctx.hash); - if (address) { - params.leftSide = address.leftSide; - params.lineNum = address.lineNum; - } - this.$.reporting.setRepoName(params.project); - this._redirectOrNavigate(params); - } - - _handleChangeLegacyRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - changeNum: ctx.params[0], - basePatchNum: ctx.params[3], - patchNum: ctx.params[5], - view: Gerrit.Nav.View.CHANGE, - querystring: ctx.querystring, - }; - - this._normalizeLegacyRouteParams(params); - } - - _handleLegacyLinenum(ctx) { - this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1')); - } - - _handleDiffLegacyRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - changeNum: ctx.params[0], - basePatchNum: ctx.params[2], - patchNum: ctx.params[4], - path: ctx.params[5], - view: Gerrit.Nav.View.DIFF, - }; - - const address = this._parseLineAddress(ctx.hash); - if (address) { - params.leftSide = address.leftSide; - params.lineNum = address.lineNum; - } - - this._normalizeLegacyRouteParams(params); - } - - _handleDiffEditRoute(ctx) { - // Parameter order is based on the regex group number matched. - const project = ctx.params[0]; - this._redirectOrNavigate({ - project, - changeNum: ctx.params[1], - patchNum: ctx.params[2], - path: ctx.params[3], - lineNum: ctx.hash, - view: Gerrit.Nav.View.EDIT, - }); - this.$.reporting.setRepoName(project); - } - - _handleChangeEditRoute(ctx) { - // Parameter order is based on the regex group number matched. - const project = ctx.params[0]; - this._redirectOrNavigate({ - project, - changeNum: ctx.params[1], - patchNum: ctx.params[3], - view: Gerrit.Nav.View.CHANGE, - edit: true, - }); - this.$.reporting.setRepoName(project); - } - - /** - * Normalize the patch range params for a the change or diff view and - * redirect if URL upgrade is needed. - */ - _redirectOrNavigate(params) { - const needsRedirect = this._normalizePatchRangeParams(params); - if (needsRedirect) { - this._redirect(this._generateUrl(params)); - } else { - this._setParams(params); + _handleProjectsOldRoute(data) { + let params = ''; + if (data.params[1]) { + params = encodeURIComponent(data.params[1]); + if (data.params[1].includes(',')) { + params = + encodeURIComponent(data.params[1]).replace('%2C', ','); } } - _handleAgreementsRoute() { - this._redirect('/settings/#Agreements'); + this._redirect(`/admin/repos/${params}`); + } + + _handleRepoCommandsRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.COMMANDS, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handleRepoAccessRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handleRepoDashboardsRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handleBranchListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: data.params[0], + offset: data.params[2] || 0, + filter: null, + }); + } + + _handleBranchListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: data.params.repo, + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handleBranchListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: data.params.repo, + filter: data.params.filter || null, + }); + } + + _handleTagListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: data.params[0], + offset: data.params[2] || 0, + filter: null, + }); + } + + _handleTagListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: data.params.repo, + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handleTagListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: data.params.repo, + filter: data.params.filter || null, + }); + } + + _handleRepoListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-repo-list', + offset: data.params[1] || 0, + filter: null, + openCreateModal: data.hash === 'create', + }); + } + + _handleRepoListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-repo-list', + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handleRepoListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-repo-list', + filter: data.params.filter || null, + }); + } + + _handleCreateProjectRoute(data) { + // Redirects the legacy route to the new route, which displays the project + // list with a hash 'create'. + this._redirect('/admin/repos#create'); + } + + _handleCreateGroupRoute(data) { + // Redirects the legacy route to the new route, which displays the group + // list with a hash 'create'. + this._redirect('/admin/groups#create'); + } + + _handleRepoRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handlePluginListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: data.params[1] || 0, + filter: null, + }); + } + + _handlePluginListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handlePluginListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + filter: data.params.filter || null, + }); + } + + _handlePluginListRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + }); + } + + _handleQueryRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.SEARCH, + query: data.params[0], + offset: data.params[2], + }); + } + + _handleQueryLegacySuffixRoute(ctx) { + this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, '')); + } + + _handleChangeNumberLegacyRoute(ctx) { + this._redirect('/c/' + encodeURIComponent(ctx.params[0])); + } + + _handleChangeRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + project: ctx.params[0], + changeNum: ctx.params[1], + basePatchNum: ctx.params[4], + patchNum: ctx.params[6], + view: Gerrit.Nav.View.CHANGE, + }; + + this.$.reporting.setRepoName(params.project); + this._redirectOrNavigate(params); + } + + _handleDiffRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + project: ctx.params[0], + changeNum: ctx.params[1], + basePatchNum: ctx.params[4], + patchNum: ctx.params[6], + path: ctx.params[8], + view: Gerrit.Nav.View.DIFF, + }; + + const address = this._parseLineAddress(ctx.hash); + if (address) { + params.leftSide = address.leftSide; + params.lineNum = address.lineNum; + } + this.$.reporting.setRepoName(params.project); + this._redirectOrNavigate(params); + } + + _handleChangeLegacyRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + changeNum: ctx.params[0], + basePatchNum: ctx.params[3], + patchNum: ctx.params[5], + view: Gerrit.Nav.View.CHANGE, + querystring: ctx.querystring, + }; + + this._normalizeLegacyRouteParams(params); + } + + _handleLegacyLinenum(ctx) { + this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1')); + } + + _handleDiffLegacyRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + changeNum: ctx.params[0], + basePatchNum: ctx.params[2], + patchNum: ctx.params[4], + path: ctx.params[5], + view: Gerrit.Nav.View.DIFF, + }; + + const address = this._parseLineAddress(ctx.hash); + if (address) { + params.leftSide = address.leftSide; + params.lineNum = address.lineNum; } - _handleNewAgreementsRoute(data) { - data.params.view = Gerrit.Nav.View.AGREEMENTS; - this._setParams(data.params); - } + this._normalizeLegacyRouteParams(params); + } - _handleSettingsLegacyRoute(data) { - // email tokens may contain '+' but no space. - // The parameter parsing replaces all '+' with a space, - // undo that to have valid tokens. - const token = data.params[0].replace(/ /g, '+'); - this._setParams({ - view: Gerrit.Nav.View.SETTINGS, - emailToken: token, - }); - } + _handleDiffEditRoute(ctx) { + // Parameter order is based on the regex group number matched. + const project = ctx.params[0]; + this._redirectOrNavigate({ + project, + changeNum: ctx.params[1], + patchNum: ctx.params[2], + path: ctx.params[3], + lineNum: ctx.hash, + view: Gerrit.Nav.View.EDIT, + }); + this.$.reporting.setRepoName(project); + } - _handleSettingsRoute(data) { - this._setParams({view: Gerrit.Nav.View.SETTINGS}); - } + _handleChangeEditRoute(ctx) { + // Parameter order is based on the regex group number matched. + const project = ctx.params[0]; + this._redirectOrNavigate({ + project, + changeNum: ctx.params[1], + patchNum: ctx.params[3], + view: Gerrit.Nav.View.CHANGE, + edit: true, + }); + this.$.reporting.setRepoName(project); + } - _handleRegisterRoute(ctx) { - this._setParams({justRegistered: true}); - let path = ctx.params[0] || '/'; - - // Prevent redirect looping. - if (path.startsWith('/register')) { path = '/'; } - - if (path[0] !== '/') { return; } - this._redirect(this.getBaseUrl() + path); - } - - /** - * Handler for routes that should pass through the router and not be caught - * by the catchall _handleDefaultRoute handler. - */ - _handlePassThroughRoute() { - location.reload(); - } - - /** - * URL may sometimes have /+/ encoded to / /. - * Context: Issue 6888, Issue 7100 - */ - _handleImproperlyEncodedPlusRoute(ctx) { - let hash = this._getHashFromCanonicalPath(ctx.canonicalPath); - if (hash.length) { hash = '#' + hash; } - this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`); - } - - _handlePluginScreen(ctx) { - const view = Gerrit.Nav.View.PLUGIN_SCREEN; - const plugin = ctx.params[0]; - const screen = ctx.params[1]; - this._setParams({view, plugin, screen}); - } - - _handleDocumentationSearchRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.DOCUMENTATION_SEARCH, - filter: data.params.filter || null, - }); - } - - _handleDocumentationSearchRedirectRoute(data) { - this._redirect('/Documentation/q/filter:' + - encodeURIComponent(data.params[0])); - } - - _handleDocumentationRedirectRoute(data) { - if (data.params[1]) { - location.reload(); - } else { - // Redirect /Documentation to /Documentation/index.html - this._redirect('/Documentation/index.html'); - } - } - - /** - * Catchall route for when no other route is matched. - */ - _handleDefaultRoute() { - if (this._isInitialLoad) { - // Server recognized this route as polygerrit, so we show 404. - this._show404(); - } else { - // Route can be recognized by server, so we pass it to server. - this._handlePassThroughRoute(); - } - } - - _show404() { - // Note: the app's 404 display is tightly-coupled with catching 404 - // network responses, so we simulate a 404 response status to display it. - // TODO: Decouple the gr-app error view from network responses. - this._appElement().dispatchEvent(new CustomEvent('page-error', - {detail: {response: {status: 404}}})); + /** + * Normalize the patch range params for a the change or diff view and + * redirect if URL upgrade is needed. + */ + _redirectOrNavigate(params) { + const needsRedirect = this._normalizePatchRangeParams(params); + if (needsRedirect) { + this._redirect(this._generateUrl(params)); + } else { + this._setParams(params); } } - customElements.define(GrRouter.is, GrRouter); -})(); + _handleAgreementsRoute() { + this._redirect('/settings/#Agreements'); + } + + _handleNewAgreementsRoute(data) { + data.params.view = Gerrit.Nav.View.AGREEMENTS; + this._setParams(data.params); + } + + _handleSettingsLegacyRoute(data) { + // email tokens may contain '+' but no space. + // The parameter parsing replaces all '+' with a space, + // undo that to have valid tokens. + const token = data.params[0].replace(/ /g, '+'); + this._setParams({ + view: Gerrit.Nav.View.SETTINGS, + emailToken: token, + }); + } + + _handleSettingsRoute(data) { + this._setParams({view: Gerrit.Nav.View.SETTINGS}); + } + + _handleRegisterRoute(ctx) { + this._setParams({justRegistered: true}); + let path = ctx.params[0] || '/'; + + // Prevent redirect looping. + if (path.startsWith('/register')) { path = '/'; } + + if (path[0] !== '/') { return; } + this._redirect(this.getBaseUrl() + path); + } + + /** + * Handler for routes that should pass through the router and not be caught + * by the catchall _handleDefaultRoute handler. + */ + _handlePassThroughRoute() { + location.reload(); + } + + /** + * URL may sometimes have /+/ encoded to / /. + * Context: Issue 6888, Issue 7100 + */ + _handleImproperlyEncodedPlusRoute(ctx) { + let hash = this._getHashFromCanonicalPath(ctx.canonicalPath); + if (hash.length) { hash = '#' + hash; } + this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`); + } + + _handlePluginScreen(ctx) { + const view = Gerrit.Nav.View.PLUGIN_SCREEN; + const plugin = ctx.params[0]; + const screen = ctx.params[1]; + this._setParams({view, plugin, screen}); + } + + _handleDocumentationSearchRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.DOCUMENTATION_SEARCH, + filter: data.params.filter || null, + }); + } + + _handleDocumentationSearchRedirectRoute(data) { + this._redirect('/Documentation/q/filter:' + + encodeURIComponent(data.params[0])); + } + + _handleDocumentationRedirectRoute(data) { + if (data.params[1]) { + location.reload(); + } else { + // Redirect /Documentation to /Documentation/index.html + this._redirect('/Documentation/index.html'); + } + } + + /** + * Catchall route for when no other route is matched. + */ + _handleDefaultRoute() { + if (this._isInitialLoad) { + // Server recognized this route as polygerrit, so we show 404. + this._show404(); + } else { + // Route can be recognized by server, so we pass it to server. + this._handlePassThroughRoute(); + } + } + + _show404() { + // Note: the app's 404 display is tightly-coupled with catching 404 + // network responses, so we simulate a 404 response status to display it. + // TODO: Decouple the gr-app error view from network responses. + this._appElement().dispatchEvent(new CustomEvent('page-error', + {detail: {response: {status: 404}}})); + } +} + +customElements.define(GrRouter.is, GrRouter);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js index 5d2531e..01acaa3 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
@@ -1,34 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.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="../gr-reporting/gr-reporting.html"> -<script src="/bower_components/page/page.js"></script> - -<dom-module id="gr-router"> - <template> +export const htmlTemplate = html` <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-router.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html index f127a91..6ea07a5 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-router</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-router.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-router.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-router.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,1617 +40,1619 @@ </template> </test-fixture> -<script> - suite('gr-router tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-router.js'; +suite('gr-router tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { sandbox.restore(); }); + + test('_firstCodeBrowserWeblink', () => { + assert.deepEqual(element._firstCodeBrowserWeblink([ + {name: 'gitweb'}, + {name: 'gitiles'}, + {name: 'browse'}, + {name: 'test'}]), {name: 'gitiles'}); + + assert.deepEqual(element._firstCodeBrowserWeblink([ + {name: 'gitweb'}, + {name: 'test'}]), {name: 'gitweb'}); + }); + + test('_getBrowseCommitWeblink', () => { + const browserLink = {name: 'browser', url: 'browser/url'}; + const link = {name: 'test', url: 'test/url'}; + const weblinks = [browserLink, link]; + const config = {gerrit: {primary_weblink_name: browserLink.name}}; + sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link); + + assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config), + browserLink); + + assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link); + }); + + test('_getChangeWeblinks', () => { + const link = {name: 'test', url: 'test/url'}; + const browserLink = {name: 'browser', url: 'browser/url'}; + const mapLinksToConfig = weblinks => { return {options: {weblinks}}; }; + sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink); + + assert.deepEqual( + element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0], + {name: 'test', url: 'test/url'}); + + assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], + {name: 'test', url: 'test/url'}); + + link.url = 'https://' + link.url; + assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], + {name: 'test', url: 'https://test/url'}); + }); + + test('_getHashFromCanonicalPath', () => { + let url = '/foo/bar'; + let hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, ''); + + url = ''; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, ''); + + url = '/foo#bar'; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, 'bar'); + + url = '/foo#bar#baz'; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, 'bar#baz'); + + url = '#foo#bar#baz'; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, 'foo#bar#baz'); + }); + + suite('_parseLineAddress', () => { + test('returns null for empty and invalid hashes', () => { + let actual = element._parseLineAddress(''); + assert.isNull(actual); + + actual = element._parseLineAddress('foobar'); + assert.isNull(actual); + + actual = element._parseLineAddress('foo123'); + assert.isNull(actual); + + actual = element._parseLineAddress('123bar'); + assert.isNull(actual); + }); + + test('parses correctly', () => { + let actual = element._parseLineAddress('1234'); + assert.isOk(actual); + assert.equal(actual.lineNum, 1234); + assert.isFalse(actual.leftSide); + + actual = element._parseLineAddress('a4'); + assert.isOk(actual); + assert.equal(actual.lineNum, 4); + assert.isTrue(actual.leftSide); + + actual = element._parseLineAddress('b77'); + assert.isOk(actual); + assert.equal(actual.lineNum, 77); + assert.isTrue(actual.leftSide); + }); + }); + + test('_startRouter requires auth for the right handlers', () => { + // This test encodes the lists of route handler methods that gr-router + // automatically checks for authentication before triggering. + + const requiresAuth = {}; + const doesNotRequireAuth = {}; + sandbox.stub(Gerrit.Nav, 'setup'); + sandbox.stub(window.page, 'start'); + sandbox.stub(window.page, 'base'); + sandbox.stub(window, 'page'); + sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => { + if (usesAuth) { + requiresAuth[methodName] = true; + } else { + doesNotRequireAuth[methodName] = true; + } + }); + element._startRouter(); + + const actualRequiresAuth = Object.keys(requiresAuth); + actualRequiresAuth.sort(); + const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth); + actualDoesNotRequireAuth.sort(); + + const shouldRequireAutoAuth = [ + '_handleAgreementsRoute', + '_handleChangeEditRoute', + '_handleCreateGroupRoute', + '_handleCreateProjectRoute', + '_handleDiffEditRoute', + '_handleGroupAuditLogRoute', + '_handleGroupInfoRoute', + '_handleGroupListFilterOffsetRoute', + '_handleGroupListFilterRoute', + '_handleGroupListOffsetRoute', + '_handleGroupMembersRoute', + '_handleGroupRoute', + '_handleGroupSelfRedirectRoute', + '_handleNewAgreementsRoute', + '_handlePluginListFilterOffsetRoute', + '_handlePluginListFilterRoute', + '_handlePluginListOffsetRoute', + '_handlePluginListRoute', + '_handleRepoCommandsRoute', + '_handleSettingsLegacyRoute', + '_handleSettingsRoute', + ]; + assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth); + + const unauthenticatedHandlers = [ + '_handleBranchListFilterOffsetRoute', + '_handleBranchListFilterRoute', + '_handleBranchListOffsetRoute', + '_handleChangeNumberLegacyRoute', + '_handleChangeRoute', + '_handleDiffRoute', + '_handleDefaultRoute', + '_handleChangeLegacyRoute', + '_handleDiffLegacyRoute', + '_handleDocumentationRedirectRoute', + '_handleDocumentationSearchRoute', + '_handleDocumentationSearchRedirectRoute', + '_handleLegacyLinenum', + '_handleImproperlyEncodedPlusRoute', + '_handlePassThroughRoute', + '_handleProjectDashboardRoute', + '_handleProjectsOldRoute', + '_handleRepoAccessRoute', + '_handleRepoDashboardsRoute', + '_handleRepoListFilterOffsetRoute', + '_handleRepoListFilterRoute', + '_handleRepoListOffsetRoute', + '_handleRepoRoute', + '_handleQueryLegacySuffixRoute', + '_handleQueryRoute', + '_handleRegisterRoute', + '_handleTagListFilterOffsetRoute', + '_handleTagListFilterRoute', + '_handleTagListOffsetRoute', + '_handlePluginScreen', + ]; + + // Handler names that check authentication themselves, and thus don't need + // it performed for them. + const selfAuthenticatingHandlers = [ + '_handleDashboardRoute', + '_handleCustomDashboardRoute', + '_handleRootRoute', + ]; + + const shouldNotRequireAuth = unauthenticatedHandlers + .concat(selfAuthenticatingHandlers); + shouldNotRequireAuth.sort(); + assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth); + }); + + test('_redirectIfNotLoggedIn while logged in', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + const data = {canonicalPath: ''}; + const redirectStub = sandbox.stub(element, '_redirectToLogin'); + return element._redirectIfNotLoggedIn(data).then(() => { + assert.isFalse(redirectStub.called); + }); + }); + + test('_redirectIfNotLoggedIn while logged out', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const redirectStub = sandbox.stub(element, '_redirectToLogin'); + const data = {canonicalPath: ''}; + return new Promise(resolve => { + element._redirectIfNotLoggedIn(data) + .then(() => { + assert.isTrue(false, 'Should never execute'); + }) + .catch(() => { + assert.isTrue(redirectStub.calledOnce); + resolve(); + }); + }); + }); + + suite('generateUrl', () => { + test('search', () => { + let params = { + view: Gerrit.Nav.View.SEARCH, + owner: 'a%b', + project: 'c%d', + branch: 'e%f', + topic: 'g%h', + statuses: ['op%en'], + }; + assert.equal(element._generateUrl(params), + '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + + 'topic:"g%2525h"+status:op%2525en'); + + params.offset = 100; + assert.equal(element._generateUrl(params), + '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + + 'topic:"g%2525h"+status:op%2525en,100'); + delete params.offset; + + // The presence of the query param overrides other params. + params.query = 'foo$bar'; + assert.equal(element._generateUrl(params), '/q/foo%2524bar'); + + params.offset = 100; + assert.equal(element._generateUrl(params), '/q/foo%2524bar,100'); + + params = { + view: Gerrit.Nav.View.SEARCH, + statuses: ['a', 'b', 'c'], + }; + assert.equal(element._generateUrl(params), + '/q/(status:a OR status:b OR status:c)'); + }); + + test('change', () => { + const params = { + view: Gerrit.Nav.View.CHANGE, + changeNum: '1234', + project: 'test', + }; + const paramsWithQuery = { + view: Gerrit.Nav.View.CHANGE, + changeNum: '1234', + project: 'test', + querystring: 'revert&foo=bar', + }; + + assert.equal(element._generateUrl(params), '/c/test/+/1234'); + assert.equal(element._generateUrl(paramsWithQuery), + '/c/test/+/1234?revert&foo=bar'); + + params.patchNum = 10; + assert.equal(element._generateUrl(params), '/c/test/+/1234/10'); + paramsWithQuery.patchNum = 10; + assert.equal(element._generateUrl(paramsWithQuery), + '/c/test/+/1234/10?revert&foo=bar'); + + params.basePatchNum = 5; + assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10'); + paramsWithQuery.basePatchNum = 5; + assert.equal(element._generateUrl(paramsWithQuery), + '/c/test/+/1234/5..10?revert&foo=bar'); + + params.messageHash = '#123'; + assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123'); + }); + + test('change with repo name encoding', () => { + const params = { + view: Gerrit.Nav.View.CHANGE, + changeNum: '1234', + project: 'x+/y+/z+/w', + }; + assert.equal(element._generateUrl(params), + '/c/x%252B/y%252B/z%252B/w/+/1234'); + }); + + test('diff', () => { + const params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + path: 'x+y/path.cpp', + patchNum: 12, + }; + assert.equal(element._generateUrl(params), + '/c/42/12/x%252By/path.cpp'); + + params.project = 'test'; + assert.equal(element._generateUrl(params), + '/c/test/+/42/12/x%252By/path.cpp'); + + params.basePatchNum = 6; + assert.equal(element._generateUrl(params), + '/c/test/+/42/6..12/x%252By/path.cpp'); + + params.path = 'foo bar/my+file.txt%'; + params.patchNum = 2; + delete params.basePatchNum; + assert.equal(element._generateUrl(params), + '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'); + + params.path = 'file.cpp'; + params.lineNum = 123; + assert.equal(element._generateUrl(params), + '/c/test/+/42/2/file.cpp#123'); + + params.leftSide = true; + assert.equal(element._generateUrl(params), + '/c/test/+/42/2/file.cpp#b123'); + }); + + test('diff with repo name encoding', () => { + const params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + path: 'x+y/path.cpp', + patchNum: 12, + project: 'x+/y', + }; + assert.equal(element._generateUrl(params), + '/c/x%252B/y/+/42/12/x%252By/path.cpp'); + }); + + test('edit', () => { + const params = { + view: Gerrit.Nav.View.EDIT, + changeNum: '42', + project: 'test', + path: 'x+y/path.cpp', + }; + assert.equal(element._generateUrl(params), + '/c/test/+/42/x%252By/path.cpp,edit'); + }); + + test('_getPatchRangeExpression', () => { + const params = {}; + let actual = element._getPatchRangeExpression(params); + assert.equal(actual, ''); + + params.patchNum = 4; + actual = element._getPatchRangeExpression(params); + assert.equal(actual, '4'); + + params.basePatchNum = 2; + actual = element._getPatchRangeExpression(params); + assert.equal(actual, '2..4'); + + delete params.patchNum; + actual = element._getPatchRangeExpression(params); + assert.equal(actual, '2..'); + }); + + suite('dashboard', () => { + test('self dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + }; + assert.equal(element._generateUrl(params), '/dashboard/self'); + }); + + test('user dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + user: 'user', + }; + assert.equal(element._generateUrl(params), '/dashboard/user'); + }); + + test('custom self dashboard, no title', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2'}, + ], + }; + assert.equal( + element._generateUrl(params), + '/dashboard/?section%201=query%201§ion%202=query%202'); + }); + + test('custom repo dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + sections: [ + {name: 'section 1', query: 'query 1 ${project}'}, + {name: 'section 2', query: 'query 2 ${repo}'}, + ], + repo: 'repo-name', + }; + assert.equal( + element._generateUrl(params), + '/dashboard/?section%201=query%201%20repo-name&' + + 'section%202=query%202%20repo-name'); + }); + + test('custom user dashboard, with title', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + user: 'user', + sections: [{name: 'name', query: 'query'}], + title: 'custom dashboard', + }; + assert.equal( + element._generateUrl(params), + '/dashboard/user?name=query&title=custom%20dashboard'); + }); + + test('repo dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + repo: 'gerrit/repo', + dashboard: 'default:main', + }; + assert.equal( + element._generateUrl(params), + '/p/gerrit/repo/+/dashboard/default:main'); + }); + + test('project dashboard (legacy)', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + project: 'gerrit/project', + dashboard: 'default:main', + }; + assert.equal( + element._generateUrl(params), + '/p/gerrit/project/+/dashboard/default:main'); + }); + }); + + suite('groups', () => { + test('group info', () => { + const params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + }; + assert.equal(element._generateUrl(params), '/admin/groups/1234'); + }); + + test('group members', () => { + const params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + detail: 'members', + }; + assert.equal(element._generateUrl(params), + '/admin/groups/1234,members'); + }); + + test('group audit log', () => { + const params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + detail: 'log', + }; + assert.equal(element._generateUrl(params), + '/admin/groups/1234,audit-log'); + }); + }); + }); + + suite('param normalization', () => { + let projectLookupStub; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + projectLookupStub = sandbox + .stub(element.$.restAPI, 'getFromProjectLookup'); + sandbox.stub(element, '_generateUrl'); }); - teardown(() => { sandbox.restore(); }); + suite('_normalizeLegacyRouteParams', () => { + let rangeStub; + let redirectStub; + let show404Stub; - test('_firstCodeBrowserWeblink', () => { - assert.deepEqual(element._firstCodeBrowserWeblink([ - {name: 'gitweb'}, - {name: 'gitiles'}, - {name: 'browse'}, - {name: 'test'}]), {name: 'gitiles'}); - - assert.deepEqual(element._firstCodeBrowserWeblink([ - {name: 'gitweb'}, - {name: 'test'}]), {name: 'gitweb'}); - }); - - test('_getBrowseCommitWeblink', () => { - const browserLink = {name: 'browser', url: 'browser/url'}; - const link = {name: 'test', url: 'test/url'}; - const weblinks = [browserLink, link]; - const config = {gerrit: {primary_weblink_name: browserLink.name}}; - sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link); - - assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config), - browserLink); - - assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link); - }); - - test('_getChangeWeblinks', () => { - const link = {name: 'test', url: 'test/url'}; - const browserLink = {name: 'browser', url: 'browser/url'}; - const mapLinksToConfig = weblinks => { return {options: {weblinks}}; }; - sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink); - - assert.deepEqual( - element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0], - {name: 'test', url: 'test/url'}); - - assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], - {name: 'test', url: 'test/url'}); - - link.url = 'https://' + link.url; - assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], - {name: 'test', url: 'https://test/url'}); - }); - - test('_getHashFromCanonicalPath', () => { - let url = '/foo/bar'; - let hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, ''); - - url = ''; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, ''); - - url = '/foo#bar'; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, 'bar'); - - url = '/foo#bar#baz'; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, 'bar#baz'); - - url = '#foo#bar#baz'; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, 'foo#bar#baz'); - }); - - suite('_parseLineAddress', () => { - test('returns null for empty and invalid hashes', () => { - let actual = element._parseLineAddress(''); - assert.isNull(actual); - - actual = element._parseLineAddress('foobar'); - assert.isNull(actual); - - actual = element._parseLineAddress('foo123'); - assert.isNull(actual); - - actual = element._parseLineAddress('123bar'); - assert.isNull(actual); + setup(() => { + rangeStub = sandbox.stub(element, '_normalizePatchRangeParams') + .returns(Promise.resolve()); + redirectStub = sandbox.stub(element, '_redirect'); + show404Stub = sandbox.stub(element, '_show404'); }); - test('parses correctly', () => { - let actual = element._parseLineAddress('1234'); - assert.isOk(actual); - assert.equal(actual.lineNum, 1234); - assert.isFalse(actual.leftSide); + test('w/o changeNum', () => { + projectLookupStub.returns(Promise.resolve('foo/bar')); + const params = {}; + return element._normalizeLegacyRouteParams(params).then(() => { + assert.isFalse(projectLookupStub.called); + assert.isFalse(rangeStub.called); + assert.isNotOk(params.project); + assert.isFalse(redirectStub.called); + assert.isFalse(show404Stub.called); + }); + }); - actual = element._parseLineAddress('a4'); - assert.isOk(actual); - assert.equal(actual.lineNum, 4); - assert.isTrue(actual.leftSide); + test('w/ changeNum', () => { + projectLookupStub.returns(Promise.resolve('foo/bar')); + const params = {changeNum: 1234}; + return element._normalizeLegacyRouteParams(params).then(() => { + assert.isTrue(projectLookupStub.called); + assert.isTrue(rangeStub.called); + assert.equal(params.project, 'foo/bar'); + assert.isTrue(redirectStub.calledOnce); + assert.isFalse(show404Stub.called); + }); + }); - actual = element._parseLineAddress('b77'); - assert.isOk(actual); - assert.equal(actual.lineNum, 77); - assert.isTrue(actual.leftSide); + test('halts on project lookup failure', () => { + projectLookupStub.returns(Promise.resolve(undefined)); + const params = {changeNum: 1234}; + return element._normalizeLegacyRouteParams(params).then(() => { + assert.isTrue(projectLookupStub.called); + assert.isFalse(rangeStub.called); + assert.isUndefined(params.project); + assert.isFalse(redirectStub.called); + assert.isTrue(show404Stub.calledOnce); + }); }); }); - test('_startRouter requires auth for the right handlers', () => { - // This test encodes the lists of route handler methods that gr-router - // automatically checks for authentication before triggering. + suite('_normalizePatchRangeParams', () => { + test('range n..n normalizes to n', () => { + const params = {basePatchNum: 4, patchNum: 4}; + const needsRedirect = element._normalizePatchRangeParams(params); + assert.isTrue(needsRedirect); + assert.isNotOk(params.basePatchNum); + assert.equal(params.patchNum, 4); + }); - const requiresAuth = {}; - const doesNotRequireAuth = {}; + test('range n.. normalizes to n', () => { + const params = {basePatchNum: 4}; + const needsRedirect = element._normalizePatchRangeParams(params); + assert.isFalse(needsRedirect); + assert.isNotOk(params.basePatchNum); + assert.equal(params.patchNum, 4); + }); + }); + }); + + suite('route handlers', () => { + let redirectStub; + let setParamsStub; + let handlePassThroughRoute; + + // Simple route handlers are direct mappings from parsed route data to a + // new set of app.params. This test helper asserts that passing `data` + // into `methodName` results in setting the params specified in `params`. + function assertDataToParams(data, methodName, params) { + element[methodName](data); + assert.deepEqual(setParamsStub.lastCall.args[0], params); + } + + setup(() => { + redirectStub = sandbox.stub(element, '_redirect'); + setParamsStub = sandbox.stub(element, '_setParams'); + handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute'); + }); + + test('_handleAgreementsRoute', () => { + const data = {params: {}}; + element._handleAgreementsRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements'); + }); + + test('_handleNewAgreementsRoute', () => { + element._handleNewAgreementsRoute({params: {}}); + assert.isTrue(setParamsStub.calledOnce); + assert.equal(setParamsStub.lastCall.args[0].view, + Gerrit.Nav.View.AGREEMENTS); + }); + + test('_handleSettingsLegacyRoute', () => { + const data = {params: {0: 'my-token'}}; + assertDataToParams(data, '_handleSettingsLegacyRoute', { + view: Gerrit.Nav.View.SETTINGS, + emailToken: 'my-token', + }); + }); + + test('_handleSettingsLegacyRoute with +', () => { + const data = {params: {0: 'my-token test'}}; + assertDataToParams(data, '_handleSettingsLegacyRoute', { + view: Gerrit.Nav.View.SETTINGS, + emailToken: 'my-token+test', + }); + }); + + test('_handleSettingsRoute', () => { + const data = {}; + assertDataToParams(data, '_handleSettingsRoute', { + view: Gerrit.Nav.View.SETTINGS, + }); + }); + + test('_handleDefaultRoute on first load', () => { + const appElementStub = {dispatchEvent: sinon.stub()}; + element._appElement = () => appElementStub; + element._handleDefaultRoute(); + assert.isTrue(appElementStub.dispatchEvent.calledOnce); + assert.equal( + appElementStub.dispatchEvent.lastCall.args[0].detail.response.status, + 404); + }); + + test('_handleDefaultRoute after internal navigation', () => { + let onExit = null; + const onRegisteringExit = (match, _onExit) => { + onExit = _onExit; + }; + sandbox.stub(window.page, 'exit', onRegisteringExit); sandbox.stub(Gerrit.Nav, 'setup'); sandbox.stub(window.page, 'start'); sandbox.stub(window.page, 'base'); sandbox.stub(window, 'page'); - sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => { - if (usesAuth) { - requiresAuth[methodName] = true; - } else { - doesNotRequireAuth[methodName] = true; - } - }); element._startRouter(); - const actualRequiresAuth = Object.keys(requiresAuth); - actualRequiresAuth.sort(); - const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth); - actualDoesNotRequireAuth.sort(); + const appElementStub = {dispatchEvent: sinon.stub()}; + element._appElement = () => appElementStub; + element._handleDefaultRoute(); - const shouldRequireAutoAuth = [ - '_handleAgreementsRoute', - '_handleChangeEditRoute', - '_handleCreateGroupRoute', - '_handleCreateProjectRoute', - '_handleDiffEditRoute', - '_handleGroupAuditLogRoute', - '_handleGroupInfoRoute', - '_handleGroupListFilterOffsetRoute', - '_handleGroupListFilterRoute', - '_handleGroupListOffsetRoute', - '_handleGroupMembersRoute', - '_handleGroupRoute', - '_handleGroupSelfRedirectRoute', - '_handleNewAgreementsRoute', - '_handlePluginListFilterOffsetRoute', - '_handlePluginListFilterRoute', - '_handlePluginListOffsetRoute', - '_handlePluginListRoute', - '_handleRepoCommandsRoute', - '_handleSettingsLegacyRoute', - '_handleSettingsRoute', - ]; - assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth); + onExit('', () => {}); // we left page; - const unauthenticatedHandlers = [ - '_handleBranchListFilterOffsetRoute', - '_handleBranchListFilterRoute', - '_handleBranchListOffsetRoute', - '_handleChangeNumberLegacyRoute', - '_handleChangeRoute', - '_handleDiffRoute', - '_handleDefaultRoute', - '_handleChangeLegacyRoute', - '_handleDiffLegacyRoute', - '_handleDocumentationRedirectRoute', - '_handleDocumentationSearchRoute', - '_handleDocumentationSearchRedirectRoute', - '_handleLegacyLinenum', - '_handleImproperlyEncodedPlusRoute', - '_handlePassThroughRoute', - '_handleProjectDashboardRoute', - '_handleProjectsOldRoute', - '_handleRepoAccessRoute', - '_handleRepoDashboardsRoute', - '_handleRepoListFilterOffsetRoute', - '_handleRepoListFilterRoute', - '_handleRepoListOffsetRoute', - '_handleRepoRoute', - '_handleQueryLegacySuffixRoute', - '_handleQueryRoute', - '_handleRegisterRoute', - '_handleTagListFilterOffsetRoute', - '_handleTagListFilterRoute', - '_handleTagListOffsetRoute', - '_handlePluginScreen', - ]; - - // Handler names that check authentication themselves, and thus don't need - // it performed for them. - const selfAuthenticatingHandlers = [ - '_handleDashboardRoute', - '_handleCustomDashboardRoute', - '_handleRootRoute', - ]; - - const shouldNotRequireAuth = unauthenticatedHandlers - .concat(selfAuthenticatingHandlers); - shouldNotRequireAuth.sort(); - assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth); + element._handleDefaultRoute(); + assert.isTrue(handlePassThroughRoute.calledOnce); }); - test('_redirectIfNotLoggedIn while logged in', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - const data = {canonicalPath: ''}; - const redirectStub = sandbox.stub(element, '_redirectToLogin'); - return element._redirectIfNotLoggedIn(data).then(() => { + test('_handleImproperlyEncodedPlusRoute', () => { + // Regression test for Issue 7100. + element._handleImproperlyEncodedPlusRoute( + {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); + assert.isTrue(redirectStub.calledOnce); + assert.equal( + redirectStub.lastCall.args[0], + '/c/test/+/42'); + + sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo'); + element._handleImproperlyEncodedPlusRoute( + {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); + assert.equal( + redirectStub.lastCall.args[0], + '/c/test/+/42#foo'); + }); + + test('_handleQueryRoute', () => { + const data = {params: ['project:foo/bar/baz']}; + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: undefined, + }); + + data.params.push(',123', '123'); + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: '123', + }); + }); + + test('_handleQueryLegacySuffixRoute', () => { + element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'}); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar'); + }); + + test('_handleQueryRoute', () => { + const data = {params: ['project:foo/bar/baz']}; + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: undefined, + }); + + data.params.push(',123', '123'); + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: '123', + }); + }); + + suite('_handleRegisterRoute', () => { + test('happy path', () => { + const ctx = {params: ['/foo/bar']}; + element._handleRegisterRoute(ctx); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar')); + assert.isTrue(setParamsStub.calledOnce); + assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); + }); + + test('no param', () => { + const ctx = {params: ['']}; + element._handleRegisterRoute(ctx); + assert.isTrue(redirectStub.calledWithExactly('/')); + assert.isTrue(setParamsStub.calledOnce); + assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); + }); + + test('prevent redirect', () => { + const ctx = {params: ['/register']}; + element._handleRegisterRoute(ctx); + assert.isTrue(redirectStub.calledWithExactly('/')); + assert.isTrue(setParamsStub.calledOnce); + assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); + }); + }); + + suite('_handleRootRoute', () => { + test('closes for closeAfterLogin', () => { + const data = {querystring: 'closeAfterLogin', canonicalPath: ''}; + const closeStub = sandbox.stub(window, 'close'); + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(closeStub.called); assert.isFalse(redirectStub.called); }); - }); - test('_redirectIfNotLoggedIn while logged out', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const redirectStub = sandbox.stub(element, '_redirectToLogin'); - const data = {canonicalPath: ''}; - return new Promise(resolve => { - element._redirectIfNotLoggedIn(data) - .then(() => { - assert.isTrue(false, 'Should never execute'); - }) - .catch(() => { - assert.isTrue(redirectStub.calledOnce); - resolve(); - }); - }); - }); - - suite('generateUrl', () => { - test('search', () => { - let params = { - view: Gerrit.Nav.View.SEARCH, - owner: 'a%b', - project: 'c%d', - branch: 'e%f', - topic: 'g%h', - statuses: ['op%en'], + test('redirects to dashboard if logged in', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + const data = { + canonicalPath: '/', path: '/', querystring: '', hash: '', }; - assert.equal(element._generateUrl(params), - '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + - 'topic:"g%2525h"+status:op%2525en'); + const result = element._handleRootRoute(data); + assert.isOk(result); + return result.then(() => { + assert.isTrue(redirectStub.calledWithExactly('/dashboard/self')); + }); + }); - params.offset = 100; - assert.equal(element._generateUrl(params), - '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + - 'topic:"g%2525h"+status:op%2525en,100'); - delete params.offset; - - // The presence of the query param overrides other params. - params.query = 'foo$bar'; - assert.equal(element._generateUrl(params), '/q/foo%2524bar'); - - params.offset = 100; - assert.equal(element._generateUrl(params), '/q/foo%2524bar,100'); - - params = { - view: Gerrit.Nav.View.SEARCH, - statuses: ['a', 'b', 'c'], + test('redirects to open changes if not logged in', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const data = { + canonicalPath: '/', path: '/', querystring: '', hash: '', }; - assert.equal(element._generateUrl(params), - '/q/(status:a OR status:b OR status:c)'); + const result = element._handleRootRoute(data); + assert.isOk(result); + return result.then(() => { + assert.isTrue(redirectStub.calledWithExactly('/q/status:open')); + }); }); - test('change', () => { - const params = { - view: Gerrit.Nav.View.CHANGE, - changeNum: '1234', - project: 'test', - }; - const paramsWithQuery = { - view: Gerrit.Nav.View.CHANGE, - changeNum: '1234', - project: 'test', - querystring: 'revert&foo=bar', - }; - - assert.equal(element._generateUrl(params), '/c/test/+/1234'); - assert.equal(element._generateUrl(paramsWithQuery), - '/c/test/+/1234?revert&foo=bar'); - - params.patchNum = 10; - assert.equal(element._generateUrl(params), '/c/test/+/1234/10'); - paramsWithQuery.patchNum = 10; - assert.equal(element._generateUrl(paramsWithQuery), - '/c/test/+/1234/10?revert&foo=bar'); - - params.basePatchNum = 5; - assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10'); - paramsWithQuery.basePatchNum = 5; - assert.equal(element._generateUrl(paramsWithQuery), - '/c/test/+/1234/5..10?revert&foo=bar'); - - params.messageHash = '#123'; - assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123'); - }); - - test('change with repo name encoding', () => { - const params = { - view: Gerrit.Nav.View.CHANGE, - changeNum: '1234', - project: 'x+/y+/z+/w', - }; - assert.equal(element._generateUrl(params), - '/c/x%252B/y%252B/z%252B/w/+/1234'); - }); - - test('diff', () => { - const params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - path: 'x+y/path.cpp', - patchNum: 12, - }; - assert.equal(element._generateUrl(params), - '/c/42/12/x%252By/path.cpp'); - - params.project = 'test'; - assert.equal(element._generateUrl(params), - '/c/test/+/42/12/x%252By/path.cpp'); - - params.basePatchNum = 6; - assert.equal(element._generateUrl(params), - '/c/test/+/42/6..12/x%252By/path.cpp'); - - params.path = 'foo bar/my+file.txt%'; - params.patchNum = 2; - delete params.basePatchNum; - assert.equal(element._generateUrl(params), - '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'); - - params.path = 'file.cpp'; - params.lineNum = 123; - assert.equal(element._generateUrl(params), - '/c/test/+/42/2/file.cpp#123'); - - params.leftSide = true; - assert.equal(element._generateUrl(params), - '/c/test/+/42/2/file.cpp#b123'); - }); - - test('diff with repo name encoding', () => { - const params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - path: 'x+y/path.cpp', - patchNum: 12, - project: 'x+/y', - }; - assert.equal(element._generateUrl(params), - '/c/x%252B/y/+/42/12/x%252By/path.cpp'); - }); - - test('edit', () => { - const params = { - view: Gerrit.Nav.View.EDIT, - changeNum: '42', - project: 'test', - path: 'x+y/path.cpp', - }; - assert.equal(element._generateUrl(params), - '/c/test/+/42/x%252By/path.cpp,edit'); - }); - - test('_getPatchRangeExpression', () => { - const params = {}; - let actual = element._getPatchRangeExpression(params); - assert.equal(actual, ''); - - params.patchNum = 4; - actual = element._getPatchRangeExpression(params); - assert.equal(actual, '4'); - - params.basePatchNum = 2; - actual = element._getPatchRangeExpression(params); - assert.equal(actual, '2..4'); - - delete params.patchNum; - actual = element._getPatchRangeExpression(params); - assert.equal(actual, '2..'); - }); - - suite('dashboard', () => { - test('self dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, + suite('GWT hash-path URLs', () => { + test('redirects hash-path URLs', () => { + const data = { + canonicalPath: '/#/foo/bar/baz', + hash: '/foo/bar/baz', + querystring: '', }; - assert.equal(element._generateUrl(params), '/dashboard/self'); - }); - - test('user dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - user: 'user', - }; - assert.equal(element._generateUrl(params), '/dashboard/user'); - }); - - test('custom self dashboard, no title', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2'}, - ], - }; - assert.equal( - element._generateUrl(params), - '/dashboard/?section%201=query%201§ion%202=query%202'); - }); - - test('custom repo dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - sections: [ - {name: 'section 1', query: 'query 1 ${project}'}, - {name: 'section 2', query: 'query 2 ${repo}'}, - ], - repo: 'repo-name', - }; - assert.equal( - element._generateUrl(params), - '/dashboard/?section%201=query%201%20repo-name&' + - 'section%202=query%202%20repo-name'); - }); - - test('custom user dashboard, with title', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - user: 'user', - sections: [{name: 'name', query: 'query'}], - title: 'custom dashboard', - }; - assert.equal( - element._generateUrl(params), - '/dashboard/user?name=query&title=custom%20dashboard'); - }); - - test('repo dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - repo: 'gerrit/repo', - dashboard: 'default:main', - }; - assert.equal( - element._generateUrl(params), - '/p/gerrit/repo/+/dashboard/default:main'); - }); - - test('project dashboard (legacy)', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - project: 'gerrit/project', - dashboard: 'default:main', - }; - assert.equal( - element._generateUrl(params), - '/p/gerrit/project/+/dashboard/default:main'); - }); - }); - - suite('groups', () => { - test('group info', () => { - const params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - }; - assert.equal(element._generateUrl(params), '/admin/groups/1234'); - }); - - test('group members', () => { - const params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - detail: 'members', - }; - assert.equal(element._generateUrl(params), - '/admin/groups/1234,members'); - }); - - test('group audit log', () => { - const params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - detail: 'log', - }; - assert.equal(element._generateUrl(params), - '/admin/groups/1234,audit-log'); - }); - }); - }); - - suite('param normalization', () => { - let projectLookupStub; - - setup(() => { - projectLookupStub = sandbox - .stub(element.$.restAPI, 'getFromProjectLookup'); - sandbox.stub(element, '_generateUrl'); - }); - - suite('_normalizeLegacyRouteParams', () => { - let rangeStub; - let redirectStub; - let show404Stub; - - setup(() => { - rangeStub = sandbox.stub(element, '_normalizePatchRangeParams') - .returns(Promise.resolve()); - redirectStub = sandbox.stub(element, '_redirect'); - show404Stub = sandbox.stub(element, '_show404'); - }); - - test('w/o changeNum', () => { - projectLookupStub.returns(Promise.resolve('foo/bar')); - const params = {}; - return element._normalizeLegacyRouteParams(params).then(() => { - assert.isFalse(projectLookupStub.called); - assert.isFalse(rangeStub.called); - assert.isNotOk(params.project); - assert.isFalse(redirectStub.called); - assert.isFalse(show404Stub.called); - }); - }); - - test('w/ changeNum', () => { - projectLookupStub.returns(Promise.resolve('foo/bar')); - const params = {changeNum: 1234}; - return element._normalizeLegacyRouteParams(params).then(() => { - assert.isTrue(projectLookupStub.called); - assert.isTrue(rangeStub.called); - assert.equal(params.project, 'foo/bar'); - assert.isTrue(redirectStub.calledOnce); - assert.isFalse(show404Stub.called); - }); - }); - - test('halts on project lookup failure', () => { - projectLookupStub.returns(Promise.resolve(undefined)); - const params = {changeNum: 1234}; - return element._normalizeLegacyRouteParams(params).then(() => { - assert.isTrue(projectLookupStub.called); - assert.isFalse(rangeStub.called); - assert.isUndefined(params.project); - assert.isFalse(redirectStub.called); - assert.isTrue(show404Stub.calledOnce); - }); - }); - }); - - suite('_normalizePatchRangeParams', () => { - test('range n..n normalizes to n', () => { - const params = {basePatchNum: 4, patchNum: 4}; - const needsRedirect = element._normalizePatchRangeParams(params); - assert.isTrue(needsRedirect); - assert.isNotOk(params.basePatchNum); - assert.equal(params.patchNum, 4); - }); - - test('range n.. normalizes to n', () => { - const params = {basePatchNum: 4}; - const needsRedirect = element._normalizePatchRangeParams(params); - assert.isFalse(needsRedirect); - assert.isNotOk(params.basePatchNum); - assert.equal(params.patchNum, 4); - }); - }); - }); - - suite('route handlers', () => { - let redirectStub; - let setParamsStub; - let handlePassThroughRoute; - - // Simple route handlers are direct mappings from parsed route data to a - // new set of app.params. This test helper asserts that passing `data` - // into `methodName` results in setting the params specified in `params`. - function assertDataToParams(data, methodName, params) { - element[methodName](data); - assert.deepEqual(setParamsStub.lastCall.args[0], params); - } - - setup(() => { - redirectStub = sandbox.stub(element, '_redirect'); - setParamsStub = sandbox.stub(element, '_setParams'); - handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute'); - }); - - test('_handleAgreementsRoute', () => { - const data = {params: {}}; - element._handleAgreementsRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements'); - }); - - test('_handleNewAgreementsRoute', () => { - element._handleNewAgreementsRoute({params: {}}); - assert.isTrue(setParamsStub.calledOnce); - assert.equal(setParamsStub.lastCall.args[0].view, - Gerrit.Nav.View.AGREEMENTS); - }); - - test('_handleSettingsLegacyRoute', () => { - const data = {params: {0: 'my-token'}}; - assertDataToParams(data, '_handleSettingsLegacyRoute', { - view: Gerrit.Nav.View.SETTINGS, - emailToken: 'my-token', - }); - }); - - test('_handleSettingsLegacyRoute with +', () => { - const data = {params: {0: 'my-token test'}}; - assertDataToParams(data, '_handleSettingsLegacyRoute', { - view: Gerrit.Nav.View.SETTINGS, - emailToken: 'my-token+test', - }); - }); - - test('_handleSettingsRoute', () => { - const data = {}; - assertDataToParams(data, '_handleSettingsRoute', { - view: Gerrit.Nav.View.SETTINGS, - }); - }); - - test('_handleDefaultRoute on first load', () => { - const appElementStub = {dispatchEvent: sinon.stub()}; - element._appElement = () => appElementStub; - element._handleDefaultRoute(); - assert.isTrue(appElementStub.dispatchEvent.calledOnce); - assert.equal( - appElementStub.dispatchEvent.lastCall.args[0].detail.response.status, - 404); - }); - - test('_handleDefaultRoute after internal navigation', () => { - let onExit = null; - const onRegisteringExit = (match, _onExit) => { - onExit = _onExit; - }; - sandbox.stub(window.page, 'exit', onRegisteringExit); - sandbox.stub(Gerrit.Nav, 'setup'); - sandbox.stub(window.page, 'start'); - sandbox.stub(window.page, 'base'); - sandbox.stub(window, 'page'); - element._startRouter(); - - const appElementStub = {dispatchEvent: sinon.stub()}; - element._appElement = () => appElementStub; - element._handleDefaultRoute(); - - onExit('', () => {}); // we left page; - - element._handleDefaultRoute(); - assert.isTrue(handlePassThroughRoute.calledOnce); - }); - - test('_handleImproperlyEncodedPlusRoute', () => { - // Regression test for Issue 7100. - element._handleImproperlyEncodedPlusRoute( - {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); - assert.isTrue(redirectStub.calledOnce); - assert.equal( - redirectStub.lastCall.args[0], - '/c/test/+/42'); - - sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo'); - element._handleImproperlyEncodedPlusRoute( - {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); - assert.equal( - redirectStub.lastCall.args[0], - '/c/test/+/42#foo'); - }); - - test('_handleQueryRoute', () => { - const data = {params: ['project:foo/bar/baz']}; - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: undefined, - }); - - data.params.push(',123', '123'); - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: '123', - }); - }); - - test('_handleQueryLegacySuffixRoute', () => { - element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'}); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar'); - }); - - test('_handleQueryRoute', () => { - const data = {params: ['project:foo/bar/baz']}; - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: undefined, - }); - - data.params.push(',123', '123'); - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: '123', - }); - }); - - suite('_handleRegisterRoute', () => { - test('happy path', () => { - const ctx = {params: ['/foo/bar']}; - element._handleRegisterRoute(ctx); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar')); - assert.isTrue(setParamsStub.calledOnce); - assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); - }); - - test('no param', () => { - const ctx = {params: ['']}; - element._handleRegisterRoute(ctx); - assert.isTrue(redirectStub.calledWithExactly('/')); - assert.isTrue(setParamsStub.calledOnce); - assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); - }); - - test('prevent redirect', () => { - const ctx = {params: ['/register']}; - element._handleRegisterRoute(ctx); - assert.isTrue(redirectStub.calledWithExactly('/')); - assert.isTrue(setParamsStub.calledOnce); - assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); - }); - }); - - suite('_handleRootRoute', () => { - test('closes for closeAfterLogin', () => { - const data = {querystring: 'closeAfterLogin', canonicalPath: ''}; - const closeStub = sandbox.stub(window, 'close'); const result = element._handleRootRoute(data); assert.isNotOk(result); - assert.isTrue(closeStub.called); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); + }); + + test('redirects hash-path URLs w/o leading slash', () => { + const data = { + canonicalPath: '/#foo/bar/baz', + querystring: '', + hash: 'foo/bar/baz', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); + }); + + test('normalizes "/ /" in hash to "/+/"', () => { + const data = { + canonicalPath: '/#/foo/bar/+/123/4', + querystring: '', + hash: '/foo/bar/ /123/4', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4')); + }); + + test('prepends baseurl to hash-path', () => { + const data = { + canonicalPath: '/#/foo/bar', + querystring: '', + hash: '/foo/bar', + }; + sandbox.stub(element, 'getBaseUrl').returns('/baz'); + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar')); + }); + + test('normalizes /VE/ settings hash-paths', () => { + const data = { + canonicalPath: '/#/VE/foo/bar', + querystring: '', + hash: '/VE/foo/bar', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly( + '/settings/VE/foo/bar')); + }); + + test('does not drop "inner hashes"', () => { + const data = { + canonicalPath: '/#/foo/bar#baz', + querystring: '', + hash: '/foo/bar', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz')); + }); + }); + }); + + suite('_handleDashboardRoute', () => { + let redirectToLoginStub; + + setup(() => { + redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); + }); + + test('own dashboard but signed out redirects to login', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}}; + return element._handleDashboardRoute(data, '').then(() => { + assert.isTrue(redirectToLoginStub.calledOnce); assert.isFalse(redirectStub.called); - }); - - test('redirects to dashboard if logged in', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - const data = { - canonicalPath: '/', path: '/', querystring: '', hash: '', - }; - const result = element._handleRootRoute(data); - assert.isOk(result); - return result.then(() => { - assert.isTrue(redirectStub.calledWithExactly('/dashboard/self')); - }); - }); - - test('redirects to open changes if not logged in', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const data = { - canonicalPath: '/', path: '/', querystring: '', hash: '', - }; - const result = element._handleRootRoute(data); - assert.isOk(result); - return result.then(() => { - assert.isTrue(redirectStub.calledWithExactly('/q/status:open')); - }); - }); - - suite('GWT hash-path URLs', () => { - test('redirects hash-path URLs', () => { - const data = { - canonicalPath: '/#/foo/bar/baz', - hash: '/foo/bar/baz', - querystring: '', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); - }); - - test('redirects hash-path URLs w/o leading slash', () => { - const data = { - canonicalPath: '/#foo/bar/baz', - querystring: '', - hash: 'foo/bar/baz', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); - }); - - test('normalizes "/ /" in hash to "/+/"', () => { - const data = { - canonicalPath: '/#/foo/bar/+/123/4', - querystring: '', - hash: '/foo/bar/ /123/4', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4')); - }); - - test('prepends baseurl to hash-path', () => { - const data = { - canonicalPath: '/#/foo/bar', - querystring: '', - hash: '/foo/bar', - }; - sandbox.stub(element, 'getBaseUrl').returns('/baz'); - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar')); - }); - - test('normalizes /VE/ settings hash-paths', () => { - const data = { - canonicalPath: '/#/VE/foo/bar', - querystring: '', - hash: '/VE/foo/bar', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly( - '/settings/VE/foo/bar')); - }); - - test('does not drop "inner hashes"', () => { - const data = { - canonicalPath: '/#/foo/bar#baz', - querystring: '', - hash: '/foo/bar', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz')); - }); + assert.isFalse(setParamsStub.called); }); }); - suite('_handleDashboardRoute', () => { - let redirectToLoginStub; - - setup(() => { - redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); - }); - - test('own dashboard but signed out redirects to login', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}}; - return element._handleDashboardRoute(data, '').then(() => { - assert.isTrue(redirectToLoginStub.calledOnce); - assert.isFalse(redirectStub.called); - assert.isFalse(setParamsStub.called); - }); - }); - - test('non-self dashboard but signed out does not redirect', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; - return element._handleDashboardRoute(data, '').then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo'); - }); - }); - - test('dashboard while signed in sets params', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; - return element._handleDashboardRoute(data, '').then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'foo', - }); - }); - }); - }); - - suite('_handleCustomDashboardRoute', () => { - let redirectToLoginStub; - - setup(() => { - redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); - }); - - test('no user specified', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, '').then(() => { - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.called); - assert.equal(redirectStub.lastCall.args[0], '/dashboard/self'); - }); - }); - - test('custom dashboard without title', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, '?a=b&c&d=e') - .then(() => { - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections: [ - {name: 'a', query: 'b'}, - {name: 'd', query: 'e'}, - ], - title: 'Custom Dashboard', - }); - }); - }); - - test('custom dashboard with title', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, - '?a=b&c&d=&=e&title=t') - .then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections: [ - {name: 'a', query: 'b'}, - ], - title: 't', - }); - }); - }); - - test('custom dashboard with foreach', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, - '?a=b&c&d=&=e&foreach=is:open') - .then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections: [ - {name: 'a', query: 'is:open b'}, - ], - title: 'Custom Dashboard', - }); - }); - }); - }); - - suite('group routes', () => { - test('_handleGroupInfoRoute', () => { - const data = {params: {0: 1234}}; - element._handleGroupInfoRoute(data); + test('non-self dashboard but signed out does not redirect', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; + return element._handleDashboardRoute(data, '').then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(setParamsStub.called); assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234'); + assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo'); + }); + }); + + test('dashboard while signed in sets params', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; + return element._handleDashboardRoute(data, '').then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'foo', + }); + }); + }); + }); + + suite('_handleCustomDashboardRoute', () => { + let redirectToLoginStub; + + setup(() => { + redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); + }); + + test('no user specified', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, '').then(() => { + assert.isFalse(setParamsStub.called); + assert.isTrue(redirectStub.called); + assert.equal(redirectStub.lastCall.args[0], '/dashboard/self'); + }); + }); + + test('custom dashboard without title', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, '?a=b&c&d=e') + .then(() => { + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections: [ + {name: 'a', query: 'b'}, + {name: 'd', query: 'e'}, + ], + title: 'Custom Dashboard', + }); + }); + }); + + test('custom dashboard with title', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, + '?a=b&c&d=&=e&title=t') + .then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections: [ + {name: 'a', query: 'b'}, + ], + title: 't', + }); + }); + }); + + test('custom dashboard with foreach', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, + '?a=b&c&d=&=e&foreach=is:open') + .then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections: [ + {name: 'a', query: 'is:open b'}, + ], + title: 'Custom Dashboard', + }); + }); + }); + }); + + suite('group routes', () => { + test('_handleGroupInfoRoute', () => { + const data = {params: {0: 1234}}; + element._handleGroupInfoRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234'); + }); + + test('_handleGroupAuditLogRoute', () => { + const data = {params: {0: 1234}}; + assertDataToParams(data, '_handleGroupAuditLogRoute', { + view: Gerrit.Nav.View.GROUP, + detail: 'log', + groupId: 1234, + }); + }); + + test('_handleGroupMembersRoute', () => { + const data = {params: {0: 1234}}; + assertDataToParams(data, '_handleGroupMembersRoute', { + view: Gerrit.Nav.View.GROUP, + detail: 'members', + groupId: 1234, + }); + }); + + test('_handleGroupListOffsetRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handleGroupListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 0, + filter: null, + openCreateModal: false, }); - test('_handleGroupAuditLogRoute', () => { - const data = {params: {0: 1234}}; - assertDataToParams(data, '_handleGroupAuditLogRoute', { - view: Gerrit.Nav.View.GROUP, - detail: 'log', - groupId: 1234, + data.params[1] = 42; + assertDataToParams(data, '_handleGroupListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 42, + filter: null, + openCreateModal: false, + }); + + data.hash = 'create'; + assertDataToParams(data, '_handleGroupListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 42, + filter: null, + openCreateModal: true, + }); + }); + + test('_handleGroupListFilterOffsetRoute', () => { + const data = {params: {filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handleGroupListFilterOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 42, + filter: 'foo', + }); + }); + + test('_handleGroupListFilterRoute', () => { + const data = {params: {filter: 'foo'}}; + assertDataToParams(data, '_handleGroupListFilterRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + filter: 'foo', + }); + }); + + test('_handleGroupRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleGroupRoute', { + view: Gerrit.Nav.View.GROUP, + groupId: 4321, + }); + }); + }); + + suite('repo routes', () => { + test('_handleProjectsOldRoute', () => { + const data = {params: {}}; + element._handleProjectsOldRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/admin/repos/'); + }); + + test('_handleProjectsOldRoute test', () => { + const data = {params: {1: 'test'}}; + element._handleProjectsOldRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test'); + }); + + test('_handleProjectsOldRoute test,branches', () => { + const data = {params: {1: 'test,branches'}}; + element._handleProjectsOldRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal( + redirectStub.lastCall.args[0], '/admin/repos/test,branches'); + }); + + test('_handleRepoRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleRepoRoute', { + view: Gerrit.Nav.View.REPO, + repo: 4321, + }); + }); + + test('_handleRepoCommandsRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleRepoCommandsRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.COMMANDS, + repo: 4321, + }); + }); + + test('_handleRepoAccessRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleRepoAccessRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + repo: 4321, + }); + }); + + suite('branch list routes', () => { + test('_handleBranchListOffsetRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleBranchListOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + offset: 0, + filter: null, + }); + + data.params[2] = 42; + assertDataToParams(data, '_handleBranchListOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + offset: 42, + filter: null, }); }); - test('_handleGroupMembersRoute', () => { - const data = {params: {0: 1234}}; - assertDataToParams(data, '_handleGroupMembersRoute', { - view: Gerrit.Nav.View.GROUP, - detail: 'members', - groupId: 1234, + test('_handleBranchListFilterOffsetRoute', () => { + const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handleBranchListFilterOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + offset: 42, + filter: 'foo', }); }); - test('_handleGroupListOffsetRoute', () => { + test('_handleBranchListFilterRoute', () => { + const data = {params: {repo: 4321, filter: 'foo'}}; + assertDataToParams(data, '_handleBranchListFilterRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + filter: 'foo', + }); + }); + }); + + suite('tag list routes', () => { + test('_handleTagListOffsetRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleTagListOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + offset: 0, + filter: null, + }); + }); + + test('_handleTagListFilterOffsetRoute', () => { + const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handleTagListFilterOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + offset: 42, + filter: 'foo', + }); + }); + + test('_handleTagListFilterRoute', () => { + const data = {params: {repo: 4321}}; + assertDataToParams(data, '_handleTagListFilterRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + filter: null, + }); + + data.params.filter = 'foo'; + assertDataToParams(data, '_handleTagListFilterRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + filter: 'foo', + }); + }); + }); + + suite('repo list routes', () => { + test('_handleRepoListOffsetRoute', () => { const data = {params: {}}; - assertDataToParams(data, '_handleGroupListOffsetRoute', { + assertDataToParams(data, '_handleRepoListOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 0, filter: null, openCreateModal: false, }); data.params[1] = 42; - assertDataToParams(data, '_handleGroupListOffsetRoute', { + assertDataToParams(data, '_handleRepoListOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 42, filter: null, openCreateModal: false, }); data.hash = 'create'; - assertDataToParams(data, '_handleGroupListOffsetRoute', { + assertDataToParams(data, '_handleRepoListOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 42, filter: null, openCreateModal: true, }); }); - test('_handleGroupListFilterOffsetRoute', () => { + test('_handleRepoListFilterOffsetRoute', () => { const data = {params: {filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleGroupListFilterOffsetRoute', { + assertDataToParams(data, '_handleRepoListFilterOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 42, filter: 'foo', }); }); - test('_handleGroupListFilterRoute', () => { - const data = {params: {filter: 'foo'}}; - assertDataToParams(data, '_handleGroupListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - filter: 'foo', - }); - }); - - test('_handleGroupRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleGroupRoute', { - view: Gerrit.Nav.View.GROUP, - groupId: 4321, - }); - }); - }); - - suite('repo routes', () => { - test('_handleProjectsOldRoute', () => { + test('_handleRepoListFilterRoute', () => { const data = {params: {}}; - element._handleProjectsOldRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/admin/repos/'); - }); - - test('_handleProjectsOldRoute test', () => { - const data = {params: {1: 'test'}}; - element._handleProjectsOldRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test'); - }); - - test('_handleProjectsOldRoute test,branches', () => { - const data = {params: {1: 'test,branches'}}; - element._handleProjectsOldRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal( - redirectStub.lastCall.args[0], '/admin/repos/test,branches'); - }); - - test('_handleRepoRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleRepoRoute', { - view: Gerrit.Nav.View.REPO, - repo: 4321, - }); - }); - - test('_handleRepoCommandsRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleRepoCommandsRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.COMMANDS, - repo: 4321, - }); - }); - - test('_handleRepoAccessRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleRepoAccessRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - repo: 4321, - }); - }); - - suite('branch list routes', () => { - test('_handleBranchListOffsetRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleBranchListOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - offset: 0, - filter: null, - }); - - data.params[2] = 42; - assertDataToParams(data, '_handleBranchListOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - offset: 42, - filter: null, - }); - }); - - test('_handleBranchListFilterOffsetRoute', () => { - const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleBranchListFilterOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - offset: 42, - filter: 'foo', - }); - }); - - test('_handleBranchListFilterRoute', () => { - const data = {params: {repo: 4321, filter: 'foo'}}; - assertDataToParams(data, '_handleBranchListFilterRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - filter: 'foo', - }); - }); - }); - - suite('tag list routes', () => { - test('_handleTagListOffsetRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleTagListOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - offset: 0, - filter: null, - }); - }); - - test('_handleTagListFilterOffsetRoute', () => { - const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleTagListFilterOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - offset: 42, - filter: 'foo', - }); - }); - - test('_handleTagListFilterRoute', () => { - const data = {params: {repo: 4321}}; - assertDataToParams(data, '_handleTagListFilterRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - filter: null, - }); - - data.params.filter = 'foo'; - assertDataToParams(data, '_handleTagListFilterRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - filter: 'foo', - }); - }); - }); - - suite('repo list routes', () => { - test('_handleRepoListOffsetRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handleRepoListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 0, - filter: null, - openCreateModal: false, - }); - - data.params[1] = 42; - assertDataToParams(data, '_handleRepoListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 42, - filter: null, - openCreateModal: false, - }); - - data.hash = 'create'; - assertDataToParams(data, '_handleRepoListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 42, - filter: null, - openCreateModal: true, - }); - }); - - test('_handleRepoListFilterOffsetRoute', () => { - const data = {params: {filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleRepoListFilterOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 42, - filter: 'foo', - }); - }); - - test('_handleRepoListFilterRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handleRepoListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - filter: null, - }); - - data.params.filter = 'foo'; - assertDataToParams(data, '_handleRepoListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - filter: 'foo', - }); - }); - }); - }); - - suite('plugin routes', () => { - test('_handlePluginListOffsetRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handlePluginListOffsetRoute', { + assertDataToParams(data, '_handleRepoListFilterRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: 0, - filter: null, - }); - - data.params[1] = 42; - assertDataToParams(data, '_handlePluginListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: 42, - filter: null, - }); - }); - - test('_handlePluginListFilterOffsetRoute', () => { - const data = {params: {filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handlePluginListFilterOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: 42, - filter: 'foo', - }); - }); - - test('_handlePluginListFilterRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handlePluginListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', + adminView: 'gr-repo-list', filter: null, }); data.params.filter = 'foo'; - assertDataToParams(data, '_handlePluginListFilterRoute', { + assertDataToParams(data, '_handleRepoListFilterRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', + adminView: 'gr-repo-list', filter: 'foo', }); }); - - test('_handlePluginListRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handlePluginListRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - }); - }); - }); - - suite('change/diff routes', () => { - test('_handleChangeNumberLegacyRoute', () => { - const data = {params: {0: 12345}}; - element._handleChangeNumberLegacyRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly('/c/12345')); - }); - - test('_handleChangeLegacyRoute', () => { - const normalizeRouteStub = sandbox.stub(element, - '_normalizeLegacyRouteParams'); - const ctx = { - params: [ - 1234, // 0 Change number - null, // 1 Unused - null, // 2 Unused - 6, // 3 Base patch number - null, // 4 Unused - 9, // 5 Patch number - ], - querystring: '', - }; - element._handleChangeLegacyRoute(ctx); - assert.isTrue(normalizeRouteStub.calledOnce); - assert.deepEqual(normalizeRouteStub.lastCall.args[0], { - changeNum: 1234, - basePatchNum: 6, - patchNum: 9, - view: Gerrit.Nav.View.CHANGE, - querystring: '', - }); - }); - - test('_handleDiffLegacyRoute', () => { - const normalizeRouteStub = sandbox.stub(element, - '_normalizeLegacyRouteParams'); - const ctx = { - params: [ - 1234, // 0 Change number - null, // 1 Unused - 3, // 2 Base patch number - null, // 3 Unused - 8, // 4 Patch number - 'foo/bar', // 5 Diff path - ], - path: '/c/1234/3..8/foo/bar', - hash: 'b123', - }; - element._handleDiffLegacyRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRouteStub.calledOnce); - assert.deepEqual(normalizeRouteStub.lastCall.args[0], { - changeNum: 1234, - basePatchNum: 3, - patchNum: 8, - view: Gerrit.Nav.View.DIFF, - path: 'foo/bar', - lineNum: 123, - leftSide: true, - }); - }); - - test('_handleLegacyLinenum w/ @321', () => { - const ctx = {path: '/c/1234/3..8/foo/bar@321'}; - element._handleLegacyLinenum(ctx); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly( - '/c/1234/3..8/foo/bar#321')); - }); - - test('_handleLegacyLinenum w/ @b123', () => { - const ctx = {path: '/c/1234/3..8/foo/bar@b123'}; - element._handleLegacyLinenum(ctx); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly( - '/c/1234/3..8/foo/bar#b123')); - }); - - suite('_handleChangeRoute', () => { - let normalizeRangeStub; - - function makeParams(path, hash) { - return { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - null, // 2 Unused - null, // 3 Unused - 4, // 4 Base patch number - null, // 5 Unused - 7, // 6 Patch number - ], - }; - } - - setup(() => { - normalizeRangeStub = sandbox.stub(element, - '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - }); - - test('needs redirect', () => { - normalizeRangeStub.returns(true); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams(null, ''); - element._handleChangeRoute(ctx); - assert.isTrue(normalizeRangeStub.called); - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly('foo')); - }); - - test('change view', () => { - normalizeRangeStub.returns(false); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams(null, ''); - assertDataToParams(ctx, '_handleChangeRoute', { - view: Gerrit.Nav.View.CHANGE, - project: 'foo/bar', - changeNum: 1234, - basePatchNum: 4, - patchNum: 7, - }); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeStub.called); - }); - }); - - suite('_handleDiffRoute', () => { - let normalizeRangeStub; - - function makeParams(path, hash) { - return { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - null, // 2 Unused - null, // 3 Unused - 4, // 4 Base patch number - null, // 5 Unused - 7, // 6 Patch number - null, // 7 Unused, - path, // 8 Diff path - ], - hash, - }; - } - - setup(() => { - normalizeRangeStub = sandbox.stub(element, - '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - }); - - test('needs redirect', () => { - normalizeRangeStub.returns(true); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams(null, ''); - element._handleDiffRoute(ctx); - assert.isTrue(normalizeRangeStub.called); - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly('foo')); - }); - - test('diff view', () => { - normalizeRangeStub.returns(false); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams('foo/bar/baz', 'b44'); - assertDataToParams(ctx, '_handleDiffRoute', { - view: Gerrit.Nav.View.DIFF, - project: 'foo/bar', - changeNum: 1234, - basePatchNum: 4, - patchNum: 7, - path: 'foo/bar/baz', - leftSide: true, - lineNum: 44, - }); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeStub.called); - }); - }); - - test('_handleDiffEditRoute', () => { - const normalizeRangeSpy = - sandbox.spy(element, '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - const ctx = { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - 3, // 2 Patch num - 'foo/bar/baz', // 3 File path - ], - }; - const appParams = { - project: 'foo/bar', - changeNum: 1234, - view: Gerrit.Nav.View.EDIT, - path: 'foo/bar/baz', - patchNum: 3, - lineNum: undefined, - }; - - element._handleDiffEditRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeSpy.calledOnce); - assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); - assert.isFalse(normalizeRangeSpy.lastCall.returnValue); - assert.deepEqual(setParamsStub.lastCall.args[0], appParams); - }); - - test('_handleDiffEditRoute with lineNum', () => { - const normalizeRangeSpy = - sandbox.spy(element, '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - const ctx = { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - 3, // 2 Patch num - 'foo/bar/baz', // 3 File path - ], - hash: 4, - }; - const appParams = { - project: 'foo/bar', - changeNum: 1234, - view: Gerrit.Nav.View.EDIT, - path: 'foo/bar/baz', - patchNum: 3, - lineNum: 4, - }; - - element._handleDiffEditRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeSpy.calledOnce); - assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); - assert.isFalse(normalizeRangeSpy.lastCall.returnValue); - assert.deepEqual(setParamsStub.lastCall.args[0], appParams); - }); - - test('_handleChangeEditRoute', () => { - const normalizeRangeSpy = - sandbox.spy(element, '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - const ctx = { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - null, - 3, // 3 Patch num - ], - }; - const appParams = { - project: 'foo/bar', - changeNum: 1234, - view: Gerrit.Nav.View.CHANGE, - patchNum: 3, - edit: true, - }; - - element._handleChangeEditRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeSpy.calledOnce); - assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); - assert.isFalse(normalizeRangeSpy.lastCall.returnValue); - assert.deepEqual(setParamsStub.lastCall.args[0], appParams); - }); - }); - - test('_handlePluginScreen', () => { - const ctx = {params: ['foo', 'bar']}; - assertDataToParams(ctx, '_handlePluginScreen', { - view: Gerrit.Nav.View.PLUGIN_SCREEN, - plugin: 'foo', - screen: 'bar', - }); - assert.isFalse(redirectStub.called); }); }); - suite('_parseQueryString', () => { - test('empty queries', () => { - assert.deepEqual(element._parseQueryString(''), []); - assert.deepEqual(element._parseQueryString('?'), []); - assert.deepEqual(element._parseQueryString('??'), []); - assert.deepEqual(element._parseQueryString('&&&'), []); + suite('plugin routes', () => { + test('_handlePluginListOffsetRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handlePluginListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: 0, + filter: null, + }); + + data.params[1] = 42; + assertDataToParams(data, '_handlePluginListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: 42, + filter: null, + }); }); - test('url decoding', () => { - assert.deepEqual(element._parseQueryString('+'), [[' ', '']]); - assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]); - assert.deepEqual( - element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'), - [['name', 'value']]); + test('_handlePluginListFilterOffsetRoute', () => { + const data = {params: {filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handlePluginListFilterOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: 42, + filter: 'foo', + }); }); - test('multiple parameters', () => { - assert.deepEqual( - element._parseQueryString('a=b&c=d&e=f'), - [['a', 'b'], ['c', 'd'], ['e', 'f']]); - assert.deepEqual( - element._parseQueryString('&a=b&&&e=f&'), - [['a', 'b'], ['e', 'f']]); + test('_handlePluginListFilterRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handlePluginListFilterRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + filter: null, + }); + + data.params.filter = 'foo'; + assertDataToParams(data, '_handlePluginListFilterRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + filter: 'foo', + }); }); + + test('_handlePluginListRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handlePluginListRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + }); + }); + }); + + suite('change/diff routes', () => { + test('_handleChangeNumberLegacyRoute', () => { + const data = {params: {0: 12345}}; + element._handleChangeNumberLegacyRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly('/c/12345')); + }); + + test('_handleChangeLegacyRoute', () => { + const normalizeRouteStub = sandbox.stub(element, + '_normalizeLegacyRouteParams'); + const ctx = { + params: [ + 1234, // 0 Change number + null, // 1 Unused + null, // 2 Unused + 6, // 3 Base patch number + null, // 4 Unused + 9, // 5 Patch number + ], + querystring: '', + }; + element._handleChangeLegacyRoute(ctx); + assert.isTrue(normalizeRouteStub.calledOnce); + assert.deepEqual(normalizeRouteStub.lastCall.args[0], { + changeNum: 1234, + basePatchNum: 6, + patchNum: 9, + view: Gerrit.Nav.View.CHANGE, + querystring: '', + }); + }); + + test('_handleDiffLegacyRoute', () => { + const normalizeRouteStub = sandbox.stub(element, + '_normalizeLegacyRouteParams'); + const ctx = { + params: [ + 1234, // 0 Change number + null, // 1 Unused + 3, // 2 Base patch number + null, // 3 Unused + 8, // 4 Patch number + 'foo/bar', // 5 Diff path + ], + path: '/c/1234/3..8/foo/bar', + hash: 'b123', + }; + element._handleDiffLegacyRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRouteStub.calledOnce); + assert.deepEqual(normalizeRouteStub.lastCall.args[0], { + changeNum: 1234, + basePatchNum: 3, + patchNum: 8, + view: Gerrit.Nav.View.DIFF, + path: 'foo/bar', + lineNum: 123, + leftSide: true, + }); + }); + + test('_handleLegacyLinenum w/ @321', () => { + const ctx = {path: '/c/1234/3..8/foo/bar@321'}; + element._handleLegacyLinenum(ctx); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly( + '/c/1234/3..8/foo/bar#321')); + }); + + test('_handleLegacyLinenum w/ @b123', () => { + const ctx = {path: '/c/1234/3..8/foo/bar@b123'}; + element._handleLegacyLinenum(ctx); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly( + '/c/1234/3..8/foo/bar#b123')); + }); + + suite('_handleChangeRoute', () => { + let normalizeRangeStub; + + function makeParams(path, hash) { + return { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + null, // 2 Unused + null, // 3 Unused + 4, // 4 Base patch number + null, // 5 Unused + 7, // 6 Patch number + ], + }; + } + + setup(() => { + normalizeRangeStub = sandbox.stub(element, + '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + }); + + test('needs redirect', () => { + normalizeRangeStub.returns(true); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams(null, ''); + element._handleChangeRoute(ctx); + assert.isTrue(normalizeRangeStub.called); + assert.isFalse(setParamsStub.called); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly('foo')); + }); + + test('change view', () => { + normalizeRangeStub.returns(false); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams(null, ''); + assertDataToParams(ctx, '_handleChangeRoute', { + view: Gerrit.Nav.View.CHANGE, + project: 'foo/bar', + changeNum: 1234, + basePatchNum: 4, + patchNum: 7, + }); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeStub.called); + }); + }); + + suite('_handleDiffRoute', () => { + let normalizeRangeStub; + + function makeParams(path, hash) { + return { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + null, // 2 Unused + null, // 3 Unused + 4, // 4 Base patch number + null, // 5 Unused + 7, // 6 Patch number + null, // 7 Unused, + path, // 8 Diff path + ], + hash, + }; + } + + setup(() => { + normalizeRangeStub = sandbox.stub(element, + '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + }); + + test('needs redirect', () => { + normalizeRangeStub.returns(true); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams(null, ''); + element._handleDiffRoute(ctx); + assert.isTrue(normalizeRangeStub.called); + assert.isFalse(setParamsStub.called); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly('foo')); + }); + + test('diff view', () => { + normalizeRangeStub.returns(false); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams('foo/bar/baz', 'b44'); + assertDataToParams(ctx, '_handleDiffRoute', { + view: Gerrit.Nav.View.DIFF, + project: 'foo/bar', + changeNum: 1234, + basePatchNum: 4, + patchNum: 7, + path: 'foo/bar/baz', + leftSide: true, + lineNum: 44, + }); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeStub.called); + }); + }); + + test('_handleDiffEditRoute', () => { + const normalizeRangeSpy = + sandbox.spy(element, '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + const ctx = { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + 3, // 2 Patch num + 'foo/bar/baz', // 3 File path + ], + }; + const appParams = { + project: 'foo/bar', + changeNum: 1234, + view: Gerrit.Nav.View.EDIT, + path: 'foo/bar/baz', + patchNum: 3, + lineNum: undefined, + }; + + element._handleDiffEditRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeSpy.calledOnce); + assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); + assert.isFalse(normalizeRangeSpy.lastCall.returnValue); + assert.deepEqual(setParamsStub.lastCall.args[0], appParams); + }); + + test('_handleDiffEditRoute with lineNum', () => { + const normalizeRangeSpy = + sandbox.spy(element, '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + const ctx = { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + 3, // 2 Patch num + 'foo/bar/baz', // 3 File path + ], + hash: 4, + }; + const appParams = { + project: 'foo/bar', + changeNum: 1234, + view: Gerrit.Nav.View.EDIT, + path: 'foo/bar/baz', + patchNum: 3, + lineNum: 4, + }; + + element._handleDiffEditRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeSpy.calledOnce); + assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); + assert.isFalse(normalizeRangeSpy.lastCall.returnValue); + assert.deepEqual(setParamsStub.lastCall.args[0], appParams); + }); + + test('_handleChangeEditRoute', () => { + const normalizeRangeSpy = + sandbox.spy(element, '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + const ctx = { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + null, + 3, // 3 Patch num + ], + }; + const appParams = { + project: 'foo/bar', + changeNum: 1234, + view: Gerrit.Nav.View.CHANGE, + patchNum: 3, + edit: true, + }; + + element._handleChangeEditRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeSpy.calledOnce); + assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); + assert.isFalse(normalizeRangeSpy.lastCall.returnValue); + assert.deepEqual(setParamsStub.lastCall.args[0], appParams); + }); + }); + + test('_handlePluginScreen', () => { + const ctx = {params: ['foo', 'bar']}; + assertDataToParams(ctx, '_handlePluginScreen', { + view: Gerrit.Nav.View.PLUGIN_SCREEN, + plugin: 'foo', + screen: 'bar', + }); + assert.isFalse(redirectStub.called); }); }); + + suite('_parseQueryString', () => { + test('empty queries', () => { + assert.deepEqual(element._parseQueryString(''), []); + assert.deepEqual(element._parseQueryString('?'), []); + assert.deepEqual(element._parseQueryString('??'), []); + assert.deepEqual(element._parseQueryString('&&&'), []); + }); + + test('url decoding', () => { + assert.deepEqual(element._parseQueryString('+'), [[' ', '']]); + assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]); + assert.deepEqual( + element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'), + [['name', 'value']]); + }); + + test('multiple parameters', () => { + assert.deepEqual( + element._parseQueryString('a=b&c=d&e=f'), + [['a', 'b'], ['c', 'd'], ['e', 'f']]); + assert.deepEqual( + element._parseQueryString('&a=b&&&e=f&'), + [['a', 'b'], ['e', 'f']]); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js index 41caab5..0ed5291 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 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. @@ -14,320 +14,332 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; - // Possible static search options for auto complete, without negations. - const SEARCH_OPERATORS = [ - 'added:', - 'age:', - 'age:1week', // Give an example age - 'assignee:', - 'author:', - 'branch:', - 'bug:', - 'cc:', - 'cc:self', - 'change:', - 'cherrypickof:', - 'comment:', - 'commentby:', - 'commit:', - 'committer:', - 'conflicts:', - 'deleted:', - 'delta:', - 'dir:', - 'directory:', - 'ext:', - 'extension:', - 'file:', - 'footer:', - 'from:', - 'has:', - 'has:draft', - 'has:edit', - 'has:star', - 'has:stars', - 'has:unresolved', - 'hashtag:', - 'intopic:', - 'is:', - 'is:abandoned', - 'is:assigned', - 'is:closed', - 'is:ignored', - 'is:merged', - 'is:open', - 'is:owner', - 'is:private', - 'is:reviewed', - 'is:reviewer', - 'is:starred', - 'is:submittable', - 'is:watched', - 'is:wip', - 'label:', - 'message:', - 'onlyexts:', - 'onlyextensions:', - 'owner:', - 'ownerin:', - 'parentproject:', - 'project:', - 'projects:', - 'query:', - 'ref:', - 'reviewedby:', - 'reviewer:', - 'reviewer:self', - 'reviewerin:', - 'size:', - 'star:', - 'status:', - 'status:abandoned', - 'status:closed', - 'status:merged', - 'status:open', - 'status:reviewed', - 'submissionid:', - 'topic:', - 'tr:', - ]; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-search-bar_html.js'; - // All of the ops, with corresponding negations. - const SEARCH_OPERATORS_WITH_NEGATIONS_SET = - new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`))); +// Possible static search options for auto complete, without negations. +const SEARCH_OPERATORS = [ + 'added:', + 'age:', + 'age:1week', // Give an example age + 'assignee:', + 'author:', + 'branch:', + 'bug:', + 'cc:', + 'cc:self', + 'change:', + 'cherrypickof:', + 'comment:', + 'commentby:', + 'commit:', + 'committer:', + 'conflicts:', + 'deleted:', + 'delta:', + 'dir:', + 'directory:', + 'ext:', + 'extension:', + 'file:', + 'footer:', + 'from:', + 'has:', + 'has:draft', + 'has:edit', + 'has:star', + 'has:stars', + 'has:unresolved', + 'hashtag:', + 'intopic:', + 'is:', + 'is:abandoned', + 'is:assigned', + 'is:closed', + 'is:ignored', + 'is:merged', + 'is:open', + 'is:owner', + 'is:private', + 'is:reviewed', + 'is:reviewer', + 'is:starred', + 'is:submittable', + 'is:watched', + 'is:wip', + 'label:', + 'message:', + 'onlyexts:', + 'onlyextensions:', + 'owner:', + 'ownerin:', + 'parentproject:', + 'project:', + 'projects:', + 'query:', + 'ref:', + 'reviewedby:', + 'reviewer:', + 'reviewer:self', + 'reviewerin:', + 'size:', + 'star:', + 'status:', + 'status:abandoned', + 'status:closed', + 'status:merged', + 'status:open', + 'status:reviewed', + 'submissionid:', + 'topic:', + 'tr:', +]; - const MAX_AUTOCOMPLETE_RESULTS = 10; +// All of the ops, with corresponding negations. +const SEARCH_OPERATORS_WITH_NEGATIONS_SET = + new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`))); - const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; +const MAX_AUTOCOMPLETE_RESULTS = 10; +const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; + +/** + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrSearchBar extends mixinBehaviors( [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-search-bar'; } /** - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element + * Fired when a search is committed + * + * @event handle-search */ - class GrSearchBar extends Polymer.mixinBehaviors( [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-search-bar'; } - /** - * Fired when a search is committed - * - * @event handle-search - */ - static get properties() { - return { - value: { - type: String, - value: '', - notify: true, - observer: '_valueChanged', + static get properties() { + return { + value: { + type: String, + value: '', + notify: true, + observer: '_valueChanged', + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + query: { + type: Function, + value() { + return this._getSearchSuggestions.bind(this); }, - keyEventTarget: { - type: Object, - value() { return document.body; }, + }, + projectSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); }, - query: { - type: Function, - value() { - return this._getSearchSuggestions.bind(this); - }, + }, + groupSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); }, - projectSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); - }, + }, + accountSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); }, - groupSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); - }, - }, - accountSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); - }, - }, - _inputVal: String, - _threshold: { - type: Number, - value: 1, - }, - }; - } + }, + _inputVal: String, + _threshold: { + type: Number, + value: 1, + }, + }; + } - attached() { - super.attached(); - this.$.restAPI.getConfig().then(serverConfig => { - const mergeability = serverConfig - && serverConfig.index - && serverConfig.index.mergeabilityComputationBehavior; - if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX' - || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') { - // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET - this._addOperator('is:mergeable'); - } - }); - } - - _addOperator(name, include_neg = true) { - SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name); - if (include_neg) { - SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`); + attached() { + super.attached(); + this.$.restAPI.getConfig().then(serverConfig => { + const mergeability = serverConfig + && serverConfig.index + && serverConfig.index.mergeabilityComputationBehavior; + if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX' + || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') { + // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET + this._addOperator('is:mergeable'); } - } + }); + } - keyboardShortcuts() { - return { - [this.Shortcut.SEARCH]: '_handleSearch', - }; - } - - _valueChanged(value) { - this._inputVal = value; - } - - _handleInputCommit(e) { - this._preventDefaultAndNavigateToInputVal(e); - } - - /** - * This function is called in a few different cases: - * - e.target is the search button - * - e.target is the gr-autocomplete widget (#searchInput) - * - e.target is the input element wrapped within #searchInput - * - * @param {!Event} e - */ - _preventDefaultAndNavigateToInputVal(e) { - e.preventDefault(); - const target = Polymer.dom(e).rootTarget; - // If the target is the #searchInput or has a sub-input component, that - // is what holds the focus as opposed to the target from the DOM event. - if (target.$.input) { - target.$.input.blur(); - } else { - target.blur(); - } - const trimmedInput = this._inputVal && this._inputVal.trim(); - if (trimmedInput) { - const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET] - .some(op => op.endsWith(':') && op === trimmedInput); - if (predefinedOpOnlyQuery) { - return; - } - this.dispatchEvent(new CustomEvent('handle-search', { - detail: {inputVal: this._inputVal}, - })); - } - } - - /** - * Determine what array of possible suggestions should be provided - * to _getSearchSuggestions. - * - * @param {string} input - The full search term, in lowercase. - * @return {!Promise} This returns a promise that resolves to an array of - * suggestion objects. - */ - _fetchSuggestions(input) { - // Split the input on colon to get a two part predicate/expression. - const splitInput = input.split(':'); - const predicate = splitInput[0]; - const expression = splitInput[1] || ''; - // Switch on the predicate to determine what to autocomplete. - switch (predicate) { - case 'ownerin': - case 'reviewerin': - // Fetch groups. - return this.groupSuggestions(predicate, expression); - - case 'parentproject': - case 'project': - // Fetch projects. - return this.projectSuggestions(predicate, expression); - - case 'author': - case 'cc': - case 'commentby': - case 'committer': - case 'from': - case 'owner': - case 'reviewedby': - case 'reviewer': - // Fetch accounts. - return this.accountSuggestions(predicate, expression); - - default: - return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET] - .filter(operator => operator.includes(input)) - .map(operator => { return {text: operator}; })); - } - } - - /** - * Get the sorted, pruned list of suggestions for the current search query. - * - * @param {string} input - The complete search query. - * @return {!Promise} This returns a promise that resolves to an array of - * suggestions. - */ - _getSearchSuggestions(input) { - // Allow spaces within quoted terms. - const tokens = input.match(TOKENIZE_REGEX); - const trimmedInput = tokens[tokens.length - 1].toLowerCase(); - - return this._fetchSuggestions(trimmedInput) - .then(suggestions => { - if (!suggestions || !suggestions.length) { return []; } - return suggestions - // Prioritize results that start with the input. - .sort((a, b) => { - const aContains = a.text.toLowerCase().indexOf(trimmedInput); - const bContains = b.text.toLowerCase().indexOf(trimmedInput); - if (aContains === bContains) { - return a.text.localeCompare(b.text); - } - if (aContains === -1) { - return 1; - } - if (bContains === -1) { - return -1; - } - return aContains - bContains; - }) - // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results. - .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1) - // Map to an object to play nice with gr-autocomplete. - .map(({text, label}) => { - return { - name: text, - value: text, - label, - }; - }); - }); - } - - _handleSearch(e) { - const keyboardEvent = this.getKeyboardEvent(e); - if (this.shouldSuppressKeyboardShortcut(e) || - (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; } - - e.preventDefault(); - this.$.searchInput.focus(); - this.$.searchInput.selectAll(); + _addOperator(name, include_neg = true) { + SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name); + if (include_neg) { + SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`); } } - customElements.define(GrSearchBar.is, GrSearchBar); -})(); + keyboardShortcuts() { + return { + [this.Shortcut.SEARCH]: '_handleSearch', + }; + } + + _valueChanged(value) { + this._inputVal = value; + } + + _handleInputCommit(e) { + this._preventDefaultAndNavigateToInputVal(e); + } + + /** + * This function is called in a few different cases: + * - e.target is the search button + * - e.target is the gr-autocomplete widget (#searchInput) + * - e.target is the input element wrapped within #searchInput + * + * @param {!Event} e + */ + _preventDefaultAndNavigateToInputVal(e) { + e.preventDefault(); + const target = dom(e).rootTarget; + // If the target is the #searchInput or has a sub-input component, that + // is what holds the focus as opposed to the target from the DOM event. + if (target.$.input) { + target.$.input.blur(); + } else { + target.blur(); + } + const trimmedInput = this._inputVal && this._inputVal.trim(); + if (trimmedInput) { + const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET] + .some(op => op.endsWith(':') && op === trimmedInput); + if (predefinedOpOnlyQuery) { + return; + } + this.dispatchEvent(new CustomEvent('handle-search', { + detail: {inputVal: this._inputVal}, + })); + } + } + + /** + * Determine what array of possible suggestions should be provided + * to _getSearchSuggestions. + * + * @param {string} input - The full search term, in lowercase. + * @return {!Promise} This returns a promise that resolves to an array of + * suggestion objects. + */ + _fetchSuggestions(input) { + // Split the input on colon to get a two part predicate/expression. + const splitInput = input.split(':'); + const predicate = splitInput[0]; + const expression = splitInput[1] || ''; + // Switch on the predicate to determine what to autocomplete. + switch (predicate) { + case 'ownerin': + case 'reviewerin': + // Fetch groups. + return this.groupSuggestions(predicate, expression); + + case 'parentproject': + case 'project': + // Fetch projects. + return this.projectSuggestions(predicate, expression); + + case 'author': + case 'cc': + case 'commentby': + case 'committer': + case 'from': + case 'owner': + case 'reviewedby': + case 'reviewer': + // Fetch accounts. + return this.accountSuggestions(predicate, expression); + + default: + return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET] + .filter(operator => operator.includes(input)) + .map(operator => { return {text: operator}; })); + } + } + + /** + * Get the sorted, pruned list of suggestions for the current search query. + * + * @param {string} input - The complete search query. + * @return {!Promise} This returns a promise that resolves to an array of + * suggestions. + */ + _getSearchSuggestions(input) { + // Allow spaces within quoted terms. + const tokens = input.match(TOKENIZE_REGEX); + const trimmedInput = tokens[tokens.length - 1].toLowerCase(); + + return this._fetchSuggestions(trimmedInput) + .then(suggestions => { + if (!suggestions || !suggestions.length) { return []; } + return suggestions + // Prioritize results that start with the input. + .sort((a, b) => { + const aContains = a.text.toLowerCase().indexOf(trimmedInput); + const bContains = b.text.toLowerCase().indexOf(trimmedInput); + if (aContains === bContains) { + return a.text.localeCompare(b.text); + } + if (aContains === -1) { + return 1; + } + if (bContains === -1) { + return -1; + } + return aContains - bContains; + }) + // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results. + .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1) + // Map to an object to play nice with gr-autocomplete. + .map(({text, label}) => { + return { + name: text, + value: text, + label, + }; + }); + }); + } + + _handleSearch(e) { + const keyboardEvent = this.getKeyboardEvent(e); + if (this.shouldSuppressKeyboardShortcut(e) || + (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; } + + e.preventDefault(); + this.$.searchInput.focus(); + this.$.searchInput.selectAll(); + } +} + +customElements.define(GrSearchBar.is, GrSearchBar);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js index cb8e142..831b080 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -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="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<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-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-search-bar"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> form { display: flex; @@ -36,19 +29,7 @@ } </style> <form> - <gr-autocomplete - show-search-icon - id="searchInput" - text="{{_inputVal}}" - query="[[query]]" - on-commit="_handleInputCommit" - allow-non-suggested-values - multi - threshold="[[_threshold]]" - tab-complete - vertical-offset="30"></gr-autocomplete> + <gr-autocomplete show-search-icon="" id="searchInput" text="{{_inputVal}}" query="[[query]]" on-commit="_handleInputCommit" allow-non-suggested-values="" multi="" threshold="[[_threshold]]" tab-complete="" vertical-offset="30"></gr-autocomplete> </form> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-search-bar.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html index 5b5dc02..1bd0fca 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -19,18 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-search-bar</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> -<link rel="import" href="gr-search-bar.html"> -<script src="../../../scripts/util.js"></script> +<script type="module" src="./gr-search-bar.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<script>void (0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-search-bar.js'; +import '../../../scripts/util.js'; +void (0); +</script> <test-fixture id="basic"> <template> @@ -38,199 +44,202 @@ </template> </test-fixture> -<script> - suite('gr-search-bar tests', async () => { - await readyToTest(); - const kb = window.Gerrit.KeyboardShortcutBinder; - kb.bindShortcut(kb.Shortcut.SEARCH, '/'); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-search-bar.js'; +import '../../../scripts/util.js'; +suite('gr-search-bar tests', () => { + const kb = window.Gerrit.KeyboardShortcutBinder; + kb.bindShortcut(kb.Shortcut.SEARCH, '/'); - let element; - let sandbox; + let element; + let sandbox; - setup(done => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - flush(done); + setup(done => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + flush(done); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('value is propagated to _inputVal', () => { + element.value = 'foo'; + assert.equal(element._inputVal, 'foo'); + }); + + const getActiveElement = () => (document.activeElement.shadowRoot ? + document.activeElement.shadowRoot.activeElement : + document.activeElement); + + test('enter in search input fires event', done => { + element.addEventListener('handle-search', () => { + assert.notEqual(getActiveElement(), element.$.searchInput); + assert.notEqual(getActiveElement(), element.$.searchButton); + done(); + }); + element.value = 'test'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + }); + + test('input blurred after commit', () => { + const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur'); + element.$.searchInput.text = 'fate/stay'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(blurSpy.called); + }); + + test('empty search query does not trigger nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = ''; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isFalse(searchSpy.called); + }); + + test('Predefined query op with no predication doesnt trigger nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'added:'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isFalse(searchSpy.called); + }); + + test('predefined predicate query triggers nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'age:1week'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(searchSpy.called); + }); + + test('undefined predicate query triggers nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'random:1week'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(searchSpy.called); + }); + + test('empty undefined predicate query triggers nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'random:'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(searchSpy.called); + }); + + test('keyboard shortcuts', () => { + const focusSpy = sandbox.spy(element.$.searchInput, 'focus'); + const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll'); + MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/'); + assert.isTrue(focusSpy.called); + assert.isTrue(selectAllSpy.called); + }); + + suite('_getSearchSuggestions', () => { + test('Autocompletes accounts', () => { + sandbox.stub(element, 'accountSuggestions', () => + Promise.resolve([{text: 'owner:fred@goog.co'}]) + ); + return element._getSearchSuggestions('owner:fr').then(s => { + assert.equal(s[0].value, 'owner:fred@goog.co'); + }); }); - teardown(() => { - sandbox.restore(); - }); - - test('value is propagated to _inputVal', () => { - element.value = 'foo'; - assert.equal(element._inputVal, 'foo'); - }); - - const getActiveElement = () => (document.activeElement.shadowRoot ? - document.activeElement.shadowRoot.activeElement : - document.activeElement); - - test('enter in search input fires event', done => { - element.addEventListener('handle-search', () => { - assert.notEqual(getActiveElement(), element.$.searchInput); - assert.notEqual(getActiveElement(), element.$.searchButton); + test('Autocompletes groups', done => { + sandbox.stub(element, 'groupSuggestions', () => + Promise.resolve([ + {text: 'ownerin:Polygerrit'}, + {text: 'ownerin:gerrit'}, + ]) + ); + element._getSearchSuggestions('ownerin:pol').then(s => { + assert.equal(s[0].value, 'ownerin:Polygerrit'); done(); }); - element.value = 'test'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); }); - test('input blurred after commit', () => { - const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur'); - element.$.searchInput.text = 'fate/stay'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(blurSpy.called); + test('Autocompletes projects', done => { + sandbox.stub(element, 'projectSuggestions', () => + Promise.resolve([ + {text: 'project:Polygerrit'}, + {text: 'project:gerrit'}, + {text: 'project:gerrittest'}, + ]) + ); + element._getSearchSuggestions('project:pol').then(s => { + assert.equal(s[0].value, 'project:Polygerrit'); + done(); + }); }); - test('empty search query does not trigger nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = ''; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isFalse(searchSpy.called); + test('Autocompletes simple searches', done => { + element._getSearchSuggestions('is:o').then(s => { + assert.equal(s[0].name, 'is:open'); + assert.equal(s[0].value, 'is:open'); + assert.equal(s[1].name, 'is:owner'); + assert.equal(s[1].value, 'is:owner'); + done(); + }); }); - test('Predefined query op with no predication doesnt trigger nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'added:'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isFalse(searchSpy.called); + test('Does not autocomplete with no match', done => { + element._getSearchSuggestions('asdasdasdasd').then(s => { + assert.equal(s.length, 0); + done(); + }); }); - test('predefined predicate query triggers nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'age:1week'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(searchSpy.called); + test('Autocompltes without is:mergable when disabled', done => { + element._getSearchSuggestions('is:mergeab').then(s => { + assert.equal(s.length, 0); + done(); + }); }); + }); - test('undefined predicate query triggers nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'random:1week'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(searchSpy.called); - }); - - test('empty undefined predicate query triggers nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'random:'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(searchSpy.called); - }); - - test('keyboard shortcuts', () => { - const focusSpy = sandbox.spy(element.$.searchInput, 'focus'); - const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll'); - MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/'); - assert.isTrue(focusSpy.called); - assert.isTrue(selectAllSpy.called); - }); - - suite('_getSearchSuggestions', () => { - test('Autocompletes accounts', () => { - sandbox.stub(element, 'accountSuggestions', () => - Promise.resolve([{text: 'owner:fred@goog.co'}]) - ); - return element._getSearchSuggestions('owner:fr').then(s => { - assert.equal(s[0].value, 'owner:fred@goog.co'); + [ + 'API_REF_UPDATED_AND_CHANGE_REINDEX', + 'REF_UPDATED_AND_CHANGE_REINDEX', + ].forEach(mergeability => { + suite(`mergeability as ${mergeability}`, () => { + setup(done => { + stub('gr-rest-api-interface', { + getConfig() { + return Promise.resolve({ + index: { + mergeabilityComputationBehavior: mergeability, + }, + }); + }, }); + + element = fixture('basic'); + flush(done); }); - test('Autocompletes groups', done => { - sandbox.stub(element, 'groupSuggestions', () => - Promise.resolve([ - {text: 'ownerin:Polygerrit'}, - {text: 'ownerin:gerrit'}, - ]) - ); - element._getSearchSuggestions('ownerin:pol').then(s => { - assert.equal(s[0].value, 'ownerin:Polygerrit'); - done(); - }); - }); - - test('Autocompletes projects', done => { - sandbox.stub(element, 'projectSuggestions', () => - Promise.resolve([ - {text: 'project:Polygerrit'}, - {text: 'project:gerrit'}, - {text: 'project:gerrittest'}, - ]) - ); - element._getSearchSuggestions('project:pol').then(s => { - assert.equal(s[0].value, 'project:Polygerrit'); - done(); - }); - }); - - test('Autocompletes simple searches', done => { - element._getSearchSuggestions('is:o').then(s => { - assert.equal(s[0].name, 'is:open'); - assert.equal(s[0].value, 'is:open'); - assert.equal(s[1].name, 'is:owner'); - assert.equal(s[1].value, 'is:owner'); - done(); - }); - }); - - test('Does not autocomplete with no match', done => { - element._getSearchSuggestions('asdasdasdasd').then(s => { - assert.equal(s.length, 0); - done(); - }); - }); - - test('Autocompltes without is:mergable when disabled', done => { + test('Autocompltes with is:mergable when enabled', done => { element._getSearchSuggestions('is:mergeab').then(s => { - assert.equal(s.length, 0); + assert.equal(s.length, 2); + assert.equal(s[0].name, 'is:mergeable'); + assert.equal(s[0].value, 'is:mergeable'); + assert.equal(s[1].name, '-is:mergeable'); + assert.equal(s[1].value, '-is:mergeable'); done(); }); }); }); - - [ - 'API_REF_UPDATED_AND_CHANGE_REINDEX', - 'REF_UPDATED_AND_CHANGE_REINDEX', - ].forEach(mergeability => { - suite(`mergeability as ${mergeability}`, () => { - setup(done => { - stub('gr-rest-api-interface', { - getConfig() { - return Promise.resolve({ - index: { - mergeabilityComputationBehavior: mergeability, - }, - }); - }, - }); - - element = fixture('basic'); - flush(done); - }); - - test('Autocompltes with is:mergable when enabled', done => { - element._getSearchSuggestions('is:mergeab').then(s => { - assert.equal(s.length, 2); - assert.equal(s[0].name, 'is:mergeable'); - assert.equal(s[0].value, 'is:mergeable'); - assert.equal(s[1].name, '-is:mergeable'); - assert.equal(s[1].value, '-is:mergeable'); - done(); - }); - }); - }); - }); }); +}); </script> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js index cfdd524..a93c139 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -14,152 +14,162 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const MAX_AUTOCOMPLETE_RESULTS = 10; - const SELF_EXPRESSION = 'self'; - const ME_EXPRESSION = 'me'; +import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js'; +import '../gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-search-bar/gr-search-bar.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-smart-search_html.js'; - /** - * @appliesMixin Gerrit.DisplayNameMixin - * @extends Polymer.Element - */ - class GrSmartSearch extends Polymer.mixinBehaviors( [ - Gerrit.DisplayNameBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-smart-search'; } +const MAX_AUTOCOMPLETE_RESULTS = 10; +const SELF_EXPRESSION = 'self'; +const ME_EXPRESSION = 'me'; - static get properties() { - return { - searchQuery: String, - _config: Object, - _projectSuggestions: { - type: Function, - value() { - return this._fetchProjects.bind(this); - }, +/** + * @appliesMixin Gerrit.DisplayNameMixin + * @extends Polymer.Element + */ +class GrSmartSearch extends mixinBehaviors( [ + Gerrit.DisplayNameBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-smart-search'; } + + static get properties() { + return { + searchQuery: String, + _config: Object, + _projectSuggestions: { + type: Function, + value() { + return this._fetchProjects.bind(this); }, - _groupSuggestions: { - type: Function, - value() { - return this._fetchGroups.bind(this); - }, + }, + _groupSuggestions: { + type: Function, + value() { + return this._fetchGroups.bind(this); }, - _accountSuggestions: { - type: Function, - value() { - return this._fetchAccounts.bind(this); - }, + }, + _accountSuggestions: { + type: Function, + value() { + return this._fetchAccounts.bind(this); }, - }; - } + }, + }; + } - /** @override */ - attached() { - super.attached(); - this.$.restAPI.getConfig().then(cfg => { - this._config = cfg; - }); - } + /** @override */ + attached() { + super.attached(); + this.$.restAPI.getConfig().then(cfg => { + this._config = cfg; + }); + } - _handleSearch(e) { - const input = e.detail.inputVal; - if (input) { - Gerrit.Nav.navigateToSearchQuery(input); - } - } - - /** - * Fetch from the API the predicted projects. - * - * @param {string} predicate - The first part of the search term, e.g. - * 'project' - * @param {string} expression - The second part of the search term, e.g. - * 'gerr' - * @return {!Promise} This returns a promise that resolves to an array of - * strings. - */ - _fetchProjects(predicate, expression) { - return this.$.restAPI.getSuggestedProjects( - expression, - MAX_AUTOCOMPLETE_RESULTS) - .then(projects => { - if (!projects) { return []; } - const keys = Object.keys(projects); - return keys.map(key => { return {text: predicate + ':' + key}; }); - }); - } - - /** - * Fetch from the API the predicted groups. - * - * @param {string} predicate - The first part of the search term, e.g. - * 'ownerin' - * @param {string} expression - The second part of the search term, e.g. - * 'polyger' - * @return {!Promise} This returns a promise that resolves to an array of - * strings. - */ - _fetchGroups(predicate, expression) { - if (expression.length === 0) { return Promise.resolve([]); } - return this.$.restAPI.getSuggestedGroups( - expression, - MAX_AUTOCOMPLETE_RESULTS) - .then(groups => { - if (!groups) { return []; } - const keys = Object.keys(groups); - return keys.map(key => { return {text: predicate + ':' + key}; }); - }); - } - - /** - * Fetch from the API the predicted accounts. - * - * @param {string} predicate - The first part of the search term, e.g. - * 'owner' - * @param {string} expression - The second part of the search term, e.g. - * 'kasp' - * @return {!Promise} This returns a promise that resolves to an array of - * strings. - */ - _fetchAccounts(predicate, expression) { - if (expression.length === 0) { return Promise.resolve([]); } - return this.$.restAPI.getSuggestedAccounts( - expression, - MAX_AUTOCOMPLETE_RESULTS) - .then(accounts => { - if (!accounts) { return []; } - return this._mapAccountsHelper(accounts, predicate); - }) - .then(accounts => { - // When the expression supplied is a beginning substring of 'self', - // add it as an autocomplete option. - if (SELF_EXPRESSION.startsWith(expression)) { - return accounts.concat( - [{text: predicate + ':' + SELF_EXPRESSION}]); - } else if (ME_EXPRESSION.startsWith(expression)) { - return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]); - } else { - return accounts; - } - }); - } - - _mapAccountsHelper(accounts, predicate) { - return accounts.map(account => { - const userName = this.getUserName(this._serverConfig, account, false); - return { - label: account.name || '', - text: account.email ? - `${predicate}:${account.email}` : - `${predicate}:"${userName}"`, - }; - }); + _handleSearch(e) { + const input = e.detail.inputVal; + if (input) { + Gerrit.Nav.navigateToSearchQuery(input); } } - customElements.define(GrSmartSearch.is, GrSmartSearch); -})(); + /** + * Fetch from the API the predicted projects. + * + * @param {string} predicate - The first part of the search term, e.g. + * 'project' + * @param {string} expression - The second part of the search term, e.g. + * 'gerr' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchProjects(predicate, expression) { + return this.$.restAPI.getSuggestedProjects( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(projects => { + if (!projects) { return []; } + const keys = Object.keys(projects); + return keys.map(key => { return {text: predicate + ':' + key}; }); + }); + } + + /** + * Fetch from the API the predicted groups. + * + * @param {string} predicate - The first part of the search term, e.g. + * 'ownerin' + * @param {string} expression - The second part of the search term, e.g. + * 'polyger' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchGroups(predicate, expression) { + if (expression.length === 0) { return Promise.resolve([]); } + return this.$.restAPI.getSuggestedGroups( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(groups => { + if (!groups) { return []; } + const keys = Object.keys(groups); + return keys.map(key => { return {text: predicate + ':' + key}; }); + }); + } + + /** + * Fetch from the API the predicted accounts. + * + * @param {string} predicate - The first part of the search term, e.g. + * 'owner' + * @param {string} expression - The second part of the search term, e.g. + * 'kasp' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchAccounts(predicate, expression) { + if (expression.length === 0) { return Promise.resolve([]); } + return this.$.restAPI.getSuggestedAccounts( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(accounts => { + if (!accounts) { return []; } + return this._mapAccountsHelper(accounts, predicate); + }) + .then(accounts => { + // When the expression supplied is a beginning substring of 'self', + // add it as an autocomplete option. + if (SELF_EXPRESSION.startsWith(expression)) { + return accounts.concat( + [{text: predicate + ':' + SELF_EXPRESSION}]); + } else if (ME_EXPRESSION.startsWith(expression)) { + return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]); + } else { + return accounts; + } + }); + } + + _mapAccountsHelper(accounts, predicate) { + return accounts.map(account => { + const userName = this.getUserName(this._serverConfig, account, false); + return { + label: account.name || '', + text: account.email ? + `${predicate}:${account.email}` : + `${predicate}:"${userName}"`, + }; + }); + } +} + +customElements.define(GrSmartSearch.is, GrSmartSearch);
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js index c4ae41b..78906a8 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
@@ -1,38 +1,25 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @license + * Copyright (C) 2020 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. + */ +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.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="../gr-search-bar/gr-search-bar.html"> - -<dom-module id="gr-smart-search"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> </style> - <gr-search-bar id="search" - value="{{searchQuery}}" - on-handle-search="_handleSearch" - project-suggestions="[[_projectSuggestions]]" - group-suggestions="[[_groupSuggestions]]" - account-suggestions="[[_accountSuggestions]]"></gr-search-bar> + <gr-search-bar id="search" value="{{searchQuery}}" on-handle-search="_handleSearch" project-suggestions="[[_projectSuggestions]]" group-suggestions="[[_groupSuggestions]]" account-suggestions="[[_accountSuggestions]]"></gr-search-bar> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-smart-search.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html index 6fd00c7..a0ba203 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-smart-search</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/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> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-smart-search.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-smart-search.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-smart-search.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,125 +40,127 @@ </template> </test-fixture> -<script> - suite('gr-smart-search tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-smart-search.js'; +suite('gr-smart-search tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('Autocompletes accounts', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([ - { - name: 'fred', - email: 'fred@goog.co', - }, - ]) - ); - return element._fetchAccounts('owner', 'fr').then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); - }); - }); - - test('Inserts self as option when valid', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([ - { - name: 'fred', - email: 'fred@goog.co', - }, - ]) - ); - element._fetchAccounts('owner', 's') - .then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); - assert.deepEqual(s[1], {text: 'owner:self'}); - }) - .then(() => element._fetchAccounts('owner', 'selfs')) - .then(s => { - assert.notEqual(s[0], {text: 'owner:self'}); - }); - }); - - test('Inserts me as option when valid', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([ - { - name: 'fred', - email: 'fred@goog.co', - }, - ]) - ); - return element._fetchAccounts('owner', 'm') - .then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); - assert.deepEqual(s[1], {text: 'owner:me'}); - }) - .then(() => element._fetchAccounts('owner', 'meme')) - .then(s => { - assert.notEqual(s[0], {text: 'owner:me'}); - }); - }); - - test('Autocompletes groups', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => - Promise.resolve({ - Polygerrit: 0, - gerrit: 0, - gerrittest: 0, - }) - ); - return element._fetchGroups('ownerin', 'pol').then(s => { - assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); - }); - }); - - test('Autocompletes projects', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () => - Promise.resolve({Polygerrit: 0})); - return element._fetchProjects('project', 'pol').then(s => { - assert.deepEqual(s[0], {text: 'project:Polygerrit'}); - }); - }); - - test('Autocomplete doesnt override exact matches to input', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => - Promise.resolve({ - Polygerrit: 0, - gerrit: 0, - gerrittest: 0, - }) - ); - return element._fetchGroups('ownerin', 'gerrit').then(s => { - assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); - assert.deepEqual(s[1], {text: 'ownerin:gerrit'}); - assert.deepEqual(s[2], {text: 'ownerin:gerrittest'}); - }); - }); - - test('Autocompletes accounts with no email', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([{name: 'fred'}])); - return element._fetchAccounts('owner', 'fr').then(s => { - assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'}); - }); - }); - - test('Autocompletes accounts with email', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([{email: 'fred@goog.co'}])); - return element._fetchAccounts('owner', 'fr').then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''}); - }); + test('Autocompletes accounts', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([ + { + name: 'fred', + email: 'fred@goog.co', + }, + ]) + ); + return element._fetchAccounts('owner', 'fr').then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); }); }); + + test('Inserts self as option when valid', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([ + { + name: 'fred', + email: 'fred@goog.co', + }, + ]) + ); + element._fetchAccounts('owner', 's') + .then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); + assert.deepEqual(s[1], {text: 'owner:self'}); + }) + .then(() => element._fetchAccounts('owner', 'selfs')) + .then(s => { + assert.notEqual(s[0], {text: 'owner:self'}); + }); + }); + + test('Inserts me as option when valid', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([ + { + name: 'fred', + email: 'fred@goog.co', + }, + ]) + ); + return element._fetchAccounts('owner', 'm') + .then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); + assert.deepEqual(s[1], {text: 'owner:me'}); + }) + .then(() => element._fetchAccounts('owner', 'meme')) + .then(s => { + assert.notEqual(s[0], {text: 'owner:me'}); + }); + }); + + test('Autocompletes groups', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => + Promise.resolve({ + Polygerrit: 0, + gerrit: 0, + gerrittest: 0, + }) + ); + return element._fetchGroups('ownerin', 'pol').then(s => { + assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); + }); + }); + + test('Autocompletes projects', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () => + Promise.resolve({Polygerrit: 0})); + return element._fetchProjects('project', 'pol').then(s => { + assert.deepEqual(s[0], {text: 'project:Polygerrit'}); + }); + }); + + test('Autocomplete doesnt override exact matches to input', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => + Promise.resolve({ + Polygerrit: 0, + gerrit: 0, + gerrittest: 0, + }) + ); + return element._fetchGroups('ownerin', 'gerrit').then(s => { + assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); + assert.deepEqual(s[1], {text: 'ownerin:gerrit'}); + assert.deepEqual(s[2], {text: 'ownerin:gerrittest'}); + }); + }); + + test('Autocompletes accounts with no email', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([{name: 'fred'}])); + return element._fetchAccounts('owner', 'fr').then(s => { + assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'}); + }); + }); + + test('Autocompletes accounts with email', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([{email: 'fred@goog.co'}])); + return element._fetchAccounts('owner', 'fr').then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''}); + }); + }); +}); </script>