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/behaviors/async-foreach-behavior/async-foreach-behavior.js b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
index f560ea8..5e6f7c6 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.js
@@ -1,21 +1,19 @@
-<!--
-@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.
--->
-
-<script>
+/**
+ * @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';
@@ -63,4 +61,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
index f2f0e6b..fa5409b 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -19,40 +19,42 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>async-foreach-behavior</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="async-foreach-behavior.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="./async-foreach-behavior.js"></script>
-<script>
- suite('async-foreach-behavior tests', async () => {
- await readyToTest();
- test('loops over each item', () => {
- const fn = sinon.stub().returns(Promise.resolve());
- return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
- .then(() => {
- assert.isTrue(fn.calledThrice);
- assert.equal(fn.getCall(0).args[0], 1);
- assert.equal(fn.getCall(1).args[0], 2);
- assert.equal(fn.getCall(2).args[0], 3);
- });
- });
-
- test('halts on stop condition', () => {
- const stub = sinon.stub();
- const fn = (e, stop) => {
- stub(e);
- stop();
- return Promise.resolve();
- };
- return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
- .then(() => {
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args[0], 1);
- });
- });
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './async-foreach-behavior.js';
+suite('async-foreach-behavior tests', () => {
+ test('loops over each item', () => {
+ const fn = sinon.stub().returns(Promise.resolve());
+ return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+ .then(() => {
+ assert.isTrue(fn.calledThrice);
+ assert.equal(fn.getCall(0).args[0], 1);
+ assert.equal(fn.getCall(1).args[0], 2);
+ assert.equal(fn.getCall(2).args[0], 3);
+ });
});
+
+ test('halts on stop condition', () => {
+ const stub = sinon.stub();
+ const fn = (e, stop) => {
+ stub(e);
+ stop();
+ return Promise.resolve();
+ };
+ return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+ .then(() => {
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args[0], 1);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
index 92596e0..9682776 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.js
@@ -1,21 +1,19 @@
-<!--
-@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.
--->
-
-<script>
+/**
+ * @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';
@@ -46,4 +44,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
index eb6fd3f..4f6f1f5 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -19,17 +19,20 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>base-url-behavior</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>
- /** @type {string} */
- window.CANONICAL_PATH = '/r';
+<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">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './base-url-behavior.js';
+/** @type {string} */
+window.CANONICAL_PATH = '/r';
</script>
-<link rel="import" href="base-url-behavior.html">
+<script type="module" src="./base-url-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -45,30 +48,33 @@
</template>
</test-fixture>
-<script>
- suite('base-url-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './base-url-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('base-url-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [
- Gerrit.BaseUrlBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- });
-
- test('getBaseUrl', () => {
- assert.deepEqual(element.getBaseUrl(), '/r');
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [
+ Gerrit.BaseUrlBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ });
+
+ test('getBaseUrl', () => {
+ assert.deepEqual(element.getBaseUrl(), '/r');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
index 05a7a58..01bcc87 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.js
@@ -1,21 +1,21 @@
-<!--
-@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.
+ */
+import '../base-url-behavior/base-url-behavior.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="../base-url-behavior/base-url-behavior.html">
-<script>
(function(window) {
'use strict';
@@ -77,4 +77,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
index e480e30..4b72748 100644
--- a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -15,17 +15,22 @@
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
-<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>
<title>docs-url-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<link rel="import" href="docs-url-behavior.html">
+<script type="module" src="./docs-url-behavior.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './docs-url-behavior.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -33,71 +38,74 @@
</template>
</test-fixture>
-<script>
- suite('docs-url-behavior tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './docs-url-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('docs-url-behavior tests', () => {
+ let element;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'docs-url-behavior-element',
- behaviors: [Gerrit.DocsUrlBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- element._clearDocsBaseUrlCache();
- });
-
- test('null config', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
- };
- return element.getDocsBaseUrl(null, mockRestApi)
- .then(docsBaseUrl => {
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
- assert.equal(docsBaseUrl, '/Documentation');
- });
- });
-
- test('no doc config', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
- };
- const config = {gerrit: {}};
- return element.getDocsBaseUrl(config, mockRestApi)
- .then(docsBaseUrl => {
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
- assert.equal(docsBaseUrl, '/Documentation');
- });
- });
-
- test('has doc config', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(true)),
- };
- const config = {gerrit: {doc_url: 'foobar'}};
- return element.getDocsBaseUrl(config, mockRestApi)
- .then(docsBaseUrl => {
- assert.isFalse(mockRestApi.probePath.called);
- assert.equal(docsBaseUrl, 'foobar');
- });
- });
-
- test('no probe', () => {
- const mockRestApi = {
- probePath: sinon.stub().returns(Promise.resolve(false)),
- };
- return element.getDocsBaseUrl(null, mockRestApi)
- .then(docsBaseUrl => {
- assert.isTrue(
- mockRestApi.probePath.calledWith('/Documentation/index.html'));
- assert.isNotOk(docsBaseUrl);
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'docs-url-behavior-element',
+ behaviors: [Gerrit.DocsUrlBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ element._clearDocsBaseUrlCache();
+ });
+
+ test('null config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ return element.getDocsBaseUrl(null, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.equal(docsBaseUrl, '/Documentation');
+ });
+ });
+
+ test('no doc config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ const config = {gerrit: {}};
+ return element.getDocsBaseUrl(config, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.equal(docsBaseUrl, '/Documentation');
+ });
+ });
+
+ test('has doc config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ const config = {gerrit: {doc_url: 'foobar'}};
+ return element.getDocsBaseUrl(config, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isFalse(mockRestApi.probePath.called);
+ assert.equal(docsBaseUrl, 'foobar');
+ });
+ });
+
+ test('no probe', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(false)),
+ };
+ return element.getDocsBaseUrl(null, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.isNotOk(docsBaseUrl);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
index 1377627..1607ba8 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@license
-Copyright (C) 2018 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.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2018 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';
@@ -61,4 +60,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
index 9acc749..09a7cc6 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>dom-util-behavior</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="dom-util-behavior.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="./dom-util-behavior.js"></script>
<test-fixture id="nested-structure">
<template>
@@ -40,34 +40,37 @@
</template>
</test-fixture>
-<script>
- suite('dom-util-behavior tests', async () => {
- await readyToTest();
- let element;
- let divs;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './dom-util-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('dom-util-behavior tests', () => {
+ let element;
+ let divs;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.DomUtilBehavior],
- });
- });
-
- setup(() => {
- const testDom = fixture('nested-structure');
- element = testDom[0];
- divs = testDom[1];
- });
-
- test('descendedFromClass', () => {
- // .c is a child of .a and not vice versa.
- assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
- assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
-
- // Stops at stop element.
- assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
- divs.querySelector('.b')));
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.DomUtilBehavior],
});
});
+
+ setup(() => {
+ const testDom = fixture('nested-structure');
+ element = testDom[0];
+ divs = testDom[1];
+ });
+
+ test('descendedFromClass', () => {
+ // .c is a child of .a and not vice versa.
+ assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
+ assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
+
+ // Stops at stop element.
+ assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
+ divs.querySelector('.b')));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
index 5b3d420..9e9df1d 100644
--- a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
+++ b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
@@ -1,21 +1,19 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<script>
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -69,4 +67,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
index 7f01789..18cd356 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
@@ -1,21 +1,19 @@
-<!--
-@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.
--->
-
-<script>
+/**
+ * @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';
@@ -177,4 +175,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
index f4b3ab0..e989a74 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</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-access-behavior.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-access-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -33,41 +33,44 @@
</template>
</test-fixture>
-<script>
- suite('gr-access-behavior tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-access-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-access-behavior tests', () => {
+ let element;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.AccessBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('toSortedArray', () => {
- const rules = {
- 'global:Project-Owners': {
- action: 'ALLOW', force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW', force: false,
- },
- };
- const expectedResult = [
- {id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
- action: 'ALLOW', force: false,
- }},
- {id: 'global:Project-Owners', value: {
- action: 'ALLOW', force: false,
- }},
- ];
- assert.deepEqual(element.toSortedArray(rules), expectedResult);
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.AccessBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('toSortedArray', () => {
+ const rules = {
+ 'global:Project-Owners': {
+ action: 'ALLOW', force: false,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW', force: false,
+ },
+ };
+ const expectedResult = [
+ {id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
+ action: 'ALLOW', force: false,
+ }},
+ {id: 'global:Project-Owners', value: {
+ action: 'ALLOW', force: false,
+ }},
+ ];
+ assert.deepEqual(element.toSortedArray(rules), expectedResult);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
index 49160da..90800a3 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@license
-Copyright (C) 2018 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.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2018 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';
@@ -220,4 +219,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
index 7c179b8..c2659bb 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</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-admin-nav-behavior.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-admin-nav-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -33,338 +33,341 @@
</template>
</test-fixture>
-<script>
- suite('gr-admin-nav-behavior tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let capabilityStub;
- let menuLinkStub;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-admin-nav-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-admin-nav-behavior tests', () => {
+ let element;
+ let sandbox;
+ let capabilityStub;
+ let menuLinkStub;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [
- Gerrit.AdminNavBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- capabilityStub = sinon.stub();
- menuLinkStub = sinon.stub().returns([]);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- const testAdminLinks = (account, options, expected, done) => {
- element.getAdminLinks(account,
- capabilityStub,
- menuLinkStub,
- options)
- .then(res => {
- assert.equal(expected.totalLength, res.links.length);
- assert.equal(res.links[0].name, 'Repositories');
- // Repos
- if (expected.groupListShown) {
- assert.equal(res.links[1].name, 'Groups');
- }
-
- if (expected.pluginListShown) {
- assert.equal(res.links[2].name, 'Plugins');
- assert.isNotOk(res.links[2].subsection);
- }
-
- if (expected.projectPageShown) {
- assert.isOk(res.links[0].subsection);
- assert.equal(res.links[0].subsection.children.length, 5);
- } else {
- assert.isNotOk(res.links[0].subsection);
- }
- // Groups
- if (expected.groupPageShown) {
- assert.isOk(res.links[1].subsection);
- assert.equal(res.links[1].subsection.children.length,
- expected.groupSubpageLength);
- } else if ( expected.totalLength > 1) {
- assert.isNotOk(res.links[1].subsection);
- }
-
- if (expected.pluginGeneratedLinks) {
- for (const link of expected.pluginGeneratedLinks) {
- const linkMatch = res.links
- .find(l => (l.url === link.url && l.name === link.text));
- assert.isTrue(!!linkMatch);
-
- // External links should open in new tab.
- if (link.url[0] !== '/') {
- assert.equal(linkMatch.target, '_blank');
- } else {
- assert.isNotOk(linkMatch.target);
- }
- }
- }
-
- // Current section
- if (expected.projectPageShown || expected.groupPageShown) {
- assert.isOk(res.expandedSection);
- assert.isOk(res.expandedSection.children);
- } else {
- assert.isNotOk(res.expandedSection);
- }
- if (expected.projectPageShown) {
- assert.equal(res.expandedSection.name, 'my-repo');
- assert.equal(res.expandedSection.children.length, 5);
- } else if (expected.groupPageShown) {
- assert.equal(res.expandedSection.name, 'my-group');
- assert.equal(res.expandedSection.children.length,
- expected.groupSubpageLength);
- }
- done();
- });
- };
-
- suite('logged out', () => {
- let account;
- let expected;
-
- setup(() => {
- expected = {
- groupListShown: false,
- groupPageShown: false,
- pluginListShown: false,
- };
- });
-
- test('without a specific repo or group', done => {
- let options;
- expected = Object.assign(expected, {
- totalLength: 1,
- projectPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with a repo', done => {
- const options = {repoName: 'my-repo'};
- expected = Object.assign(expected, {
- totalLength: 1,
- projectPageShown: true,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with plugin generated links', done => {
- let options;
- const generatedLinks = [
- {text: 'internal link text', url: '/internal/link/url'},
- {text: 'external link text', url: 'http://external/link/url'},
- ];
- menuLinkStub.returns(generatedLinks);
- expected = Object.assign(expected, {
- totalLength: 3,
- projectPageShown: false,
- pluginGeneratedLinks: generatedLinks,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('no plugin capability logged in', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- expected = {
- totalLength: 2,
- pluginListShown: false,
- };
- capabilityStub.returns(Promise.resolve({}));
- });
-
- test('without a specific project or group', done => {
- let options;
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupListShown: true,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with a repo', done => {
- const account = {
- name: 'test-user',
- };
- const options = {repoName: 'my-repo'};
- expected = Object.assign(expected, {
- projectPageShown: true,
- groupListShown: true,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('view plugin capability logged in', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- capabilityStub.returns(Promise.resolve({viewPlugins: true}));
- expected = {
- totalLength: 3,
- groupListShown: true,
- pluginListShown: true,
- };
- });
-
- test('without a specific repo or group', done => {
- let options;
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('with a repo', done => {
- const options = {repoName: 'my-repo'};
- expected = Object.assign(expected, {
- projectPageShown: true,
- groupPageShown: false,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('admin with internal group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: true,
- isAdmin: true,
- groupOwner: false,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 2,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('group owner with internal group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: true,
- isAdmin: false,
- groupOwner: true,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 2,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('non owner or admin with internal group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: true,
- isAdmin: false,
- groupOwner: false,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 1,
- });
- testAdminLinks(account, options, expected, done);
- });
-
- test('admin with external group', done => {
- const options = {
- groupId: 'a15262',
- groupName: 'my-group',
- groupIsInternal: false,
- isAdmin: true,
- groupOwner: true,
- };
- expected = Object.assign(expected, {
- projectPageShown: false,
- groupPageShown: true,
- groupSubpageLength: 0,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('view plugin screen with plugin capability', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- capabilityStub.returns(Promise.resolve({pluginCapability: true}));
- expected = {};
- });
-
- test('with plugin with capabilities', done => {
- let options;
- const generatedLinks = [
- {text: 'without capability', url: '/without'},
- {text: 'with capability',
- url: '/with',
- capability: 'pluginCapability'},
- ];
- menuLinkStub.returns(generatedLinks);
- expected = Object.assign(expected, {
- totalLength: 4,
- pluginGeneratedLinks: generatedLinks,
- });
- testAdminLinks(account, options, expected, done);
- });
- });
-
- suite('view plugin screen without plugin capability', () => {
- const account = {
- name: 'test-user',
- };
- let expected;
-
- setup(() => {
- capabilityStub.returns(Promise.resolve({}));
- expected = {};
- });
-
- test('with plugin with capabilities', done => {
- let options;
- const generatedLinks = [
- {text: 'without capability', url: '/without'},
- {text: 'with capability',
- url: '/with',
- capability: 'pluginCapability'},
- ];
- menuLinkStub.returns(generatedLinks);
- expected = Object.assign(expected, {
- totalLength: 3,
- pluginGeneratedLinks: [generatedLinks[0]],
- });
- testAdminLinks(account, options, expected, done);
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [
+ Gerrit.AdminNavBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ capabilityStub = sinon.stub();
+ menuLinkStub = sinon.stub().returns([]);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ const testAdminLinks = (account, options, expected, done) => {
+ element.getAdminLinks(account,
+ capabilityStub,
+ menuLinkStub,
+ options)
+ .then(res => {
+ assert.equal(expected.totalLength, res.links.length);
+ assert.equal(res.links[0].name, 'Repositories');
+ // Repos
+ if (expected.groupListShown) {
+ assert.equal(res.links[1].name, 'Groups');
+ }
+
+ if (expected.pluginListShown) {
+ assert.equal(res.links[2].name, 'Plugins');
+ assert.isNotOk(res.links[2].subsection);
+ }
+
+ if (expected.projectPageShown) {
+ assert.isOk(res.links[0].subsection);
+ assert.equal(res.links[0].subsection.children.length, 5);
+ } else {
+ assert.isNotOk(res.links[0].subsection);
+ }
+ // Groups
+ if (expected.groupPageShown) {
+ assert.isOk(res.links[1].subsection);
+ assert.equal(res.links[1].subsection.children.length,
+ expected.groupSubpageLength);
+ } else if ( expected.totalLength > 1) {
+ assert.isNotOk(res.links[1].subsection);
+ }
+
+ if (expected.pluginGeneratedLinks) {
+ for (const link of expected.pluginGeneratedLinks) {
+ const linkMatch = res.links
+ .find(l => (l.url === link.url && l.name === link.text));
+ assert.isTrue(!!linkMatch);
+
+ // External links should open in new tab.
+ if (link.url[0] !== '/') {
+ assert.equal(linkMatch.target, '_blank');
+ } else {
+ assert.isNotOk(linkMatch.target);
+ }
+ }
+ }
+
+ // Current section
+ if (expected.projectPageShown || expected.groupPageShown) {
+ assert.isOk(res.expandedSection);
+ assert.isOk(res.expandedSection.children);
+ } else {
+ assert.isNotOk(res.expandedSection);
+ }
+ if (expected.projectPageShown) {
+ assert.equal(res.expandedSection.name, 'my-repo');
+ assert.equal(res.expandedSection.children.length, 5);
+ } else if (expected.groupPageShown) {
+ assert.equal(res.expandedSection.name, 'my-group');
+ assert.equal(res.expandedSection.children.length,
+ expected.groupSubpageLength);
+ }
+ done();
+ });
+ };
+
+ suite('logged out', () => {
+ let account;
+ let expected;
+
+ setup(() => {
+ expected = {
+ groupListShown: false,
+ groupPageShown: false,
+ pluginListShown: false,
+ };
+ });
+
+ test('without a specific repo or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ totalLength: 1,
+ projectPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ totalLength: 1,
+ projectPageShown: true,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with plugin generated links', done => {
+ let options;
+ const generatedLinks = [
+ {text: 'internal link text', url: '/internal/link/url'},
+ {text: 'external link text', url: 'http://external/link/url'},
+ ];
+ menuLinkStub.returns(generatedLinks);
+ expected = Object.assign(expected, {
+ totalLength: 3,
+ projectPageShown: false,
+ pluginGeneratedLinks: generatedLinks,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('no plugin capability logged in', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ expected = {
+ totalLength: 2,
+ pluginListShown: false,
+ };
+ capabilityStub.returns(Promise.resolve({}));
+ });
+
+ test('without a specific project or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupListShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const account = {
+ name: 'test-user',
+ };
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ projectPageShown: true,
+ groupListShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('view plugin capability logged in', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ capabilityStub.returns(Promise.resolve({viewPlugins: true}));
+ expected = {
+ totalLength: 3,
+ groupListShown: true,
+ pluginListShown: true,
+ };
+ });
+
+ test('without a specific repo or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ projectPageShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('admin with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: true,
+ groupOwner: false,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 2,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('group owner with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: false,
+ groupOwner: true,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 2,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('non owner or admin with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: false,
+ groupOwner: false,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 1,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('admin with external group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: false,
+ isAdmin: true,
+ groupOwner: true,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 0,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('view plugin screen with plugin capability', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ capabilityStub.returns(Promise.resolve({pluginCapability: true}));
+ expected = {};
+ });
+
+ test('with plugin with capabilities', done => {
+ let options;
+ const generatedLinks = [
+ {text: 'without capability', url: '/without'},
+ {text: 'with capability',
+ url: '/with',
+ capability: 'pluginCapability'},
+ ];
+ menuLinkStub.returns(generatedLinks);
+ expected = Object.assign(expected, {
+ totalLength: 4,
+ pluginGeneratedLinks: generatedLinks,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('view plugin screen without plugin capability', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ capabilityStub.returns(Promise.resolve({}));
+ expected = {};
+ });
+
+ test('with plugin with capabilities', done => {
+ let options;
+ const generatedLinks = [
+ {text: 'without capability', url: '/without'},
+ {text: 'with capability',
+ url: '/with',
+ capability: 'pluginCapability'},
+ ];
+ menuLinkStub.returns(generatedLinks);
+ expected = Object.assign(expected, {
+ totalLength: 3,
+ pluginGeneratedLinks: [generatedLinks[0]],
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
index d03316a..d12b279 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@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.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -105,4 +104,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
index a8d4041..c314c18 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</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-change-table-behavior.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-change-table-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -41,87 +41,90 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-table-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-change-table-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-change-table-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.ChangeTableBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- });
-
- test('getComplementColumns', () => {
- let columns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.deepEqual(element.getComplementColumns(columns), []);
-
- columns = [
- 'Subject',
- 'Status',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Size',
- ];
- assert.deepEqual(element.getComplementColumns(columns),
- ['Owner', 'Updated']);
- });
-
- test('isColumnHidden', () => {
- const columnToCheck = 'Repo';
- let columnsToDisplay = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
-
- columnsToDisplay = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Branch',
- 'Updated',
- 'Size',
- ];
- assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
- });
-
- test('getVisibleColumns maps Project to Repo', () => {
- const columns = [
- 'Subject',
- 'Status',
- 'Owner',
- ];
- assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
- assert.deepEqual(
- element.getVisibleColumns(columns.concat(['Project'])),
- columns.slice(0).concat(['Repo']));
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.ChangeTableBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ });
+
+ test('getComplementColumns', () => {
+ let columns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+ assert.deepEqual(element.getComplementColumns(columns), []);
+
+ columns = [
+ 'Subject',
+ 'Status',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Size',
+ ];
+ assert.deepEqual(element.getComplementColumns(columns),
+ ['Owner', 'Updated']);
+ });
+
+ test('isColumnHidden', () => {
+ const columnToCheck = 'Repo';
+ let columnsToDisplay = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+ assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
+
+ columnsToDisplay = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+ assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
+ });
+
+ test('getVisibleColumns maps Project to Repo', () => {
+ const columns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ ];
+ assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
+ assert.deepEqual(
+ element.getVisibleColumns(columns.concat(['Project'])),
+ columns.slice(0).concat(['Repo']));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
index e5ded0e..1ba8fd9 100644
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
@@ -1,23 +1,21 @@
-<!--
-@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.
+ */
+import '../../scripts/gr-display-name-utils/gr-display-name-utils.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.
--->
-
-<script src="../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
-
-<script>
(function(window) {
'use strict';
@@ -57,4 +55,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
index 931f6fa..863f708 100644
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-display-name-behavior</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-display-name-behavior.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-display-name-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -33,69 +33,72 @@
</template>
</test-fixture>
-<script>
- suite('gr-display-name-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- const config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-display-name-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-display-name-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ const config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element-anon',
- behaviors: [
- Gerrit.DisplayNameBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('getUserName name only', () => {
- const account = {
- name: 'test-name',
- };
- assert.deepEqual(element.getUserName(config, account, true), 'test-name');
- });
-
- test('getUserName username only', () => {
- const account = {
- username: 'test-user',
- };
- assert.deepEqual(element.getUserName(config, account, true), 'test-user');
- });
-
- test('getUserName email only', () => {
- const account = {
- email: 'test-user@test-url.com',
- };
- assert.deepEqual(element.getUserName(config, account, true),
- 'test-user@test-url.com');
- });
-
- test('getUserName returns not Anonymous Coward as the anon name', () => {
- assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
- });
-
- test('getUserName for the config returning the anon name', () => {
- const config = {
- user: {
- anonymous_coward_name: 'Test Anon',
- },
- };
- assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
- });
-
- test('getGroupDisplayName', () => {
- assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
- 'Some user name (group)');
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element-anon',
+ behaviors: [
+ Gerrit.DisplayNameBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('getUserName name only', () => {
+ const account = {
+ name: 'test-name',
+ };
+ assert.deepEqual(element.getUserName(config, account, true), 'test-name');
+ });
+
+ test('getUserName username only', () => {
+ const account = {
+ username: 'test-user',
+ };
+ assert.deepEqual(element.getUserName(config, account, true), 'test-user');
+ });
+
+ test('getUserName email only', () => {
+ const account = {
+ email: 'test-user@test-url.com',
+ };
+ assert.deepEqual(element.getUserName(config, account, true),
+ 'test-user@test-url.com');
+ });
+
+ test('getUserName returns not Anonymous Coward as the anon name', () => {
+ assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
+ });
+
+ test('getUserName for the config returning the anon name', () => {
+ const config = {
+ user: {
+ anonymous_coward_name: 'Test Anon',
+ },
+ };
+ assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
+ });
+
+ test('getGroupDisplayName', () => {
+ assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
+ 'Some user name (group)');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
index 06912d5..6e64a4d 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
@@ -1,22 +1,22 @@
-<!--
-@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.
+ */
+import '../base-url-behavior/base-url-behavior.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="../base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<script>
+import '../gr-url-encoding-behavior/gr-url-encoding-behavior.js';
(function(window) {
'use strict';
@@ -80,5 +80,3 @@
};
}
})(window);
-
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
index c2cb073..37577df 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</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-list-view-behavior.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-list-view-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -33,62 +33,65 @@
</template>
</test-fixture>
-<script>
- suite('gr-list-view-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-list-view-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-list-view-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.ListViewBehavior],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('computeLoadingClass', () => {
- assert.equal(element.computeLoadingClass(true), 'loading');
- assert.equal(element.computeLoadingClass(false), '');
- });
-
- test('computeShownItems', () => {
- const myArr = new Array(26);
- assert.equal(element.computeShownItems(myArr).length, 25);
- });
-
- test('getUrl', () => {
- assert.equal(element.getUrl('/path/to/something/', 'item'),
- '/path/to/something/item');
- assert.equal(element.getUrl('/path/to/something/', 'item%test'),
- '/path/to/something/item%2525test');
- });
-
- test('getFilterValue', () => {
- let params;
- assert.equal(element.getFilterValue(params), '');
-
- params = {filter: null};
- assert.equal(element.getFilterValue(params), '');
-
- params = {filter: 'test'};
- assert.equal(element.getFilterValue(params), 'test');
- });
-
- test('getOffsetValue', () => {
- let params;
- assert.equal(element.getOffsetValue(params), 0);
-
- params = {offset: null};
- assert.equal(element.getOffsetValue(params), 0);
-
- params = {offset: 1};
- assert.equal(element.getOffsetValue(params), 1);
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.ListViewBehavior],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('computeLoadingClass', () => {
+ assert.equal(element.computeLoadingClass(true), 'loading');
+ assert.equal(element.computeLoadingClass(false), '');
+ });
+
+ test('computeShownItems', () => {
+ const myArr = new Array(26);
+ assert.equal(element.computeShownItems(myArr).length, 25);
+ });
+
+ test('getUrl', () => {
+ assert.equal(element.getUrl('/path/to/something/', 'item'),
+ '/path/to/something/item');
+ assert.equal(element.getUrl('/path/to/something/', 'item%test'),
+ '/path/to/something/item%2525test');
+ });
+
+ test('getFilterValue', () => {
+ let params;
+ assert.equal(element.getFilterValue(params), '');
+
+ params = {filter: null};
+ assert.equal(element.getFilterValue(params), '');
+
+ params = {filter: 'test'};
+ assert.equal(element.getFilterValue(params), 'test');
+ });
+
+ test('getOffsetValue', () => {
+ let params;
+ assert.equal(element.getOffsetValue(params), 0);
+
+ params = {offset: null};
+ assert.equal(element.getOffsetValue(params), 0);
+
+ params = {offset: 1};
+ assert.equal(element.getOffsetValue(params), 1);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
index 61e6b0a..b36375e 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@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.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -298,4 +297,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index d9adb8b..8e05228 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -15,313 +15,315 @@
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
-<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>
<title>gr-patch-set-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<link rel="import" href="gr-patch-set-behavior.html">
+<script type="module" src="./gr-patch-set-behavior.js"></script>
-<script>
- suite('gr-patch-set-behavior tests', async () => {
- await readyToTest();
- test('getRevisionByPatchNum', () => {
- const get = Gerrit.PatchSetBehavior.getRevisionByPatchNum;
- const revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.deepEqual(get(revisions, '1'), revisions[1]);
- assert.deepEqual(get(revisions, 2), revisions[2]);
- assert.equal(get(revisions, '3'), undefined);
- });
-
- test('fetchChangeUpdates on latest', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(knownChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isTrue(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isFalse(result.newMessages);
- done();
- });
- });
-
- test('fetchChangeUpdates not on latest', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const actualChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- sha3: {description: 'patch 3', _number: 3},
- },
- status: 'NEW',
- messages: [],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(actualChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isFalse(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isFalse(result.newMessages);
- done();
- });
- });
-
- test('fetchChangeUpdates new status', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const actualChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'MERGED',
- messages: [],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(actualChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isTrue(result.isLatest);
- assert.equal(result.newStatus, 'MERGED');
- assert.isFalse(result.newMessages);
- done();
- });
- });
-
- test('fetchChangeUpdates new messages', done => {
- const knownChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [],
- };
- const actualChange = {
- revisions: {
- sha1: {description: 'patch 1', _number: 1},
- sha2: {description: 'patch 2', _number: 2},
- },
- status: 'NEW',
- messages: [{message: 'blah blah'}],
- };
- const mockRestApi = {
- getChangeDetail() {
- return Promise.resolve(actualChange);
- },
- };
- Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
- .then(result => {
- assert.isTrue(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isTrue(result.newMessages);
- done();
- });
- });
-
- test('_computeWipForPatchSets', () => {
- // Compute patch sets for a given timeline on a change. The initial WIP
- // property of the change can be true or false. The map of tags by
- // revision is keyed by patch set number. Each value is a list of change
- // message tags in the order that they occurred in the timeline. These
- // indicate actions that modify the WIP property of the change and/or
- // create new patch sets.
- //
- // Returns the actual results with an assertWip method that can be used
- // to compare against an expected value for a particular patch set.
- const compute = (initialWip, tagsByRevision) => {
- const change = {
- messages: [],
- work_in_progress: initialWip,
- };
- const revs = Object.keys(tagsByRevision).sort((a, b) => a - b);
- for (const rev of revs) {
- for (const tag of tagsByRevision[rev]) {
- change.messages.push({
- tag,
- _revision_number: rev,
- });
- }
- }
- let patchNums = revs.map(rev => { return {num: rev}; });
- patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets(
- change, patchNums);
- const actualWipsByRevision = {};
- for (const patchNum of patchNums) {
- actualWipsByRevision[patchNum.num] = patchNum.wip;
- }
- const verifier = {
- assertWip(revision, expectedWip) {
- const patchNum = patchNums.find(patchNum => patchNum.num == revision);
- if (!patchNum) {
- assert.fail('revision ' + revision + ' not found');
- }
- assert.equal(patchNum.wip, expectedWip,
- 'wip state for ' + revision + ' is ' +
- patchNum.wip + '; expected ' + expectedWip);
- return verifier;
- },
- };
- return verifier;
- };
-
- compute(false, {1: ['upload']}).assertWip(1, false);
- compute(true, {1: ['upload']}).assertWip(1, true);
-
- const setWip = 'autogenerated:gerrit:setWorkInProgress';
- const uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
- const clearWip = 'autogenerated:gerrit:setReadyForReview';
-
- compute(false, {
- 1: ['upload', setWip],
- 2: ['upload'],
- 3: ['upload', clearWip],
- 4: ['upload', setWip],
- }).assertWip(1, false) // Change was created with PS1 ready for review
- .assertWip(2, true) // PS2 was uploaded during WIP
- .assertWip(3, false) // PS3 was marked ready for review after upload
- .assertWip(4, false); // PS4 was uploaded ready for review
-
- compute(false, {
- 1: [uploadInWip, null, 'addReviewer'],
- 2: ['upload'],
- 3: ['upload', clearWip, setWip],
- 4: ['upload'],
- 5: ['upload', clearWip],
- 6: [uploadInWip],
- }).assertWip(1, true) // Change was created in WIP
- .assertWip(2, true) // PS2 was uploaded during WIP
- .assertWip(3, false) // PS3 was marked ready for review
- .assertWip(4, true) // PS4 was uploaded during WIP
- .assertWip(5, false) // PS5 was marked ready for review
- .assertWip(6, true); // PS6 was uploaded with WIP option
- });
-
- test('patchNumEquals', () => {
- const equals = Gerrit.PatchSetBehavior.patchNumEquals;
- assert.isFalse(equals('edit', 'PARENT'));
- assert.isFalse(equals('edit', NaN));
- assert.isFalse(equals(1, '2'));
-
- assert.isTrue(equals(1, '1'));
- assert.isTrue(equals(1, 1));
- assert.isTrue(equals('edit', 'edit'));
- assert.isTrue(equals('PARENT', 'PARENT'));
- });
-
- test('isMergeParent', () => {
- const isParent = Gerrit.PatchSetBehavior.isMergeParent;
- assert.isFalse(isParent(1));
- assert.isFalse(isParent(4321));
- assert.isFalse(isParent('52'));
- assert.isFalse(isParent('edit'));
- assert.isFalse(isParent('PARENT'));
- assert.isFalse(isParent(0));
-
- assert.isTrue(isParent(-23));
- assert.isTrue(isParent(-1));
- assert.isTrue(isParent('-42'));
- });
-
- test('findEditParentRevision', () => {
- const findParent = Gerrit.PatchSetBehavior.findEditParentRevision;
- let revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.strictEqual(findParent(revisions), null);
-
- revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
- assert.strictEqual(findParent(revisions), null);
-
- revisions = [...revisions, {_number: 3}];
- assert.deepEqual(findParent(revisions), {_number: 3});
- });
-
- test('findEditParentPatchNum', () => {
- const findNum = Gerrit.PatchSetBehavior.findEditParentPatchNum;
- let revisions = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- assert.equal(findNum(revisions), -1);
-
- revisions =
- [...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
- assert.deepEqual(findNum(revisions), 3);
- });
-
- test('sortRevisions', () => {
- const sort = Gerrit.PatchSetBehavior.sortRevisions;
- const revisions = [
- {_number: 0},
- {_number: 2},
- {_number: 1},
- ];
- const sorted = [
- {_number: 2},
- {_number: 1},
- {_number: 0},
- ];
-
- assert.deepEqual(sort(revisions), sorted);
-
- // Edit patchset should follow directly after its basePatchNum.
- revisions.push({_number: 'edit', basePatchNum: 2});
- sorted.unshift({_number: 'edit', basePatchNum: 2});
- assert.deepEqual(sort(revisions), sorted);
-
- revisions[0].basePatchNum = 0;
- const edit = sorted.shift();
- edit.basePatchNum = 0;
- // Edit patchset should be at index 2.
- sorted.splice(2, 0, edit);
- assert.deepEqual(sort(revisions), sorted);
- });
-
- test('getParentIndex', () => {
- assert.equal(Gerrit.PatchSetBehavior.getParentIndex('-13'), 13);
- assert.equal(Gerrit.PatchSetBehavior.getParentIndex(-4), 4);
- });
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-patch-set-behavior.js';
+suite('gr-patch-set-behavior tests', () => {
+ test('getRevisionByPatchNum', () => {
+ const get = Gerrit.PatchSetBehavior.getRevisionByPatchNum;
+ const revisions = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ assert.deepEqual(get(revisions, '1'), revisions[1]);
+ assert.deepEqual(get(revisions, 2), revisions[2]);
+ assert.equal(get(revisions, '3'), undefined);
});
+
+ test('fetchChangeUpdates on latest', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(knownChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates not on latest', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ sha3: {description: 'patch 3', _number: 3},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isFalse(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new status', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'MERGED',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.equal(result.newStatus, 'MERGED');
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new messages', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [{message: 'blah blah'}],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isTrue(result.newMessages);
+ done();
+ });
+ });
+
+ test('_computeWipForPatchSets', () => {
+ // Compute patch sets for a given timeline on a change. The initial WIP
+ // property of the change can be true or false. The map of tags by
+ // revision is keyed by patch set number. Each value is a list of change
+ // message tags in the order that they occurred in the timeline. These
+ // indicate actions that modify the WIP property of the change and/or
+ // create new patch sets.
+ //
+ // Returns the actual results with an assertWip method that can be used
+ // to compare against an expected value for a particular patch set.
+ const compute = (initialWip, tagsByRevision) => {
+ const change = {
+ messages: [],
+ work_in_progress: initialWip,
+ };
+ const revs = Object.keys(tagsByRevision).sort((a, b) => a - b);
+ for (const rev of revs) {
+ for (const tag of tagsByRevision[rev]) {
+ change.messages.push({
+ tag,
+ _revision_number: rev,
+ });
+ }
+ }
+ let patchNums = revs.map(rev => { return {num: rev}; });
+ patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets(
+ change, patchNums);
+ const actualWipsByRevision = {};
+ for (const patchNum of patchNums) {
+ actualWipsByRevision[patchNum.num] = patchNum.wip;
+ }
+ const verifier = {
+ assertWip(revision, expectedWip) {
+ const patchNum = patchNums.find(patchNum => patchNum.num == revision);
+ if (!patchNum) {
+ assert.fail('revision ' + revision + ' not found');
+ }
+ assert.equal(patchNum.wip, expectedWip,
+ 'wip state for ' + revision + ' is ' +
+ patchNum.wip + '; expected ' + expectedWip);
+ return verifier;
+ },
+ };
+ return verifier;
+ };
+
+ compute(false, {1: ['upload']}).assertWip(1, false);
+ compute(true, {1: ['upload']}).assertWip(1, true);
+
+ const setWip = 'autogenerated:gerrit:setWorkInProgress';
+ const uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
+ const clearWip = 'autogenerated:gerrit:setReadyForReview';
+
+ compute(false, {
+ 1: ['upload', setWip],
+ 2: ['upload'],
+ 3: ['upload', clearWip],
+ 4: ['upload', setWip],
+ }).assertWip(1, false) // Change was created with PS1 ready for review
+ .assertWip(2, true) // PS2 was uploaded during WIP
+ .assertWip(3, false) // PS3 was marked ready for review after upload
+ .assertWip(4, false); // PS4 was uploaded ready for review
+
+ compute(false, {
+ 1: [uploadInWip, null, 'addReviewer'],
+ 2: ['upload'],
+ 3: ['upload', clearWip, setWip],
+ 4: ['upload'],
+ 5: ['upload', clearWip],
+ 6: [uploadInWip],
+ }).assertWip(1, true) // Change was created in WIP
+ .assertWip(2, true) // PS2 was uploaded during WIP
+ .assertWip(3, false) // PS3 was marked ready for review
+ .assertWip(4, true) // PS4 was uploaded during WIP
+ .assertWip(5, false) // PS5 was marked ready for review
+ .assertWip(6, true); // PS6 was uploaded with WIP option
+ });
+
+ test('patchNumEquals', () => {
+ const equals = Gerrit.PatchSetBehavior.patchNumEquals;
+ assert.isFalse(equals('edit', 'PARENT'));
+ assert.isFalse(equals('edit', NaN));
+ assert.isFalse(equals(1, '2'));
+
+ assert.isTrue(equals(1, '1'));
+ assert.isTrue(equals(1, 1));
+ assert.isTrue(equals('edit', 'edit'));
+ assert.isTrue(equals('PARENT', 'PARENT'));
+ });
+
+ test('isMergeParent', () => {
+ const isParent = Gerrit.PatchSetBehavior.isMergeParent;
+ assert.isFalse(isParent(1));
+ assert.isFalse(isParent(4321));
+ assert.isFalse(isParent('52'));
+ assert.isFalse(isParent('edit'));
+ assert.isFalse(isParent('PARENT'));
+ assert.isFalse(isParent(0));
+
+ assert.isTrue(isParent(-23));
+ assert.isTrue(isParent(-1));
+ assert.isTrue(isParent('-42'));
+ });
+
+ test('findEditParentRevision', () => {
+ const findParent = Gerrit.PatchSetBehavior.findEditParentRevision;
+ let revisions = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ assert.strictEqual(findParent(revisions), null);
+
+ revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
+ assert.strictEqual(findParent(revisions), null);
+
+ revisions = [...revisions, {_number: 3}];
+ assert.deepEqual(findParent(revisions), {_number: 3});
+ });
+
+ test('findEditParentPatchNum', () => {
+ const findNum = Gerrit.PatchSetBehavior.findEditParentPatchNum;
+ let revisions = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ assert.equal(findNum(revisions), -1);
+
+ revisions =
+ [...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
+ assert.deepEqual(findNum(revisions), 3);
+ });
+
+ test('sortRevisions', () => {
+ const sort = Gerrit.PatchSetBehavior.sortRevisions;
+ const revisions = [
+ {_number: 0},
+ {_number: 2},
+ {_number: 1},
+ ];
+ const sorted = [
+ {_number: 2},
+ {_number: 1},
+ {_number: 0},
+ ];
+
+ assert.deepEqual(sort(revisions), sorted);
+
+ // Edit patchset should follow directly after its basePatchNum.
+ revisions.push({_number: 'edit', basePatchNum: 2});
+ sorted.unshift({_number: 'edit', basePatchNum: 2});
+ assert.deepEqual(sort(revisions), sorted);
+
+ revisions[0].basePatchNum = 0;
+ const edit = sorted.shift();
+ edit.basePatchNum = 0;
+ // Edit patchset should be at index 2.
+ sorted.splice(2, 0, edit);
+ assert.deepEqual(sort(revisions), sorted);
+ });
+
+ test('getParentIndex', () => {
+ assert.equal(Gerrit.PatchSetBehavior.getParentIndex('-13'), 13);
+ assert.equal(Gerrit.PatchSetBehavior.getParentIndex(-4), 4);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
index 67e4ca6..9ec7c8e 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
@@ -1,21 +1,21 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
+import '../../scripts/util.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.
--->
-<script src="../../scripts/util.js"></script>
-<script>
(function(window) {
'use strict';
@@ -137,4 +137,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 740a8c8..8395ce9 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -15,89 +15,91 @@
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
-<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>
<title>gr-path-list-behavior</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<link rel="import" href="gr-path-list-behavior.html">
+<script type="module" src="./gr-path-list-behavior.js"></script>
-<script>
- suite('gr-path-list-behavior tests', async () => {
- await readyToTest();
- test('special sort', () => {
- const sort = Gerrit.PathListBehavior.specialFilePathCompare;
- const testFiles = [
- '/a.h',
- '/MERGE_LIST',
- '/a.cpp',
- '/COMMIT_MSG',
- '/asdasd',
- '/mrPeanutbutter.py',
- ];
- assert.deepEqual(
- testFiles.sort(sort),
- [
- '/COMMIT_MSG',
- '/MERGE_LIST',
- '/a.h',
- '/a.cpp',
- '/asdasd',
- '/mrPeanutbutter.py',
- ]);
- });
-
- test('file display name', () => {
- const name = Gerrit.PathListBehavior.computeDisplayPath;
- assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
- assert.equal(name('/foobarbaz'), '/foobarbaz');
- assert.equal(name('/COMMIT_MSG'), 'Commit message');
- assert.equal(name('/MERGE_LIST'), 'Merge list');
- });
-
- test('isMagicPath', () => {
- const isMagic = Gerrit.PathListBehavior.isMagicPath;
- assert.isFalse(isMagic(undefined));
- assert.isFalse(isMagic('/foo.cc'));
- assert.isTrue(isMagic('/COMMIT_MSG'));
- assert.isTrue(isMagic('/MERGE_LIST'));
- });
-
- test('truncatePath with long path should add ellipsis', () => {
- const truncatePath = Gerrit.PathListBehavior.truncatePath;
- let path = 'level1/level2/level3/level4/file.js';
- let shortenedPath = truncatePath(path);
- // The expected path is truncated with an ellipsis.
- const expectedPath = '\u2026/file.js';
- assert.equal(shortenedPath, expectedPath);
-
- path = 'level2/file.js';
- shortenedPath = truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
-
- test('truncatePath with opt_threshold', () => {
- const truncatePath = Gerrit.PathListBehavior.truncatePath;
- let path = 'level1/level2/level3/level4/file.js';
- let shortenedPath = truncatePath(path, 2);
- // The expected path is truncated with an ellipsis.
- const expectedPath = '\u2026/level4/file.js';
- assert.equal(shortenedPath, expectedPath);
-
- path = 'level2/file.js';
- shortenedPath = truncatePath(path, 2);
- assert.equal(shortenedPath, path);
- });
-
- test('truncatePath with short path should not add ellipsis', () => {
- const truncatePath = Gerrit.PathListBehavior.truncatePath;
- const path = 'file.js';
- const expectedPath = 'file.js';
- const shortenedPath = truncatePath(path);
- assert.equal(shortenedPath, expectedPath);
- });
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-path-list-behavior.js';
+suite('gr-path-list-behavior tests', () => {
+ test('special sort', () => {
+ const sort = Gerrit.PathListBehavior.specialFilePathCompare;
+ const testFiles = [
+ '/a.h',
+ '/MERGE_LIST',
+ '/a.cpp',
+ '/COMMIT_MSG',
+ '/asdasd',
+ '/mrPeanutbutter.py',
+ ];
+ assert.deepEqual(
+ testFiles.sort(sort),
+ [
+ '/COMMIT_MSG',
+ '/MERGE_LIST',
+ '/a.h',
+ '/a.cpp',
+ '/asdasd',
+ '/mrPeanutbutter.py',
+ ]);
});
+
+ test('file display name', () => {
+ const name = Gerrit.PathListBehavior.computeDisplayPath;
+ assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
+ assert.equal(name('/foobarbaz'), '/foobarbaz');
+ assert.equal(name('/COMMIT_MSG'), 'Commit message');
+ assert.equal(name('/MERGE_LIST'), 'Merge list');
+ });
+
+ test('isMagicPath', () => {
+ const isMagic = Gerrit.PathListBehavior.isMagicPath;
+ assert.isFalse(isMagic(undefined));
+ assert.isFalse(isMagic('/foo.cc'));
+ assert.isTrue(isMagic('/COMMIT_MSG'));
+ assert.isTrue(isMagic('/MERGE_LIST'));
+ });
+
+ test('truncatePath with long path should add ellipsis', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
+
+ test('truncatePath with opt_threshold', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path, 2);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/level4/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path, 2);
+ assert.equal(shortenedPath, path);
+ });
+
+ test('truncatePath with short path should not add ellipsis', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ const path = 'file.js';
+ const expectedPath = 'file.js';
+ const shortenedPath = truncatePath(path);
+ assert.equal(shortenedPath, expectedPath);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
index 69ebf23..3758b79 100644
--- a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -52,4 +51,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
deleted file mode 100644
index 0e2e99f..0000000
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
+++ /dev/null
@@ -1,21 +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">
-<link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
-<script src="../../scripts/rootElement.js"></script>
-
-<script src="gr-tooltip-behavior.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index b65c63b..481642b 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -14,6 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../scripts/bundled-polymer.js';
+
+import '../../elements/shared/gr-tooltip/gr-tooltip.js';
+import '../../scripts/rootElement.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
(function(window) {
'use strict';
@@ -135,7 +141,7 @@
_positionTooltip(tooltip) {
// This flush is needed for tooltips to be positioned correctly in Firefox
// and Safari.
- Polymer.dom.flush();
+ flush();
const rect = this.getBoundingClientRect();
const boxRect = tooltip.getBoundingClientRect();
const parentRect = tooltip.parentElement.getBoundingClientRect();
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
index e0218ad..7387a17 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -18,15 +18,20 @@
<title>tooltip-behavior</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-tooltip-behavior.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-tooltip-behavior.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-tooltip-behavior.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,124 +39,127 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip-behavior 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-tooltip-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-tooltip-behavior tests', () => {
+ let element;
+ let sandbox;
- function makeTooltip(tooltipRect, parentRect) {
- return {
- getBoundingClientRect() { return tooltipRect; },
- updateStyles: sinon.stub(),
- style: {left: 0, top: 0},
- parentElement: {
- getBoundingClientRect() { return parentRect; },
- },
- };
- }
+ function makeTooltip(tooltipRect, parentRect) {
+ return {
+ getBoundingClientRect() { return tooltipRect; },
+ updateStyles: sinon.stub(),
+ style: {left: 0, top: 0},
+ parentElement: {
+ getBoundingClientRect() { return parentRect; },
+ },
+ };
+ }
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'tooltip-behavior-element',
- behaviors: [Gerrit.TooltipBehavior],
- });
- });
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('normal position', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 100, width: 200};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 50},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isFalse(tooltip.updateStyles.called);
- assert.equal(tooltip.style.left, '175px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('left side position', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 10, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '0px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('right side position', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 950, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('position to bottom', () => {
- sandbox.stub(element, 'getBoundingClientRect', () => {
- return {top: 100, left: 950, width: 50, height: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element.positionBelow = true;
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '157.2px');
- });
-
- test('hides tooltip when detached', () => {
- sandbox.stub(element, '_handleHideTooltip');
- element.remove();
- flushAsynchronousOperations();
- assert.isTrue(element._handleHideTooltip.called);
- });
-
- test('sets up listeners when has-tooltip is changed', () => {
- const addListenerStub = sandbox.stub(element, 'addEventListener');
- element.hasTooltip = true;
- assert.isTrue(addListenerStub.called);
- });
-
- test('clean up listeners when has-tooltip changed to false', () => {
- const removeListenerStub = sandbox.stub(element, 'removeEventListener');
- element.hasTooltip = true;
- element.hasTooltip = false;
- assert.isTrue(removeListenerStub.called);
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'tooltip-behavior-element',
+ behaviors: [Gerrit.TooltipBehavior],
});
});
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('normal position', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 100, width: 200};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 50},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isFalse(tooltip.updateStyles.called);
+ assert.equal(tooltip.style.left, '175px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('left side position', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 10, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '0px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('right side position', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 950, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('position to bottom', () => {
+ sandbox.stub(element, 'getBoundingClientRect', () => {
+ return {top: 100, left: 950, width: 50, height: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element.positionBelow = true;
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '157.2px');
+ });
+
+ test('hides tooltip when detached', () => {
+ sandbox.stub(element, '_handleHideTooltip');
+ element.remove();
+ flushAsynchronousOperations();
+ assert.isTrue(element._handleHideTooltip.called);
+ });
+
+ test('sets up listeners when has-tooltip is changed', () => {
+ const addListenerStub = sandbox.stub(element, 'addEventListener');
+ element.hasTooltip = true;
+ assert.isTrue(addListenerStub.called);
+ });
+
+ test('clean up listeners when has-tooltip changed to false', () => {
+ const removeListenerStub = sandbox.stub(element, 'removeEventListener');
+ element.hasTooltip = true;
+ element.hasTooltip = false;
+ assert.isTrue(removeListenerStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
index 0396c4f..8b139da 100644
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@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.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
(function(window) {
'use strict';
@@ -73,4 +72,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
index b7a1d92..94d6c12 100644
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.html
@@ -18,15 +18,20 @@
<title>gr-url-encoding-behavior</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-url-encoding-behavior.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-url-encoding-behavior.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-url-encoding-behavior.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,62 +39,65 @@
</template>
</test-fixture>
-<script>
- suite('gr-url-encoding-behavior 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-url-encoding-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-url-encoding-behavior tests', () => {
+ let element;
+ let sandbox;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.URLEncodingBehavior],
- });
- });
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- suite('encodeURL', () => {
- test('double encodes', () => {
- assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
- assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
- assert.equal(element.encodeURL('jkl'), 'jkl');
- assert.equal(element.encodeURL(''), '');
- });
-
- test('does not convert colons', () => {
- assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
- });
-
- test('converts spaces to +', () => {
- assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
- });
-
- test('does not convert slashes when configured', () => {
- assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
- });
-
- test('does not convert slashes when configured', () => {
- assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
- });
- });
-
- suite('singleDecodeUrl', () => {
- test('single decodes', () => {
- assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
- });
-
- test('converts + to space', () => {
- assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.URLEncodingBehavior],
});
});
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('encodeURL', () => {
+ test('double encodes', () => {
+ assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
+ assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
+ assert.equal(element.encodeURL('jkl'), 'jkl');
+ assert.equal(element.encodeURL(''), '');
+ });
+
+ test('does not convert colons', () => {
+ assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
+ });
+
+ test('converts spaces to +', () => {
+ assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
+ });
+
+ test('does not convert slashes when configured', () => {
+ assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
+ });
+
+ test('does not convert slashes when configured', () => {
+ assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
+ });
+ });
+
+ suite('singleDecodeUrl', () => {
+ test('single decodes', () => {
+ assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
+ });
+
+ test('converts + to space', () => {
+ assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index a22c3ba..360a85b 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -1,21 +1,20 @@
-<!--
-@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.
--->
-
-<!--
+/**
+ * @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.
+ */
+/*
How to Add a Keyboard Shortcut
==============================
@@ -95,12 +94,17 @@
NOTE: doc-only shortcuts will not be customizable in the same way that other
shortcuts are.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
-<script src="../../types/polymer-behaviors.js"></script>
+*/
+/*
+ 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 '../../scripts/bundled-polymer.js';
-<script>
+import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import '../../types/polymer-behaviors.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
(function(window) {
'use strict';
@@ -307,7 +311,7 @@
/** @return {!(Event|PolymerDomApi|PolymerEventApi)} */
const getKeyboardEvent = function(e) {
- e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e);
+ e = dom(e.detail ? e.detail.keyboardEvent : e);
// When e is a keyboardEvent, e.event is not null.
if (e.event) { e = e.event; }
return e;
@@ -482,7 +486,7 @@
/** @polymerBehavior Gerrit.KeyboardShortcutBehavior*/
Gerrit.KeyboardShortcutBehavior = [
- Polymer.IronA11yKeysBehavior,
+ IronA11yKeysBehavior,
{
// Exports for convenience. Note: Closure compiler crashes when
// object-shorthand syntax is used here.
@@ -518,7 +522,7 @@
shouldSuppressKeyboardShortcut(e) {
e = getKeyboardEvent(e);
- const tagName = Polymer.dom(e).rootTarget.tagName;
+ const tagName = dom(e).rootTarget.tagName;
if (tagName === 'INPUT' || tagName === 'TEXTAREA' ||
(e.keyCode === 13 && tagName === 'A')) {
// Suppress shortcuts if the key is 'enter' and target is an anchor.
@@ -542,7 +546,7 @@
},
getRootTarget(e) {
- return Polymer.dom(getKeyboardEvent(e)).rootTarget;
+ return dom(getKeyboardEvent(e)).rootTarget;
},
bindShortcut(shortcut, ...bindings) {
@@ -671,4 +675,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index e7407f7..f7231fb 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</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="keyboard-shortcut-behavior.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="./keyboard-shortcut-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -41,403 +41,406 @@
</template>
</test-fixture>
-<script>
- suite('keyboard-shortcut-behavior tests', async () => {
- await readyToTest();
- const kb = window.Gerrit.KeyboardShortcutBinder;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './keyboard-shortcut-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('keyboard-shortcut-behavior tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
- let element;
- let overlay;
- let sandbox;
+ let element;
+ let overlay;
+ let sandbox;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [Gerrit.KeyboardShortcutBehavior],
- keyBindings: {
- k: '_handleKey',
- enter: '_handleKey',
- },
- _handleKey() {},
- });
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [Gerrit.KeyboardShortcutBehavior],
+ keyBindings: {
+ k: '_handleKey',
+ enter: '_handleKey',
+ },
+ _handleKey() {},
+ });
+ });
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('ShortcutManager', () => {
+ test('bindings management', () => {
+ const mgr = new kb.ShortcutManager();
+ const {NEXT_FILE} = kb.Shortcut;
+
+ assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
+ mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
+ assert.deepEqual(
+ mgr.getBindingsForShortcut(NEXT_FILE),
+ [']', '}', 'right']);
});
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- sandbox = sinon.sandbox.create();
- });
+ suite('binding descriptions', () => {
+ function mapToObject(m) {
+ const o = {};
+ m.forEach((v, k) => o[k] = v);
+ return o;
+ }
- teardown(() => {
- sandbox.restore();
- });
-
- suite('ShortcutManager', () => {
- test('bindings management', () => {
+ test('single combo description', () => {
const mgr = new kb.ShortcutManager();
- const {NEXT_FILE} = kb.Shortcut;
-
- assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
- mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
+ assert.deepEqual(mgr.describeBinding('a'), ['a']);
+ assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
+ assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
assert.deepEqual(
- mgr.getBindingsForShortcut(NEXT_FILE),
- [']', '}', 'right']);
+ mgr.describeBinding('ctrl+shift+up:keyup'),
+ ['Ctrl', 'Shift', '↑']);
});
- suite('binding descriptions', () => {
- function mapToObject(m) {
- const o = {};
- m.forEach((v, k) => o[k] = v);
- return o;
- }
+ test('combo set description', () => {
+ const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
+ const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
- test('single combo description', () => {
- const mgr = new kb.ShortcutManager();
- assert.deepEqual(mgr.describeBinding('a'), ['a']);
- assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
- assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
- assert.deepEqual(
- mgr.describeBinding('ctrl+shift+up:keyup'),
- ['Ctrl', 'Shift', '↑']);
+ const mgr = new ShortcutManager();
+ assert.isNull(mgr.describeBindings(NEXT_FILE));
+
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
+ assert.deepEqual(
+ mgr.describeBindings(GO_TO_OPENED_CHANGES),
+ [['g', 'o']]);
+
+ mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
+ assert.deepEqual(
+ mgr.describeBindings(NEXT_FILE),
+ [[']'], ['Ctrl', 'Shift', '→']]);
+
+ mgr.bindShortcut(PREV_FILE, '[');
+ assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
+ });
+
+ test('combo set description width', () => {
+ const mgr = new kb.ShortcutManager();
+ assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
+ assert.strictEqual(
+ mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
+ 12);
+ });
+
+ test('distribute shortcut help', () => {
+ const mgr = new kb.ShortcutManager();
+ assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([['g', 'o']]),
+ [[['g', 'o']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
+ [[['ctrl', 'shift', 'meta', 'enter']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([
+ ['ctrl', 'shift', 'meta', 'enter'],
+ ['o'],
+ ]),
+ [
+ [['ctrl', 'shift', 'meta', 'enter']],
+ [['o']],
+ ]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([
+ ['ctrl', 'enter'],
+ ['meta', 'enter'],
+ ['ctrl', 's'],
+ ['meta', 's'],
+ ]),
+ [
+ [['ctrl', 'enter'], ['meta', 'enter']],
+ [['ctrl', 's'], ['meta', 's']],
+ ]);
+ });
+
+ test('active shortcuts by section', () => {
+ const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
+ kb.Shortcut;
+ const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
+
+ const mgr = new kb.ShortcutManager();
+ mgr.bindShortcut(NEXT_FILE, ']');
+ mgr.bindShortcut(NEXT_LINE, 'j');
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
+ mgr.bindShortcut(SEARCH, '/');
+
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {});
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [NEXT_FILE]: null,
+ };
+ },
});
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Go to next file'},
+ ],
+ });
- test('combo set description', () => {
- const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
- const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
-
- const mgr = new ShortcutManager();
- assert.isNull(mgr.describeBindings(NEXT_FILE));
-
- mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
- assert.deepEqual(
- mgr.describeBindings(GO_TO_OPENED_CHANGES),
- [['g', 'o']]);
-
- mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
- assert.deepEqual(
- mgr.describeBindings(NEXT_FILE),
- [[']'], ['Ctrl', 'Shift', '→']]);
-
- mgr.bindShortcut(PREV_FILE, '[');
- assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [NEXT_LINE]: null,
+ };
+ },
});
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [DIFFS]: [
+ {shortcut: NEXT_LINE, text: 'Go to next line'},
+ ],
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Go to next file'},
+ ],
+ });
- test('combo set description width', () => {
- const mgr = new kb.ShortcutManager();
- assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
- assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
- assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
- assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
- assert.strictEqual(
- mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
- 12);
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [SEARCH]: null,
+ [GO_TO_OPENED_CHANGES]: null,
+ };
+ },
});
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [DIFFS]: [
+ {shortcut: NEXT_LINE, text: 'Go to next line'},
+ ],
+ [EVERYWHERE]: [
+ {shortcut: SEARCH, text: 'Search'},
+ {
+ shortcut: GO_TO_OPENED_CHANGES,
+ text: 'Go to Opened Changes',
+ },
+ ],
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Go to next file'},
+ ],
+ });
+ });
- test('distribute shortcut help', () => {
- const mgr = new kb.ShortcutManager();
- assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['g', 'o']]),
- [[['g', 'o']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
- [[['ctrl', 'shift', 'meta', 'enter']]]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'shift', 'meta', 'enter'],
- ['o'],
- ]),
- [
- [['ctrl', 'shift', 'meta', 'enter']],
- [['o']],
- ]);
- assert.deepEqual(
- mgr.distributeBindingDesc([
- ['ctrl', 'enter'],
- ['meta', 'enter'],
- ['ctrl', 's'],
- ['meta', 's'],
- ]),
- [
- [['ctrl', 'enter'], ['meta', 'enter']],
- [['ctrl', 's'], ['meta', 's']],
- ]);
+ test('directory view', () => {
+ const {
+ NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
+ SAVE_COMMENT,
+ } = kb.Shortcut;
+ const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
+ const {GO_KEY, ShortcutManager} = kb;
+
+ const mgr = new ShortcutManager();
+ mgr.bindShortcut(NEXT_FILE, ']');
+ mgr.bindShortcut(NEXT_LINE, 'j');
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
+ mgr.bindShortcut(SEARCH, '/');
+ mgr.bindShortcut(
+ SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+
+ assert.deepEqual(mapToObject(mgr.directoryView()), {});
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [GO_TO_OPENED_CHANGES]: null,
+ [NEXT_FILE]: null,
+ [NEXT_LINE]: null,
+ [SAVE_COMMENT]: null,
+ [SEARCH]: null,
+ };
+ },
});
-
- test('active shortcuts by section', () => {
- const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
- kb.Shortcut;
- const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
-
- const mgr = new kb.ShortcutManager();
- mgr.bindShortcut(NEXT_FILE, ']');
- mgr.bindShortcut(NEXT_LINE, 'j');
- mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
- mgr.bindShortcut(SEARCH, '/');
-
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {});
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [NEXT_FILE]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [NAVIGATION]: [
- {shortcut: NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [NEXT_LINE]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [DIFFS]: [
- {shortcut: NEXT_LINE, text: 'Go to next line'},
- ],
- [NAVIGATION]: [
- {shortcut: NEXT_FILE, text: 'Go to next file'},
- ],
- });
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [SEARCH]: null,
- [GO_TO_OPENED_CHANGES]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.activeShortcutsBySection()),
- {
- [DIFFS]: [
- {shortcut: NEXT_LINE, text: 'Go to next line'},
- ],
- [EVERYWHERE]: [
- {shortcut: SEARCH, text: 'Search'},
- {
- shortcut: GO_TO_OPENED_CHANGES,
- text: 'Go to Opened Changes',
- },
- ],
- [NAVIGATION]: [
- {shortcut: NEXT_FILE, text: 'Go to next file'},
- ],
- });
- });
-
- test('directory view', () => {
- const {
- NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
- SAVE_COMMENT,
- } = kb.Shortcut;
- const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
- const {GO_KEY, ShortcutManager} = kb;
-
- const mgr = new ShortcutManager();
- mgr.bindShortcut(NEXT_FILE, ']');
- mgr.bindShortcut(NEXT_LINE, 'j');
- mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
- mgr.bindShortcut(SEARCH, '/');
- mgr.bindShortcut(
- SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
-
- assert.deepEqual(mapToObject(mgr.directoryView()), {});
-
- mgr.attachHost({
- keyboardShortcuts() {
- return {
- [GO_TO_OPENED_CHANGES]: null,
- [NEXT_FILE]: null,
- [NEXT_LINE]: null,
- [SAVE_COMMENT]: null,
- [SEARCH]: null,
- };
- },
- });
- assert.deepEqual(
- mapToObject(mgr.directoryView()),
- {
- [DIFFS]: [
- {binding: [['j']], text: 'Go to next line'},
- {
- binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
- text: 'Save comment',
- },
- {
- binding: [['Ctrl', 's'], ['Meta', 's']],
- text: 'Save comment',
- },
- ],
- [EVERYWHERE]: [
- {binding: [['/']], text: 'Search'},
- {binding: [['g', 'o']], text: 'Go to Opened Changes'},
- ],
- [NAVIGATION]: [
- {binding: [[']']], text: 'Go to next file'},
- ],
- });
- });
- });
- });
-
- test('doesn’t block kb shortcuts for non-whitelisted els', done => {
- const divEl = document.createElement('div');
- element.appendChild(divEl);
- element._handleKey = e => {
- assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- });
-
- test('blocks kb shortcuts for input els', done => {
- const inputEl = document.createElement('input');
- element.appendChild(inputEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(inputEl, 75, null, 'k');
- });
-
- test('blocks kb shortcuts for textarea els', done => {
- const textareaEl = document.createElement('textarea');
- element.appendChild(textareaEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
- });
-
- test('blocks kb shortcuts for anything in a gr-overlay', done => {
- const divEl = document.createElement('div');
- const element = overlay.querySelector('test-element');
- element.appendChild(divEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(divEl, 75, null, 'k');
- });
-
- test('blocks enter shortcut on an anchor', done => {
- const anchorEl = document.createElement('a');
- const element = overlay.querySelector('test-element');
- element.appendChild(anchorEl);
- element._handleKey = e => {
- assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
- done();
- };
- MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
- });
-
- test('modifierPressed returns accurate values', () => {
- const spy = sandbox.spy(element, 'modifierPressed');
- element._handleKey = e => {
- element.modifierPressed(e);
- };
- MockInteractions.keyDownOn(element, 75, 'shift', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'meta', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'alt', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- });
-
- test('isModifierPressed returns accurate value', () => {
- const spy = sandbox.spy(element, 'isModifierPressed');
- element._handleKey = e => {
- element.isModifierPressed(e, 'shiftKey');
- };
- MockInteractions.keyDownOn(element, 75, 'shift', 'k');
- assert.isTrue(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'meta', 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, null, 'k');
- assert.isFalse(spy.lastCall.returnValue);
- MockInteractions.keyDownOn(element, 75, 'alt', 'k');
- assert.isFalse(spy.lastCall.returnValue);
- });
-
- suite('GO_KEY timing', () => {
- let handlerStub;
-
- setup(() => {
- element._shortcut_go_table.set('a', '_handleA');
- handlerStub = element._handleA = sinon.stub();
- sandbox.stub(Date, 'now').returns(10000);
- });
-
- test('success', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isTrue(handlerStub.calledOnce);
- assert.strictEqual(handlerStub.lastCall.args[0], e);
- });
-
- test('go key not pressed', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = null;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('go key pressed too long ago', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 3000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('should suppress', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
- });
-
- test('unrecognized key', () => {
- const e = {detail: {key: 'f'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._shortcut_go_key_last_pressed = 9000;
- element._handleGoAction(e);
- assert.isFalse(handlerStub.called);
+ assert.deepEqual(
+ mapToObject(mgr.directoryView()),
+ {
+ [DIFFS]: [
+ {binding: [['j']], text: 'Go to next line'},
+ {
+ binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
+ text: 'Save comment',
+ },
+ {
+ binding: [['Ctrl', 's'], ['Meta', 's']],
+ text: 'Save comment',
+ },
+ ],
+ [EVERYWHERE]: [
+ {binding: [['/']], text: 'Search'},
+ {binding: [['g', 'o']], text: 'Go to Opened Changes'},
+ ],
+ [NAVIGATION]: [
+ {binding: [[']']], text: 'Go to next file'},
+ ],
+ });
});
});
});
+
+ test('doesn’t block kb shortcuts for non-whitelisted els', done => {
+ const divEl = document.createElement('div');
+ element.appendChild(divEl);
+ element._handleKey = e => {
+ assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(divEl, 75, null, 'k');
+ });
+
+ test('blocks kb shortcuts for input els', done => {
+ const inputEl = document.createElement('input');
+ element.appendChild(inputEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(inputEl, 75, null, 'k');
+ });
+
+ test('blocks kb shortcuts for textarea els', done => {
+ const textareaEl = document.createElement('textarea');
+ element.appendChild(textareaEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
+ });
+
+ test('blocks kb shortcuts for anything in a gr-overlay', done => {
+ const divEl = document.createElement('div');
+ const element = overlay.querySelector('test-element');
+ element.appendChild(divEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(divEl, 75, null, 'k');
+ });
+
+ test('blocks enter shortcut on an anchor', done => {
+ const anchorEl = document.createElement('a');
+ const element = overlay.querySelector('test-element');
+ element.appendChild(anchorEl);
+ element._handleKey = e => {
+ assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+ done();
+ };
+ MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
+ });
+
+ test('modifierPressed returns accurate values', () => {
+ const spy = sandbox.spy(element, 'modifierPressed');
+ element._handleKey = e => {
+ element.modifierPressed(e);
+ };
+ MockInteractions.keyDownOn(element, 75, 'shift', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'meta', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'alt', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ });
+
+ test('isModifierPressed returns accurate value', () => {
+ const spy = sandbox.spy(element, 'isModifierPressed');
+ element._handleKey = e => {
+ element.isModifierPressed(e, 'shiftKey');
+ };
+ MockInteractions.keyDownOn(element, 75, 'shift', 'k');
+ assert.isTrue(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'meta', 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, null, 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ MockInteractions.keyDownOn(element, 75, 'alt', 'k');
+ assert.isFalse(spy.lastCall.returnValue);
+ });
+
+ suite('GO_KEY timing', () => {
+ let handlerStub;
+
+ setup(() => {
+ element._shortcut_go_table.set('a', '_handleA');
+ handlerStub = element._handleA = sinon.stub();
+ sandbox.stub(Date, 'now').returns(10000);
+ });
+
+ test('success', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isTrue(handlerStub.calledOnce);
+ assert.strictEqual(handlerStub.lastCall.args[0], e);
+ });
+
+ test('go key not pressed', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = null;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('go key pressed too long ago', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 3000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('should suppress', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('unrecognized key', () => {
+ const e = {detail: {key: 'f'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
index 709cc8a..d52ffbf 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
@@ -1,22 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
+import '../../scripts/bundled-polymer.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="../base-url-behavior/base-url-behavior.html">
-<script>
+import '../base-url-behavior/base-url-behavior.js';
(function(window) {
'use strict';
@@ -198,4 +198,3 @@
};
}
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 4bcd928..af078e5 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -19,19 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</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>
- /** @type {string} */
- window.CANONICAL_PATH = '/r';
+<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">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../base-url-behavior/base-url-behavior.js';
+import './rest-client-behavior.js';
+/** @type {string} */
+window.CANONICAL_PATH = '/r';
</script>
-<link rel="import" href="../base-url-behavior/base-url-behavior.html">
-<link rel="import" href="rest-client-behavior.html">
+<script type="module" src="../base-url-behavior/base-url-behavior.js"></script>
+<script type="module" src="./rest-client-behavior.js"></script>
<test-fixture id="basic">
<template>
@@ -47,191 +51,195 @@
</template>
</test-fixture>
-<script>
- suite('rest-client-behavior tests', async () => {
- await readyToTest();
- let element;
- // eslint-disable-next-line no-unused-vars
- let overlay;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../base-url-behavior/base-url-behavior.js';
+import './rest-client-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('rest-client-behavior tests', () => {
+ let element;
+ // eslint-disable-next-line no-unused-vars
+ let overlay;
- suiteSetup(() => {
- // Define a Polymer element that uses this behavior.
- Polymer({
- is: 'test-element',
- behaviors: [
- Gerrit.BaseUrlBehavior,
- Gerrit.RESTClientBehavior,
- ],
- });
- });
-
- setup(() => {
- element = fixture('basic');
- overlay = fixture('within-overlay');
- });
-
- test('changeBaseURL', () => {
- assert.deepEqual(
- element.changeBaseURL('test/project', '1', '2'),
- '/r/changes/test%2Fproject~1/revisions/2'
- );
- });
-
- test('changePath', () => {
- assert.deepEqual(element.changePath('1'), '/r/c/1');
- });
-
- test('Open status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
- };
- let statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, []);
- assert.equal(statusString, '');
-
- change.submittable = false;
- statuses = element.changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Active']);
-
- // With no missing labels but no submitEnabled option.
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Active']);
-
- // Without missing labels and enabled submit
- statuses = element.changeStatuses(change,
- {includeDerived: true, submitEnabled: true});
- assert.deepEqual(statuses, ['Ready to submit']);
-
- change.mergeable = false;
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true});
- assert.deepEqual(statuses, ['Merge Conflict']);
-
- delete change.mergeable;
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true, mergeable: true, submitEnabled: true});
- assert.deepEqual(statuses, ['Ready to submit']);
-
- change.submittable = true;
- statuses = element.changeStatuses(change,
- {includeDerived: true, mergeable: false});
- assert.deepEqual(statuses, ['Merge Conflict']);
- });
-
- test('Merge conflict', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: false,
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Merge Conflict']);
- assert.equal(statusString, 'Merge Conflict');
- });
-
- test('mergeable prop undefined', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, []);
- assert.equal(statusString, '');
- });
-
- test('Merged status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'MERGED',
- labels: {},
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Merged']);
- assert.equal(statusString, 'Merged');
- });
-
- test('Abandoned status', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'ABANDONED',
- labels: {},
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Abandoned']);
- assert.equal(statusString, 'Abandoned');
- });
-
- test('Open status with private and wip', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- is_private: true,
- work_in_progress: true,
- labels: {},
- mergeable: true,
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['WIP', 'Private']);
- assert.equal(statusString, 'WIP, Private');
- });
-
- test('Merge conflict with private and wip', () => {
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1},
- },
- current_revision: 'rev1',
- status: 'NEW',
- is_private: true,
- work_in_progress: true,
- labels: {},
- mergeable: false,
- };
- const statuses = element.changeStatuses(change);
- const statusString = element.changeStatusString(change);
- assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
- assert.equal(statusString, 'Merge Conflict, WIP, Private');
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.RESTClientBehavior,
+ ],
});
});
+
+ setup(() => {
+ element = fixture('basic');
+ overlay = fixture('within-overlay');
+ });
+
+ test('changeBaseURL', () => {
+ assert.deepEqual(
+ element.changeBaseURL('test/project', '1', '2'),
+ '/r/changes/test%2Fproject~1/revisions/2'
+ );
+ });
+
+ test('changePath', () => {
+ assert.deepEqual(element.changePath('1'), '/r/c/1');
+ });
+
+ test('Open status', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ let statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, []);
+ assert.equal(statusString, '');
+
+ change.submittable = false;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true});
+ assert.deepEqual(statuses, ['Active']);
+
+ // With no missing labels but no submitEnabled option.
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true});
+ assert.deepEqual(statuses, ['Active']);
+
+ // Without missing labels and enabled submit
+ statuses = element.changeStatuses(change,
+ {includeDerived: true, submitEnabled: true});
+ assert.deepEqual(statuses, ['Ready to submit']);
+
+ change.mergeable = false;
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true});
+ assert.deepEqual(statuses, ['Merge Conflict']);
+
+ delete change.mergeable;
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true, mergeable: true, submitEnabled: true});
+ assert.deepEqual(statuses, ['Ready to submit']);
+
+ change.submittable = true;
+ statuses = element.changeStatuses(change,
+ {includeDerived: true, mergeable: false});
+ assert.deepEqual(statuses, ['Merge Conflict']);
+ });
+
+ test('Merge conflict', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: false,
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Merge Conflict']);
+ assert.equal(statusString, 'Merge Conflict');
+ });
+
+ test('mergeable prop undefined', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, []);
+ assert.equal(statusString, '');
+ });
+
+ test('Merged status', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'MERGED',
+ labels: {},
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Merged']);
+ assert.equal(statusString, 'Merged');
+ });
+
+ test('Abandoned status', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'ABANDONED',
+ labels: {},
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Abandoned']);
+ assert.equal(statusString, 'Abandoned');
+ });
+
+ test('Open status with private and wip', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ is_private: true,
+ work_in_progress: true,
+ labels: {},
+ mergeable: true,
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['WIP', 'Private']);
+ assert.equal(statusString, 'WIP, Private');
+ });
+
+ test('Merge conflict with private and wip', () => {
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ is_private: true,
+ work_in_progress: true,
+ labels: {},
+ mergeable: false,
+ };
+ const statuses = element.changeStatuses(change);
+ const statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
+ assert.equal(statusString, 'Merge Conflict, WIP, Private');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
index 8f08f0c..23f2290 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
@@ -1,20 +1,19 @@
-<!--
-@license
-Copyright (C) 2018 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.
--->
-<script>
+/**
+ * @license
+ * Copyright (C) 2018 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';
@@ -74,4 +73,3 @@
throw new Error(`Refused to bind value as ${type}: ${value}`);
};
})(window);
-</script>
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
index e123c96..fc1224f 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.html
@@ -18,15 +18,20 @@
<title>safe-types-behavior</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="safe-types-behavior.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="./safe-types-behavior.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './safe-types-behavior.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,92 +39,95 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip-behavior tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './safe-types-behavior.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+suite('gr-tooltip-behavior tests', () => {
+ let element;
+ let sandbox;
- suiteSetup(() => {
- Polymer({
- is: 'safe-types-element',
- behaviors: [Gerrit.SafeTypes],
- });
- });
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('SafeUrl accepts valid urls', () => {
- function accepts(url) {
- const safeUrl = new element.SafeUrl(url);
- assert.isOk(safeUrl);
- assert.equal(url, safeUrl.asString());
- }
- accepts('http://www.google.com/');
- accepts('https://www.google.com/');
- accepts('HtTpS://www.google.com/');
- accepts('//www.google.com/');
- accepts('/c/1234/file/path.html@45');
- accepts('#hash-url');
- accepts('mailto:name@example.com');
- });
-
- test('SafeUrl rejects invalid urls', () => {
- function rejects(url) {
- assert.throws(() => { new element.SafeUrl(url); });
- }
- rejects('javascript://alert("evil");');
- rejects('ftp:example.com');
- rejects('data:text/html,scary business');
- });
-
- suite('safeTypesBridge', () => {
- function acceptsString(value, type) {
- assert.equal(Gerrit.SafeTypes.safeTypesBridge(value, type),
- value);
- }
-
- function rejects(value, type) {
- assert.throws(() => { Gerrit.SafeTypes.safeTypesBridge(value, type); });
- }
-
- test('accepts valid URL strings', () => {
- acceptsString('/foo/bar', 'URL');
- acceptsString('#baz', 'URL');
- });
-
- test('rejects invalid URL strings', () => {
- rejects('javascript://void();', 'URL');
- });
-
- test('accepts SafeUrl values', () => {
- const url = '/abc/123';
- const safeUrl = new element.SafeUrl(url);
- assert.equal(Gerrit.SafeTypes.safeTypesBridge(safeUrl, 'URL'), url);
- });
-
- test('rejects non-string or non-SafeUrl types', () => {
- rejects(3.1415926, 'URL');
- });
-
- test('accepts any binding to STRING or CONSTANT', () => {
- acceptsString('foo/bar/baz', 'STRING');
- acceptsString('lorem ipsum dolor', 'CONSTANT');
- });
-
- test('rejects all other types', () => {
- rejects('foo', 'JAVASCRIPT');
- rejects('foo', 'HTML');
- rejects('foo', 'RESOURCE_URL');
- rejects('foo', 'STYLE');
- });
+ suiteSetup(() => {
+ Polymer({
+ is: 'safe-types-element',
+ behaviors: [Gerrit.SafeTypes],
});
});
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('SafeUrl accepts valid urls', () => {
+ function accepts(url) {
+ const safeUrl = new element.SafeUrl(url);
+ assert.isOk(safeUrl);
+ assert.equal(url, safeUrl.asString());
+ }
+ accepts('http://www.google.com/');
+ accepts('https://www.google.com/');
+ accepts('HtTpS://www.google.com/');
+ accepts('//www.google.com/');
+ accepts('/c/1234/file/path.html@45');
+ accepts('#hash-url');
+ accepts('mailto:name@example.com');
+ });
+
+ test('SafeUrl rejects invalid urls', () => {
+ function rejects(url) {
+ assert.throws(() => { new element.SafeUrl(url); });
+ }
+ rejects('javascript://alert("evil");');
+ rejects('ftp:example.com');
+ rejects('data:text/html,scary business');
+ });
+
+ suite('safeTypesBridge', () => {
+ function acceptsString(value, type) {
+ assert.equal(Gerrit.SafeTypes.safeTypesBridge(value, type),
+ value);
+ }
+
+ function rejects(value, type) {
+ assert.throws(() => { Gerrit.SafeTypes.safeTypesBridge(value, type); });
+ }
+
+ test('accepts valid URL strings', () => {
+ acceptsString('/foo/bar', 'URL');
+ acceptsString('#baz', 'URL');
+ });
+
+ test('rejects invalid URL strings', () => {
+ rejects('javascript://void();', 'URL');
+ });
+
+ test('accepts SafeUrl values', () => {
+ const url = '/abc/123';
+ const safeUrl = new element.SafeUrl(url);
+ assert.equal(Gerrit.SafeTypes.safeTypesBridge(safeUrl, 'URL'), url);
+ });
+
+ test('rejects non-string or non-SafeUrl types', () => {
+ rejects(3.1415926, 'URL');
+ });
+
+ test('accepts any binding to STRING or CONSTANT', () => {
+ acceptsString('foo/bar/baz', 'STRING');
+ acceptsString('lorem ipsum dolor', 'CONSTANT');
+ });
+
+ test('rejects all other types', () => {
+ rejects('foo', 'JAVASCRIPT');
+ rejects('foo', 'HTML');
+ rejects('foo', 'RESOURCE_URL');
+ rejects('foo', 'STYLE');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index a421043..cfb28bb 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -14,291 +14,308 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-permission/gr-permission.js';
+import '../../../scripts/util.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.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 {htmlTemplate} from './gr-access-section_html.js';
+
+/**
+ * Fired when the section has been modified or removed.
+ *
+ * @event access-modified
+ */
+
+/**
+ * Fired when a section that was previously added was removed.
+ *
+ * @event added-section-removed
+ */
+
+const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
+
+// The name that gets automatically input when a new reference is added.
+const NEW_NAME = 'refs/heads/*';
+const REFS_NAME = 'refs/';
+const ON_BEHALF_OF = '(On Behalf Of)';
+const LABEL = 'Label';
+
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccessSection extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
/**
- * Fired when the section has been modified or removed.
- *
- * @event access-modified
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-repo-access_test.
*/
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * Fired when a section that was previously added was removed.
- *
- * @event added-section-removed
- */
+ static get is() { return 'gr-access-section'; }
- const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
+ static get properties() {
+ return {
+ capabilities: Object,
+ /** @type {?} */
+ section: {
+ type: Object,
+ notify: true,
+ observer: '_updateSection',
+ },
+ groups: Object,
+ labels: Object,
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ canUpload: Boolean,
+ ownerOf: Array,
+ _originalId: String,
+ _editingRef: {
+ type: Boolean,
+ value: false,
+ },
+ _deleted: {
+ type: Boolean,
+ value: false,
+ },
+ _permissions: Array,
+ };
+ }
- // The name that gets automatically input when a new reference is added.
- const NEW_NAME = 'refs/heads/*';
- const REFS_NAME = 'refs/';
- const ON_BEHALF_OF = '(On Behalf Of)';
- const LABEL = 'Label';
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-saved',
+ () => this._handleAccessSaved());
+ }
- /**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrAccessSection extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-repo-access_test.
- */
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-access-section'; }
+ _updateSection(section) {
+ this._permissions = this.toSortedArray(section.value.permissions);
+ this._originalId = section.id;
+ }
- static get properties() {
- return {
- capabilities: Object,
- /** @type {?} */
- section: {
- type: Object,
- notify: true,
- observer: '_updateSection',
- },
- groups: Object,
- labels: Object,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- canUpload: Boolean,
- ownerOf: Array,
- _originalId: String,
- _editingRef: {
- type: Boolean,
- value: false,
- },
- _deleted: {
- type: Boolean,
- value: false,
- },
- _permissions: Array,
- };
+ _handleAccessSaved() {
+ // Set a new 'original' value to keep track of after the value has been
+ // saved.
+ this._updateSection(this.section);
+ }
+
+ _handleValueChange() {
+ if (!this.section.value.added) {
+ this.section.value.modified = this.section.id !== this._originalId;
+ // Allows overall access page to know a change has been made.
+ // For a new section, this is not fired because new permissions and
+ // rules have to be added in order to save, modifying the ref is not
+ // enough.
+ this.dispatchEvent(new CustomEvent(
+ 'access-modified', {bubbles: true, composed: true}));
}
+ this.section.value.updatedId = this.section.id;
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
- }
-
- _updateSection(section) {
- this._permissions = this.toSortedArray(section.value.permissions);
- this._originalId = section.id;
- }
-
- _handleAccessSaved() {
- // Set a new 'original' value to keep track of after the value has been
- // saved.
- this._updateSection(this.section);
- }
-
- _handleValueChange() {
- if (!this.section.value.added) {
- this.section.value.modified = this.section.id !== this._originalId;
- // Allows overall access page to know a change has been made.
- // For a new section, this is not fired because new permissions and
- // rules have to be added in order to save, modifying the ref is not
- // enough.
- this.dispatchEvent(new CustomEvent(
- 'access-modified', {bubbles: true, composed: true}));
- }
- this.section.value.updatedId = this.section.id;
- }
-
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld) { return; }
- // Restore original values if no longer editing.
- if (!editing) {
- this._editingRef = false;
- this._deleted = false;
- delete this.section.value.deleted;
- // Restore section ref.
- this.set(['section', 'id'], this._originalId);
- // Remove any unsaved but added permissions.
- this._permissions = this._permissions.filter(p => !p.value.added);
- for (const key of Object.keys(this.section.value.permissions)) {
- if (this.section.value.permissions[key].added) {
- delete this.section.value.permissions[key];
- }
- }
- }
- }
-
- _computePermissions(name, capabilities, labels) {
- let allPermissions;
- if (!this.section || !this.section.value) {
- return [];
- }
- if (name === GLOBAL_NAME) {
- allPermissions = this.toSortedArray(capabilities);
- } else {
- const labelOptions = this._computeLabelOptions(labels);
- allPermissions = labelOptions.concat(
- this.toSortedArray(this.permissionValues));
- }
- return allPermissions
- .filter(permission => !this.section.value.permissions[permission.id]);
- }
-
- _computeHideEditClass(section) {
- return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
- }
-
- _handleAddedPermissionRemoved(e) {
- const index = e.model.index;
- this._permissions = this._permissions.slice(0, index).concat(
- this._permissions.slice(index + 1, this._permissions.length));
- }
-
- _computeLabelOptions(labels) {
- const labelOptions = [];
- if (!labels) { return []; }
- for (const labelName of Object.keys(labels)) {
- labelOptions.push({
- id: 'label-' + labelName,
- value: {
- name: `${LABEL} ${labelName}`,
- id: 'label-' + labelName,
- },
- });
- labelOptions.push({
- id: 'labelAs-' + labelName,
- value: {
- name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
- id: 'labelAs-' + labelName,
- },
- });
- }
- return labelOptions;
- }
-
- _computePermissionName(name, permission, permissionValues, capabilities) {
- if (name === GLOBAL_NAME) {
- return capabilities[permission.id].name;
- } else if (permissionValues[permission.id]) {
- return permissionValues[permission.id].name;
- } else if (permission.value.label) {
- let behalfOf = '';
- if (permission.id.startsWith('labelAs-')) {
- behalfOf = ON_BEHALF_OF;
- }
- return `${LABEL} ${permission.value.label}${behalfOf}`;
- }
- }
-
- _computeSectionName(name) {
- // When a new section is created, it doesn't yet have a ref. Set into
- // edit mode so that the user can input one.
- if (!name) {
- this._editingRef = true;
- // Needed for the title value. This is the same default as GWT.
- name = NEW_NAME;
- // Needed for the input field value.
- this.set('section.id', name);
- }
- if (name === GLOBAL_NAME) {
- return 'Global Capabilities';
- } else if (name.startsWith(REFS_NAME)) {
- return `Reference: ${name}`;
- }
- return name;
- }
-
- _handleRemoveReference() {
- if (this.section.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-section-removed', {bubbles: true, composed: true}));
- }
- this._deleted = true;
- this.section.value.deleted = true;
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleUndoRemove() {
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld) { return; }
+ // Restore original values if no longer editing.
+ if (!editing) {
+ this._editingRef = false;
this._deleted = false;
delete this.section.value.deleted;
- }
-
- editRefInput() {
- return Polymer.dom(this.root).querySelector(Polymer.Element ?
- 'iron-input.editRefInput' :
- 'input[is=iron-input].editRefInput');
- }
-
- editReference() {
- this._editingRef = true;
- this.editRefInput().focus();
- }
-
- _isEditEnabled(canUpload, ownerOf, sectionId) {
- return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
- }
-
- _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
- const classList = [];
- if (editing
- && this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
- classList.push('editing');
+ // Restore section ref.
+ this.set(['section', 'id'], this._originalId);
+ // Remove any unsaved but added permissions.
+ this._permissions = this._permissions.filter(p => !p.value.added);
+ for (const key of Object.keys(this.section.value.permissions)) {
+ if (this.section.value.permissions[key].added) {
+ delete this.section.value.permissions[key];
+ }
}
- if (editingRef) {
- classList.push('editingRef');
- }
- if (deleted) {
- classList.push('deleted');
- }
- return classList.join(' ');
- }
-
- _computeEditBtnClass(name) {
- return name === GLOBAL_NAME ? 'global' : '';
- }
-
- _handleAddPermission() {
- const value = this.$.permissionSelect.value;
- const permission = {
- id: value,
- value: {rules: {}, added: true},
- };
-
- // This is needed to update the 'label' property of the
- // 'label-<label-name>' permission.
- //
- // The value from the add permission dropdown will either be
- // label-<label-name> or labelAs-<labelName>.
- // But, the format of the API response is as such:
- // "permissions": {
- // "label-Code-Review": {
- // "label": "Code-Review",
- // "rules": {...}
- // }
- // }
- // }
- // When we add a new item, we have to push the new permission in the same
- // format as the ones that have been returned by the API.
- if (value.startsWith('label')) {
- permission.value.label =
- value.replace('label-', '').replace('labelAs-', '');
- }
- // Add to the end of the array (used in dom-repeat) and also to the
- // section object that is two way bound with its parent element.
- this.push('_permissions', permission);
- this.set(['section.value.permissions', permission.id],
- permission.value);
}
}
- customElements.define(GrAccessSection.is, GrAccessSection);
-})();
+ _computePermissions(name, capabilities, labels) {
+ let allPermissions;
+ if (!this.section || !this.section.value) {
+ return [];
+ }
+ if (name === GLOBAL_NAME) {
+ allPermissions = this.toSortedArray(capabilities);
+ } else {
+ const labelOptions = this._computeLabelOptions(labels);
+ allPermissions = labelOptions.concat(
+ this.toSortedArray(this.permissionValues));
+ }
+ return allPermissions
+ .filter(permission => !this.section.value.permissions[permission.id]);
+ }
+
+ _computeHideEditClass(section) {
+ return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
+ }
+
+ _handleAddedPermissionRemoved(e) {
+ const index = e.model.index;
+ this._permissions = this._permissions.slice(0, index).concat(
+ this._permissions.slice(index + 1, this._permissions.length));
+ }
+
+ _computeLabelOptions(labels) {
+ const labelOptions = [];
+ if (!labels) { return []; }
+ for (const labelName of Object.keys(labels)) {
+ labelOptions.push({
+ id: 'label-' + labelName,
+ value: {
+ name: `${LABEL} ${labelName}`,
+ id: 'label-' + labelName,
+ },
+ });
+ labelOptions.push({
+ id: 'labelAs-' + labelName,
+ value: {
+ name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
+ id: 'labelAs-' + labelName,
+ },
+ });
+ }
+ return labelOptions;
+ }
+
+ _computePermissionName(name, permission, permissionValues, capabilities) {
+ if (name === GLOBAL_NAME) {
+ return capabilities[permission.id].name;
+ } else if (permissionValues[permission.id]) {
+ return permissionValues[permission.id].name;
+ } else if (permission.value.label) {
+ let behalfOf = '';
+ if (permission.id.startsWith('labelAs-')) {
+ behalfOf = ON_BEHALF_OF;
+ }
+ return `${LABEL} ${permission.value.label}${behalfOf}`;
+ }
+ }
+
+ _computeSectionName(name) {
+ // When a new section is created, it doesn't yet have a ref. Set into
+ // edit mode so that the user can input one.
+ if (!name) {
+ this._editingRef = true;
+ // Needed for the title value. This is the same default as GWT.
+ name = NEW_NAME;
+ // Needed for the input field value.
+ this.set('section.id', name);
+ }
+ if (name === GLOBAL_NAME) {
+ return 'Global Capabilities';
+ } else if (name.startsWith(REFS_NAME)) {
+ return `Reference: ${name}`;
+ }
+ return name;
+ }
+
+ _handleRemoveReference() {
+ if (this.section.value.added) {
+ this.dispatchEvent(new CustomEvent(
+ 'added-section-removed', {bubbles: true, composed: true}));
+ }
+ this._deleted = true;
+ this.section.value.deleted = true;
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleUndoRemove() {
+ this._deleted = false;
+ delete this.section.value.deleted;
+ }
+
+ editRefInput() {
+ return dom(this.root).querySelector(PolymerElement ?
+ 'iron-input.editRefInput' :
+ 'input[is=iron-input].editRefInput');
+ }
+
+ editReference() {
+ this._editingRef = true;
+ this.editRefInput().focus();
+ }
+
+ _isEditEnabled(canUpload, ownerOf, sectionId) {
+ return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
+ }
+
+ _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
+ const classList = [];
+ if (editing
+ && this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
+ classList.push('editing');
+ }
+ if (editingRef) {
+ classList.push('editingRef');
+ }
+ if (deleted) {
+ classList.push('deleted');
+ }
+ return classList.join(' ');
+ }
+
+ _computeEditBtnClass(name) {
+ return name === GLOBAL_NAME ? 'global' : '';
+ }
+
+ _handleAddPermission() {
+ const value = this.$.permissionSelect.value;
+ const permission = {
+ id: value,
+ value: {rules: {}, added: true},
+ };
+
+ // This is needed to update the 'label' property of the
+ // 'label-<label-name>' permission.
+ //
+ // The value from the add permission dropdown will either be
+ // label-<label-name> or labelAs-<labelName>.
+ // But, the format of the API response is as such:
+ // "permissions": {
+ // "label-Code-Review": {
+ // "label": "Code-Review",
+ // "rules": {...}
+ // }
+ // }
+ // }
+ // When we add a new item, we have to push the new permission in the same
+ // format as the ones that have been returned by the API.
+ if (value.startsWith('label')) {
+ permission.value.label =
+ value.replace('label-', '').replace('labelAs-', '');
+ }
+ // Add to the end of the array (used in dom-repeat) and also to the
+ // section object that is two way bound with its parent element.
+ this.push('_permissions', permission);
+ this.set(['section.value.permissions', permission.id],
+ permission.value);
+ }
+}
+
+customElements.define(GrAccessSection.is, GrAccessSection);
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
index a52cb1a..5f35f55 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
@@ -1,36 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-permission/gr-permission.html">
-
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-access-section">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -89,50 +75,23 @@
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <fieldset id="section"
- class$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]">
+ <fieldset id="section" class\$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]">
<div id="mainContainer">
<div class="header">
<div class="name">
<h3>[[_computeSectionName(section.id)]]</h3>
- <gr-button
- id="editBtn"
- link
- class$="[[_computeEditBtnClass(section.id)]]"
- on-click="editReference">
+ <gr-button id="editBtn" link="" class\$="[[_computeEditBtnClass(section.id)]]" on-click="editReference">
<iron-icon id="icon" icon="gr-icons:create"></iron-icon>
</gr-button>
</div>
- <iron-input
- class="editRefInput"
- bind-value="{{section.id}}"
- type="text"
- on-input="_handleValueChange">
- <input
- class="editRefInput"
- bind-value="{{section.id}}"
- is="iron-input"
- type="text"
- on-input="_handleValueChange">
+ <iron-input class="editRefInput" bind-value="{{section.id}}" type="text" on-input="_handleValueChange">
+ <input class="editRefInput" bind-value="{{section.id}}" is="iron-input" type="text" on-input="_handleValueChange">
</iron-input>
- <gr-button
- link
- id="deleteBtn"
- on-click="_handleRemoveReference">Remove</gr-button>
+ <gr-button link="" id="deleteBtn" on-click="_handleRemoveReference">Remove</gr-button>
</div><!-- end header -->
<div class="sectionContent">
- <template
- is="dom-repeat"
- items="{{_permissions}}"
- as="permission">
- <gr-permission
- name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]"
- permission="{{permission}}"
- labels="[[labels]]"
- section="[[section.id]]"
- editing="[[editing]]"
- groups="[[groups]]"
- on-added-permission-removed="_handleAddedPermissionRemoved">
+ <template is="dom-repeat" items="{{_permissions}}" as="permission">
+ <gr-permission name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]" permission="{{permission}}" labels="[[labels]]" section="[[section.id]]" editing="[[editing]]" groups="[[groups]]" on-added-permission-removed="_handleAddedPermissionRemoved">
</gr-permission>
</template>
<div id="addPermission">
@@ -140,29 +99,19 @@
<select id="permissionSelect">
<!-- called with a third parameter so that permissions update
after a new section is added. -->
- <template
- is="dom-repeat"
- items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]">
+ <template is="dom-repeat" items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]">
<option value="[[item.value.id]]">[[item.value.name]]</option>
</template>
</select>
- <gr-button
- link
- id="addBtn"
- on-click="_handleAddPermission">Add</gr-button>
+ <gr-button link="" id="addBtn" on-click="_handleAddPermission">Add</gr-button>
</div>
<!-- end addPermission -->
</div><!-- end sectionContent -->
</div><!-- end mainContainer -->
<div id="deletedContainer">
<span>[[_computeSectionName(section.id)]] was deleted</span>
- <gr-button
- link
- id="undoRemoveBtn"
- on-click="_handleUndoRemove">Undo</gr-button>
+ <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
</div><!-- end deletedContainer -->
</fieldset>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-access-section.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
index 2a3044e..4754c4a 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-access-section</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/page/page.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-access-section.html">
+<script src="/node_modules/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 type="module" src="./gr-access-section.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-access-section.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,28 +41,310 @@
</template>
</test-fixture>
-<script>
- suite('gr-access-section 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-access-section.js';
+suite('gr-access-section tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('unit tests', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ element.section = {
+ id: 'refs/*',
+ value: {
+ permissions: {
+ read: {
+ rules: {},
+ },
+ },
+ },
+ };
+ element.capabilities = {
+ accessDatabase: {
+ id: 'accessDatabase',
+ name: 'Access Database',
+ },
+ administrateServer: {
+ id: 'administrateServer',
+ name: 'Administrate Server',
+ },
+ batchChangesLimit: {
+ id: 'batchChangesLimit',
+ name: 'Batch Changes Limit',
+ },
+ createAccount: {
+ id: 'createAccount',
+ name: 'Create Account',
+ },
+ };
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ };
+ element._updateSection(element.section);
+ flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
+ test('_updateSection', () => {
+ // _updateSection was called in setup, so just make assertions.
+ const expectedPermissions = [
+ {
+ id: 'read',
+ value: {
+ rules: {},
+ },
+ },
+ ];
+ assert.deepEqual(element._permissions, expectedPermissions);
+ assert.equal(element._originalId, element.section.id);
});
- suite('unit tests', () => {
+ test('_computeLabelOptions', () => {
+ const expectedLabelOptions = [
+ {
+ id: 'label-Code-Review',
+ value: {
+ name: 'Label Code-Review',
+ id: 'label-Code-Review',
+ },
+ },
+ {
+ id: 'labelAs-Code-Review',
+ value: {
+ name: 'Label Code-Review (On Behalf Of)',
+ id: 'labelAs-Code-Review',
+ },
+ },
+ ];
+
+ assert.deepEqual(element._computeLabelOptions(element.labels),
+ expectedLabelOptions);
+ });
+
+ test('_handleAccessSaved', () => {
+ assert.equal(element._originalId, 'refs/*');
+ element.section.id = 'refs/for/bar';
+ element._handleAccessSaved();
+ assert.equal(element._originalId, 'refs/for/bar');
+ });
+
+ test('_computePermissions', () => {
+ sandbox.stub(element, 'toSortedArray').returns(
+ [{
+ id: 'push',
+ value: {
+ rules: {},
+ },
+ },
+ {
+ id: 'read',
+ value: {
+ rules: {},
+ },
+ },
+ ]);
+
+ const expectedPermissions = [{
+ id: 'push',
+ value: {
+ rules: {},
+ },
+ },
+ ];
+ const labelOptions = [
+ {
+ id: 'label-Code-Review',
+ value: {
+ name: 'Label Code-Review',
+ id: 'label-Code-Review',
+ },
+ },
+ {
+ id: 'labelAs-Code-Review',
+ value: {
+ name: 'Label Code-Review (On Behalf Of)',
+ id: 'labelAs-Code-Review',
+ },
+ },
+ ];
+
+ // For global capabilities, just return the sorted array filtered by
+ // existing permissions.
+ let name = 'GLOBAL_CAPABILITIES';
+ assert.deepEqual(element._computePermissions(name, element.capabilities,
+ element.labels), expectedPermissions);
+
+ // Uses the capabilities array to come up with possible values.
+ assert.isTrue(element.toSortedArray.lastCall.
+ calledWithExactly(element.capabilities));
+
+ // For everything else, include possible label values before filtering.
+ name = 'refs/for/*';
+ assert.deepEqual(element._computePermissions(name, element.capabilities,
+ element.labels), labelOptions.concat(expectedPermissions));
+
+ // Uses permissionValues (defined in gr-access-behavior) to come up with
+ // possible values.
+ assert.isTrue(element.toSortedArray.lastCall.
+ calledWithExactly(element.permissionValues));
+ });
+
+ test('_computePermissionName', () => {
+ let name = 'GLOBAL_CAPABILITIES';
+ let permission = {
+ id: 'administrateServer',
+ value: {},
+ };
+ assert.equal(element._computePermissionName(name, permission,
+ element.permissionValues, element.capabilities),
+ element.capabilities[permission.id].name);
+
+ name = 'refs/for/*';
+ permission = {
+ id: 'abandon',
+ value: {},
+ };
+
+ assert.equal(element._computePermissionName(
+ name, permission, element.permissionValues, element.capabilities),
+ element.permissionValues[permission.id].name);
+
+ name = 'refs/for/*';
+ permission = {
+ id: 'label-Code-Review',
+ value: {
+ label: 'Code-Review',
+ },
+ };
+
+ assert.equal(element._computePermissionName(name, permission,
+ element.permissionValues, element.capabilities),
+ 'Label Code-Review');
+
+ permission = {
+ id: 'labelAs-Code-Review',
+ value: {
+ label: 'Code-Review',
+ },
+ };
+
+ assert.equal(element._computePermissionName(name, permission,
+ element.permissionValues, element.capabilities),
+ 'Label Code-Review(On Behalf Of)');
+ });
+
+ test('_computeSectionName', () => {
+ let name;
+ // When computing the section name for an undefined name, it means a
+ // new section is being added. In this case, it should defualt to
+ // 'refs/heads/*'.
+ element._editingRef = false;
+ assert.equal(element._computeSectionName(name),
+ 'Reference: refs/heads/*');
+ assert.isTrue(element._editingRef);
+ assert.equal(element.section.id, 'refs/heads/*');
+
+ // Reset editing to false.
+ element._editingRef = false;
+ name = 'GLOBAL_CAPABILITIES';
+ assert.equal(element._computeSectionName(name), 'Global Capabilities');
+ assert.isFalse(element._editingRef);
+
+ name = 'refs/for/*';
+ assert.equal(element._computeSectionName(name),
+ 'Reference: refs/for/*');
+ assert.isFalse(element._editingRef);
+ });
+
+ test('editReference', () => {
+ element.editReference();
+ assert.isTrue(element._editingRef);
+ });
+
+ test('_computeSectionClass', () => {
+ let editingRef = false;
+ let canUpload = false;
+ let ownerOf = [];
+ let editing = false;
+ let deleted = false;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), '');
+
+ editing = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), '');
+
+ ownerOf = ['refs/*'];
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing');
+
+ ownerOf = [];
+ canUpload = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing');
+
+ editingRef = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing editingRef');
+
+ deleted = true;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing editingRef deleted');
+
+ editingRef = false;
+ assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
+ editingRef, deleted), 'editing deleted');
+ });
+
+ test('_computeEditBtnClass', () => {
+ let name = 'GLOBAL_CAPABILITIES';
+ assert.equal(element._computeEditBtnClass(name), 'global');
+ name = 'refs/for/*';
+ assert.equal(element._computeEditBtnClass(name), '');
+ });
+ });
+
+ suite('interactive tests', () => {
+ setup(() => {
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ };
+ });
+ suite('Global section', () => {
setup(() => {
element.section = {
- id: 'refs/*',
+ id: 'GLOBAL_CAPABILITIES',
value: {
permissions: {
- read: {
+ accessDatabase: {
rules: {},
},
},
@@ -81,476 +368,196 @@
name: 'Create Account',
},
};
- element.labels = {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- default_value: 0,
- },
- };
element._updateSection(element.section);
flushAsynchronousOperations();
});
- test('_updateSection', () => {
- // _updateSection was called in setup, so just make assertions.
- const expectedPermissions = [
- {
- id: 'read',
- value: {
- rules: {},
- },
- },
- ];
- assert.deepEqual(element._permissions, expectedPermissions);
- assert.equal(element._originalId, element.section.id);
- });
-
- test('_computeLabelOptions', () => {
- const expectedLabelOptions = [
- {
- id: 'label-Code-Review',
- value: {
- name: 'Label Code-Review',
- id: 'label-Code-Review',
- },
- },
- {
- id: 'labelAs-Code-Review',
- value: {
- name: 'Label Code-Review (On Behalf Of)',
- id: 'labelAs-Code-Review',
- },
- },
- ];
-
- assert.deepEqual(element._computeLabelOptions(element.labels),
- expectedLabelOptions);
- });
-
- test('_handleAccessSaved', () => {
- assert.equal(element._originalId, 'refs/*');
- element.section.id = 'refs/for/bar';
- element._handleAccessSaved();
- assert.equal(element._originalId, 'refs/for/bar');
- });
-
- test('_computePermissions', () => {
- sandbox.stub(element, 'toSortedArray').returns(
- [{
- id: 'push',
- value: {
- rules: {},
- },
- },
- {
- id: 'read',
- value: {
- rules: {},
- },
- },
- ]);
-
- const expectedPermissions = [{
- id: 'push',
- value: {
- rules: {},
- },
- },
- ];
- const labelOptions = [
- {
- id: 'label-Code-Review',
- value: {
- name: 'Label Code-Review',
- id: 'label-Code-Review',
- },
- },
- {
- id: 'labelAs-Code-Review',
- value: {
- name: 'Label Code-Review (On Behalf Of)',
- id: 'labelAs-Code-Review',
- },
- },
- ];
-
- // For global capabilities, just return the sorted array filtered by
- // existing permissions.
- let name = 'GLOBAL_CAPABILITIES';
- assert.deepEqual(element._computePermissions(name, element.capabilities,
- element.labels), expectedPermissions);
-
- // Uses the capabilities array to come up with possible values.
- assert.isTrue(element.toSortedArray.lastCall.
- calledWithExactly(element.capabilities));
-
- // For everything else, include possible label values before filtering.
- name = 'refs/for/*';
- assert.deepEqual(element._computePermissions(name, element.capabilities,
- element.labels), labelOptions.concat(expectedPermissions));
-
- // Uses permissionValues (defined in gr-access-behavior) to come up with
- // possible values.
- assert.isTrue(element.toSortedArray.lastCall.
- calledWithExactly(element.permissionValues));
- });
-
- test('_computePermissionName', () => {
- let name = 'GLOBAL_CAPABILITIES';
- let permission = {
- id: 'administrateServer',
- value: {},
- };
- assert.equal(element._computePermissionName(name, permission,
- element.permissionValues, element.capabilities),
- element.capabilities[permission.id].name);
-
- name = 'refs/for/*';
- permission = {
- id: 'abandon',
- value: {},
- };
-
- assert.equal(element._computePermissionName(
- name, permission, element.permissionValues, element.capabilities),
- element.permissionValues[permission.id].name);
-
- name = 'refs/for/*';
- permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- },
- };
-
- assert.equal(element._computePermissionName(name, permission,
- element.permissionValues, element.capabilities),
- 'Label Code-Review');
-
- permission = {
- id: 'labelAs-Code-Review',
- value: {
- label: 'Code-Review',
- },
- };
-
- assert.equal(element._computePermissionName(name, permission,
- element.permissionValues, element.capabilities),
- 'Label Code-Review(On Behalf Of)');
- });
-
- test('_computeSectionName', () => {
- let name;
- // When computing the section name for an undefined name, it means a
- // new section is being added. In this case, it should defualt to
- // 'refs/heads/*'.
- element._editingRef = false;
- assert.equal(element._computeSectionName(name),
- 'Reference: refs/heads/*');
- assert.isTrue(element._editingRef);
- assert.equal(element.section.id, 'refs/heads/*');
-
- // Reset editing to false.
- element._editingRef = false;
- name = 'GLOBAL_CAPABILITIES';
- assert.equal(element._computeSectionName(name), 'Global Capabilities');
- assert.isFalse(element._editingRef);
-
- name = 'refs/for/*';
- assert.equal(element._computeSectionName(name),
- 'Reference: refs/for/*');
- assert.isFalse(element._editingRef);
- });
-
- test('editReference', () => {
- element.editReference();
- assert.isTrue(element._editingRef);
- });
-
- test('_computeSectionClass', () => {
- let editingRef = false;
- let canUpload = false;
- let ownerOf = [];
- let editing = false;
- let deleted = false;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), '');
-
- ownerOf = ['refs/*'];
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing');
-
- ownerOf = [];
- canUpload = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing');
-
- editingRef = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing editingRef');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing editingRef deleted');
-
- editingRef = false;
- assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
- editingRef, deleted), 'editing deleted');
- });
-
- test('_computeEditBtnClass', () => {
- let name = 'GLOBAL_CAPABILITIES';
- assert.equal(element._computeEditBtnClass(name), 'global');
- name = 'refs/for/*';
- assert.equal(element._computeEditBtnClass(name), '');
+ test('classes are assigned correctly', () => {
+ assert.isFalse(element.$.section.classList.contains('editing'));
+ assert.isFalse(element.$.section.classList.contains('deleted'));
+ assert.isTrue(element.$.editBtn.classList.contains('global'));
+ element.editing = true;
+ element.canUpload = true;
+ element.ownerOf = [];
+ assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
});
});
- suite('interactive tests', () => {
+ suite('Non-global section', () => {
setup(() => {
- element.labels = {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
+ element.section = {
+ id: 'refs/*',
+ value: {
+ permissions: {
+ read: {
+ rules: {},
+ },
},
- default_value: 0,
},
};
- });
- suite('Global section', () => {
- setup(() => {
- element.section = {
- id: 'GLOBAL_CAPABILITIES',
- value: {
- permissions: {
- accessDatabase: {
- rules: {},
- },
- },
- },
- };
- element.capabilities = {
- accessDatabase: {
- id: 'accessDatabase',
- name: 'Access Database',
- },
- administrateServer: {
- id: 'administrateServer',
- name: 'Administrate Server',
- },
- batchChangesLimit: {
- id: 'batchChangesLimit',
- name: 'Batch Changes Limit',
- },
- createAccount: {
- id: 'createAccount',
- name: 'Create Account',
- },
- };
- element._updateSection(element.section);
- flushAsynchronousOperations();
- });
-
- test('classes are assigned correctly', () => {
- assert.isFalse(element.$.section.classList.contains('editing'));
- assert.isFalse(element.$.section.classList.contains('deleted'));
- assert.isTrue(element.$.editBtn.classList.contains('global'));
- element.editing = true;
- element.canUpload = true;
- element.ownerOf = [];
- assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
- });
+ element.capabilities = {};
+ element._updateSection(element.section);
+ flushAsynchronousOperations();
});
- suite('Non-global section', () => {
- setup(() => {
- element.section = {
- id: 'refs/*',
- value: {
- permissions: {
- read: {
- rules: {},
- },
- },
- },
- };
- element.capabilities = {};
- element._updateSection(element.section);
- flushAsynchronousOperations();
- });
+ test('classes are assigned correctly', () => {
+ assert.isFalse(element.$.section.classList.contains('editing'));
+ assert.isFalse(element.$.section.classList.contains('deleted'));
+ assert.isFalse(element.$.editBtn.classList.contains('global'));
+ element.editing = true;
+ element.canUpload = true;
+ element.ownerOf = [];
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
+ });
- test('classes are assigned correctly', () => {
- assert.isFalse(element.$.section.classList.contains('editing'));
- assert.isFalse(element.$.section.classList.contains('deleted'));
- assert.isFalse(element.$.editBtn.classList.contains('global'));
- element.editing = true;
- element.canUpload = true;
- element.ownerOf = [];
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
- });
+ test('add permission', () => {
+ element.editing = true;
+ element.$.permissionSelect.value = 'label-Code-Review';
+ assert.equal(element._permissions.length, 1);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 1);
+ MockInteractions.tap(element.$.addBtn);
+ flushAsynchronousOperations();
- test('add permission', () => {
- element.editing = true;
- element.$.permissionSelect.value = 'label-Code-Review';
- assert.equal(element._permissions.length, 1);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 1);
- MockInteractions.tap(element.$.addBtn);
- flushAsynchronousOperations();
+ // The permission is added to both the permissions array and also
+ // the section's permission object.
+ assert.equal(element._permissions.length, 2);
+ let permission = {
+ id: 'label-Code-Review',
+ value: {
+ added: true,
+ label: 'Code-Review',
+ rules: {},
+ },
+ };
+ assert.equal(element._permissions.length, 2);
+ assert.deepEqual(element._permissions[1], permission);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 2);
+ assert.deepEqual(
+ element.section.value.permissions['label-Code-Review'],
+ permission.value);
- // The permission is added to both the permissions array and also
- // the section's permission object.
- assert.equal(element._permissions.length, 2);
- let permission = {
- id: 'label-Code-Review',
- value: {
- added: true,
- label: 'Code-Review',
- rules: {},
- },
- };
- assert.equal(element._permissions.length, 2);
- assert.deepEqual(element._permissions[1], permission);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 2);
- assert.deepEqual(
- element.section.value.permissions['label-Code-Review'],
- permission.value);
+ element.$.permissionSelect.value = 'abandon';
+ MockInteractions.tap(element.$.addBtn);
+ flushAsynchronousOperations();
- element.$.permissionSelect.value = 'abandon';
- MockInteractions.tap(element.$.addBtn);
- flushAsynchronousOperations();
+ permission = {
+ id: 'abandon',
+ value: {
+ added: true,
+ rules: {},
+ },
+ };
- permission = {
- id: 'abandon',
- value: {
- added: true,
- rules: {},
- },
- };
+ assert.equal(element._permissions.length, 3);
+ assert.deepEqual(element._permissions[2], permission);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 3);
+ assert.deepEqual(element.section.value.permissions['abandon'],
+ permission.value);
- assert.equal(element._permissions.length, 3);
- assert.deepEqual(element._permissions[2], permission);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 3);
- assert.deepEqual(element.section.value.permissions['abandon'],
- permission.value);
+ // Unsaved changes are discarded when editing is cancelled.
+ element.editing = false;
+ assert.equal(element._permissions.length, 1);
+ assert.equal(Object.keys(element.section.value.permissions).length,
+ 1);
+ });
- // Unsaved changes are discarded when editing is cancelled.
+ test('edit section reference', done => {
+ element.canUpload = true;
+ element.ownerOf = [];
+ element.section = {id: 'refs/for/bar', value: {permissions: {}}};
+ assert.isFalse(element.$.section.classList.contains('editing'));
+ element.editing = true;
+ assert.isTrue(element.$.section.classList.contains('editing'));
+ assert.isFalse(element._editingRef);
+ MockInteractions.tap(element.$.editBtn);
+ element.editRefInput().bindValue='new/ref';
+ setTimeout(() => {
+ assert.equal(element.section.id, 'new/ref');
+ assert.isTrue(element._editingRef);
+ assert.isTrue(element.$.section.classList.contains('editingRef'));
element.editing = false;
- assert.equal(element._permissions.length, 1);
- assert.equal(Object.keys(element.section.value.permissions).length,
- 1);
- });
-
- test('edit section reference', done => {
- element.canUpload = true;
- element.ownerOf = [];
- element.section = {id: 'refs/for/bar', value: {permissions: {}}};
- assert.isFalse(element.$.section.classList.contains('editing'));
- element.editing = true;
- assert.isTrue(element.$.section.classList.contains('editing'));
assert.isFalse(element._editingRef);
- MockInteractions.tap(element.$.editBtn);
- element.editRefInput().bindValue='new/ref';
- setTimeout(() => {
- assert.equal(element.section.id, 'new/ref');
- assert.isTrue(element._editingRef);
- assert.isTrue(element.$.section.classList.contains('editingRef'));
- element.editing = false;
- assert.isFalse(element._editingRef);
- assert.equal(element.section.id, 'refs/for/bar');
- done();
- });
+ assert.equal(element.section.id, 'refs/for/bar');
+ done();
});
+ });
- test('_handleValueChange', () => {
- // For an exising section.
- const modifiedHandler = sandbox.stub();
- element.section = {id: 'refs/for/bar', value: {permissions: {}}};
- assert.notOk(element.section.value.updatedId);
- element.section.id = 'refs/for/baz';
- element.addEventListener('access-modified', modifiedHandler);
- assert.isNotOk(element.section.value.modified);
- element._handleValueChange();
- assert.equal(element.section.value.updatedId, 'refs/for/baz');
- assert.isTrue(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 1);
- element.section.id = 'refs/for/bar';
- element._handleValueChange();
- assert.isFalse(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 2);
+ test('_handleValueChange', () => {
+ // For an exising section.
+ const modifiedHandler = sandbox.stub();
+ element.section = {id: 'refs/for/bar', value: {permissions: {}}};
+ assert.notOk(element.section.value.updatedId);
+ element.section.id = 'refs/for/baz';
+ element.addEventListener('access-modified', modifiedHandler);
+ assert.isNotOk(element.section.value.modified);
+ element._handleValueChange();
+ assert.equal(element.section.value.updatedId, 'refs/for/baz');
+ assert.isTrue(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 1);
+ element.section.id = 'refs/for/bar';
+ element._handleValueChange();
+ assert.isFalse(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 2);
- // For a new section.
- element.section.value.added = true;
- element._handleValueChange();
- assert.isFalse(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 2);
- element.section.id = 'refs/for/bar';
- element._handleValueChange();
- assert.isFalse(element.section.value.modified);
- assert.equal(modifiedHandler.callCount, 2);
- });
+ // For a new section.
+ element.section.value.added = true;
+ element._handleValueChange();
+ assert.isFalse(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 2);
+ element.section.id = 'refs/for/bar';
+ element._handleValueChange();
+ assert.isFalse(element.section.value.modified);
+ assert.equal(modifiedHandler.callCount, 2);
+ });
- test('remove section', () => {
- element.editing = true;
- element.canUpload = true;
- element.ownerOf = [];
- assert.isFalse(element._deleted);
- assert.isNotOk(element.section.value.deleted);
- MockInteractions.tap(element.$.deleteBtn);
- flushAsynchronousOperations();
- assert.isTrue(element._deleted);
- assert.isTrue(element.section.value.deleted);
- assert.isTrue(element.$.section.classList.contains('deleted'));
- assert.isTrue(element.section.value.deleted);
+ test('remove section', () => {
+ element.editing = true;
+ element.canUpload = true;
+ element.ownerOf = [];
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.section.value.deleted);
+ MockInteractions.tap(element.$.deleteBtn);
+ flushAsynchronousOperations();
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.section.value.deleted);
+ assert.isTrue(element.$.section.classList.contains('deleted'));
+ assert.isTrue(element.section.value.deleted);
- MockInteractions.tap(element.$.undoRemoveBtn);
- flushAsynchronousOperations();
- assert.isFalse(element._deleted);
- assert.isNotOk(element.section.value.deleted);
+ MockInteractions.tap(element.$.undoRemoveBtn);
+ flushAsynchronousOperations();
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.section.value.deleted);
- MockInteractions.tap(element.$.deleteBtn);
- assert.isTrue(element._deleted);
- assert.isTrue(element.section.value.deleted);
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.section.value.deleted);
- });
+ MockInteractions.tap(element.$.deleteBtn);
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.section.value.deleted);
+ element.editing = false;
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.section.value.deleted);
+ });
- test('removing an added permission', () => {
- element.editing = true;
- assert.equal(element._permissions.length, 1);
- element.shadowRoot
- .querySelector('gr-permission').fire('added-permission-removed');
- flushAsynchronousOperations();
- assert.equal(element._permissions.length, 0);
- });
+ test('removing an added permission', () => {
+ element.editing = true;
+ assert.equal(element._permissions.length, 1);
+ element.shadowRoot
+ .querySelector('gr-permission').fire('added-permission-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._permissions.length, 0);
+ });
- test('remove an added section', () => {
- const removeStub = sandbox.stub();
- element.addEventListener('added-section-removed', removeStub);
- element.editing = true;
- element.section.value.added = true;
- MockInteractions.tap(element.$.deleteBtn);
- assert.isTrue(removeStub.called);
- });
+ test('remove an added section', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-section-removed', removeStub);
+ element.editing = true;
+ element.section.value.added = true;
+ MockInteractions.tap(element.$.deleteBtn);
+ assert.isTrue(removeStub.called);
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 96008b7..bdf64de 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -14,155 +14,171 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-group-dialog/gr-create-group-dialog.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-admin-group-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrAdminGroupList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-admin-group-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/admin/groups',
+ },
+ _hasNewGroupName: Boolean,
+ _createNewCapability: {
+ type: Boolean,
+ value: false,
+ },
+ _groups: Array,
+
+ /**
+ * Because we request one more than the groupsPerPage, _shownGroups
+ * may be one less than _groups.
+ * */
+ _shownGroups: {
+ type: Array,
+ computed: 'computeShownItems(_groups)',
+ },
+
+ _groupsPerPage: {
+ type: Number,
+ value: 25,
+ },
+
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: String,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getCreateGroupCapability();
+ this.fire('title-change', {title: 'Groups'});
+ this._maybeOpenCreateOverlay(this.params);
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getGroups(this._filter, this._groupsPerPage,
+ this._offset);
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
+ * Opens the create overlay if the route has a hash 'create'
+ *
+ * @param {!Object} params
*/
- class GrAdminGroupList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-admin-group-list'; }
-
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
-
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
- _path: {
- type: String,
- readOnly: true,
- value: '/admin/groups',
- },
- _hasNewGroupName: Boolean,
- _createNewCapability: {
- type: Boolean,
- value: false,
- },
- _groups: Array,
-
- /**
- * Because we request one more than the groupsPerPage, _shownGroups
- * may be one less than _groups.
- * */
- _shownGroups: {
- type: Array,
- computed: 'computeShownItems(_groups)',
- },
-
- _groupsPerPage: {
- type: Number,
- value: 25,
- },
-
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: String,
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getCreateGroupCapability();
- this.fire('title-change', {title: 'Groups'});
- this._maybeOpenCreateOverlay(this.params);
- }
-
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getGroups(this._filter, this._groupsPerPage,
- this._offset);
- }
-
- /**
- * Opens the create overlay if the route has a hash 'create'
- *
- * @param {!Object} params
- */
- _maybeOpenCreateOverlay(params) {
- if (params && params.openCreateModal) {
- this.$.createOverlay.open();
- }
- }
-
- _computeGroupUrl(id) {
- return Gerrit.Nav.getUrlForGroup(id);
- }
-
- _getCreateGroupCapability() {
- return this.$.restAPI.getAccount().then(account => {
- if (!account) { return; }
- return this.$.restAPI.getAccountCapabilities(['createGroup'])
- .then(capabilities => {
- if (capabilities.createGroup) {
- this._createNewCapability = true;
- }
- });
- });
- }
-
- _getGroups(filter, groupsPerPage, offset) {
- this._groups = [];
- return this.$.restAPI.getGroups(filter, groupsPerPage, offset)
- .then(groups => {
- if (!groups) {
- return;
- }
- this._groups = Object.keys(groups)
- .map(key => {
- const group = groups[key];
- group.name = key;
- return group;
- });
- this._loading = false;
- });
- }
-
- _refreshGroupsList() {
- this.$.restAPI.invalidateGroupsCache();
- return this._getGroups(this._filter, this._groupsPerPage,
- this._offset);
- }
-
- _handleCreateGroup() {
- this.$.createNewModal.handleCreateGroup().then(() => {
- this._refreshGroupsList();
- });
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
-
- _visibleToAll(item) {
- return item.options.visible_to_all === true ? 'Y' : 'N';
- }
}
- customElements.define(GrAdminGroupList.is, GrAdminGroupList);
-})();
+ _computeGroupUrl(id) {
+ return Gerrit.Nav.getUrlForGroup(id);
+ }
+
+ _getCreateGroupCapability() {
+ return this.$.restAPI.getAccount().then(account => {
+ if (!account) { return; }
+ return this.$.restAPI.getAccountCapabilities(['createGroup'])
+ .then(capabilities => {
+ if (capabilities.createGroup) {
+ this._createNewCapability = true;
+ }
+ });
+ });
+ }
+
+ _getGroups(filter, groupsPerPage, offset) {
+ this._groups = [];
+ return this.$.restAPI.getGroups(filter, groupsPerPage, offset)
+ .then(groups => {
+ if (!groups) {
+ return;
+ }
+ this._groups = Object.keys(groups)
+ .map(key => {
+ const group = groups[key];
+ group.name = key;
+ return group;
+ });
+ this._loading = false;
+ });
+ }
+
+ _refreshGroupsList() {
+ this.$.restAPI.invalidateGroupsCache();
+ return this._getGroups(this._filter, this._groupsPerPage,
+ this._offset);
+ }
+
+ _handleCreateGroup() {
+ this.$.createNewModal.handleCreateGroup().then(() => {
+ this._refreshGroupsList();
+ });
+ }
+
+ _handleCloseCreate() {
+ this.$.createOverlay.close();
+ }
+
+ _handleCreateClicked() {
+ this.$.createOverlay.open();
+ }
+
+ _visibleToAll(item) {
+ return item.options.visible_to_all === true ? 'Y' : 'N';
+ }
+}
+
+customElements.define(GrAdminGroupList.is, GrAdminGroupList);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
index 5207717..ffc10d7 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
@@ -1,64 +1,43 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-group-dialog/gr-create-group-dialog.html">
-
-<dom-module id="gr-admin-group-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <gr-list-view
- create-new="[[_createNewCapability]]"
- filter="[[_filter]]"
- items="[[_groups]]"
- items-per-page="[[_groupsPerPage]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_path]]">
+ <gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items="[[_groups]]" items-per-page="[[_groupsPerPage]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]">
<table id="list" class="genericList">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="name topHeader">Group Name</th>
<th class="description topHeader">Group Description</th>
<th class="visibleToAll topHeader">Visible To All</th>
</tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownGroups]]">
<tr class="table">
<td class="name">
- <a href$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a>
+ <a href\$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a>
</td>
<td class="description">[[item.description]]</td>
<td class="visibleToAll">[[_visibleToAll(item)]]</td>
@@ -67,27 +46,15 @@
</tbody>
</table>
</gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- id="createDialog"
- class="confirmDialog"
- disabled="[[!_hasNewGroupName]]"
- confirm-label="Create"
- confirm-on-enter
- on-confirm="_handleCreateGroup"
- on-cancel="_handleCloseCreate">
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewGroupName]]" confirm-label="Create" confirm-on-enter="" on-confirm="_handleCreateGroup" on-cancel="_handleCloseCreate">
<div class="header" slot="header">
Create Group
</div>
<div class="main" slot="main">
- <gr-create-group-dialog
- has-new-group-name="{{_hasNewGroupName}}"
- params="[[params]]"
- id="createNewModal"></gr-create-group-dialog>
+ <gr-create-group-dialog has-new-group-name="{{_hasNewGroupName}}" params="[[params]]" id="createNewModal"></gr-create-group-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-admin-group-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
index c0558d2..36c2081 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -19,18 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-group-list</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/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
-<link rel="import" href="gr-admin-group-list.html">
+<script type="module" src="./gr-admin-group-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-admin-group-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -38,153 +43,155 @@
</template>
</test-fixture>
-<script>
- let counter = 0;
- const groupGenerator = () => {
- return {
- name: `test${++counter}`,
- id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b',
- url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
- options: {
- visible_to_all: false,
- },
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
- };
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-admin-group-list.js';
+let counter = 0;
+const groupGenerator = () => {
+ return {
+ name: `test${++counter}`,
+ id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b',
+ url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
+ options: {
+ visible_to_all: false,
+ },
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
};
+};
- suite('gr-admin-group-list tests', async () => {
- await readyToTest();
- let element;
- let groups;
- let sandbox;
- let value;
+suite('gr-admin-group-list tests', () => {
+ let element;
+ let groups;
+ let sandbox;
+ let value;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with groups', () => {
+ setup(done => {
+ groups = _.times(26, groupGenerator);
+
+ stub('gr-rest-api-interface', {
+ getGroups(num, offset) {
+ return Promise.resolve(groups);
+ },
+ });
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with groups', () => {
- setup(done => {
- groups = _.times(26, groupGenerator);
-
- stub('gr-rest-api-interface', {
- getGroups(num, offset) {
- return Promise.resolve(groups);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('test for test group in the list', done => {
- flush(() => {
- assert.equal(element._groups[1].name, '1');
- assert.equal(element._groups[1].options.visible_to_all, false);
- done();
- });
- });
-
- test('_shownGroups', () => {
- assert.equal(element._shownGroups.length, 25);
- });
-
- test('_maybeOpenCreateOverlay', () => {
- const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
- element._maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- const params = {};
- element._maybeOpenCreateOverlay(params);
- assert.isFalse(overlayOpen.called);
- params.openCreateModal = true;
- element._maybeOpenCreateOverlay(params);
- assert.isTrue(overlayOpen.called);
+ test('test for test group in the list', done => {
+ flush(() => {
+ assert.equal(element._groups[1].name, '1');
+ assert.equal(element._groups[1].options.visible_to_all, false);
+ done();
});
});
- suite('test with less then 25 groups', () => {
- setup(done => {
- groups = _.times(25, groupGenerator);
-
- stub('gr-rest-api-interface', {
- getGroups(num, offset) {
- return Promise.resolve(groups);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('_shownGroups', () => {
- assert.equal(element._shownGroups.length, 25);
- });
+ test('_shownGroups', () => {
+ assert.equal(element._shownGroups.length, 25);
});
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getGroups',
- () => Promise.resolve(groups));
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value).then(() => {
- assert.isTrue(element.$.restAPI.getGroups.lastCall
- .calledWithExactly('test', 25, 25));
- done();
- });
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
+ });
+
+ suite('test with less then 25 groups', () => {
+ setup(done => {
+ groups = _.times(25, groupGenerator);
+
+ stub('gr-rest-api-interface', {
+ getGroups(num, offset) {
+ return Promise.resolve(groups);
+ },
});
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._groups = _.times(25, groupGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
+ test('_shownGroups', () => {
+ assert.equal(element._shownGroups.length, 25);
});
+ });
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sandbox.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').fire('create-clicked');
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sandbox.stub(element.$.createOverlay, 'open');
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateGroup called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateGroup');
- element.$.createDialog.fire('confirm');
- assert.isTrue(element._handleCreateGroup.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreate');
- element.$.createDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreate.called);
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getGroups',
+ () => Promise.resolve(groups));
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value).then(() => {
+ assert.isTrue(element.$.restAPI.getGroups.lastCall
+ .calledWithExactly('test', 25, 25));
+ done();
});
});
});
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._groups = _.times(25, groupGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+
+ suite('create new', () => {
+ test('_handleCreateClicked called when create-click fired', () => {
+ sandbox.stub(element, '_handleCreateClicked');
+ element.shadowRoot
+ .querySelector('gr-list-view').fire('create-clicked');
+ assert.isTrue(element._handleCreateClicked.called);
+ });
+
+ test('_handleCreateClicked opens modal', () => {
+ const openStub = sandbox.stub(element.$.createOverlay, 'open');
+ element._handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateGroup called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateGroup');
+ element.$.createDialog.fire('confirm');
+ assert.isTrue(element._handleCreateGroup.called);
+ });
+
+ test('_handleCloseCreate called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreate');
+ element.$.createDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreate.called);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index e300c90..d03df39 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -14,281 +14,310 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/gr-page-nav-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-page-nav/gr-page-nav.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-admin-group-list/gr-admin-group-list.js';
+import '../gr-group/gr-group.js';
+import '../gr-group-audit-log/gr-group-audit-log.js';
+import '../gr-group-members/gr-group-members.js';
+import '../gr-plugin-list/gr-plugin-list.js';
+import '../gr-repo/gr-repo.js';
+import '../gr-repo-access/gr-repo-access.js';
+import '../gr-repo-commands/gr-repo-commands.js';
+import '../gr-repo-dashboards/gr-repo-dashboards.js';
+import '../gr-repo-detail-list/gr-repo-detail-list.js';
+import '../gr-repo-list/gr-repo-list.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-admin-view_html.js';
- /**
- * @appliesMixin Gerrit.AdminNavMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrAdminView extends Polymer.mixinBehaviors( [
- Gerrit.AdminNavBehavior,
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-admin-view'; }
+const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
- static get properties() {
- return {
- /** @type {?} */
- params: Object,
- path: String,
- adminView: String,
+/**
+ * @appliesMixin Gerrit.AdminNavMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrAdminView extends mixinBehaviors( [
+ Gerrit.AdminNavBehavior,
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _breadcrumbParentName: String,
- _repoName: String,
- _groupId: {
- type: Number,
- observer: '_computeGroupName',
- },
- _groupIsInternal: Boolean,
- _groupName: String,
- _groupOwner: {
- type: Boolean,
- value: false,
- },
- _subsectionLinks: Array,
- _filteredLinks: Array,
- _showDownload: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- _showGroup: Boolean,
- _showGroupAuditLog: Boolean,
- _showGroupList: Boolean,
- _showGroupMembers: Boolean,
- _showRepoAccess: Boolean,
- _showRepoCommands: Boolean,
- _showRepoDashboards: Boolean,
- _showRepoDetailList: Boolean,
- _showRepoMain: Boolean,
- _showRepoList: Boolean,
- _showPluginList: Boolean,
- };
- }
+ static get is() { return 'gr-admin-view'; }
- static get observers() {
- return [
- '_paramsChanged(params)',
- ];
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ params: Object,
+ path: String,
+ adminView: String,
- /** @override */
- attached() {
- super.attached();
- this.reload();
- }
-
- reload() {
- const promises = [
- this.$.restAPI.getAccount(),
- Gerrit.awaitPluginsLoaded(),
- ];
- return Promise.all(promises).then(result => {
- this._account = result[0];
- let options;
- if (this._repoName) {
- options = {repoName: this._repoName};
- } else if (this._groupId) {
- options = {
- groupId: this._groupId,
- groupName: this._groupName,
- groupIsInternal: this._groupIsInternal,
- isAdmin: this._isAdmin,
- groupOwner: this._groupOwner,
- };
- }
-
- return this.getAdminLinks(this._account,
- this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
- this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
- options)
- .then(res => {
- this._filteredLinks = res.links;
- this._breadcrumbParentName = res.expandedSection ?
- res.expandedSection.name : '';
-
- if (!res.expandedSection) {
- this._subsectionLinks = [];
- return;
- }
- this._subsectionLinks = [res.expandedSection]
- .concat(res.expandedSection.children).map(section => {
- return {
- text: !section.detailType ? 'Home' : section.name,
- value: section.view + (section.detailType || ''),
- view: section.view,
- url: section.url,
- detailType: section.detailType,
- parent: this._groupId || this._repoName || '',
- };
- });
- });
- });
- }
-
- _computeSelectValue(params) {
- if (!params || !params.view) { return; }
- return params.view + (params.detail || '');
- }
-
- _selectedIsCurrentPage(selected) {
- return (selected.parent === (this._repoName || this._groupId) &&
- selected.view === this.params.view &&
- selected.detailType === this.params.detail);
- }
-
- _handleSubsectionChange(e) {
- const selected = this._subsectionLinks
- .find(section => section.value === e.detail.value);
-
- // This is when it gets set initially.
- if (this._selectedIsCurrentPage(selected)) {
- return;
- }
- Gerrit.Nav.navigateToRelativeUrl(selected.url);
- }
-
- _paramsChanged(params) {
- const isGroupView = params.view === Gerrit.Nav.View.GROUP;
- const isRepoView = params.view === Gerrit.Nav.View.REPO;
- const isAdminView = params.view === Gerrit.Nav.View.ADMIN;
-
- this.set('_showGroup', isGroupView && !params.detail);
- this.set('_showGroupAuditLog', isGroupView &&
- params.detail === Gerrit.Nav.GroupDetailView.LOG);
- this.set('_showGroupMembers', isGroupView &&
- params.detail === Gerrit.Nav.GroupDetailView.MEMBERS);
-
- this.set('_showGroupList', isAdminView &&
- params.adminView === 'gr-admin-group-list');
-
- this.set('_showRepoAccess', isRepoView &&
- params.detail === Gerrit.Nav.RepoDetailView.ACCESS);
- this.set('_showRepoCommands', isRepoView &&
- params.detail === Gerrit.Nav.RepoDetailView.COMMANDS);
- this.set('_showRepoDetailList', isRepoView &&
- (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES ||
- params.detail === Gerrit.Nav.RepoDetailView.TAGS));
- this.set('_showRepoDashboards', isRepoView &&
- params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS);
- this.set('_showRepoMain', isRepoView && !params.detail);
-
- this.set('_showRepoList', isAdminView &&
- params.adminView === 'gr-repo-list');
-
- this.set('_showPluginList', isAdminView &&
- params.adminView === 'gr-plugin-list');
-
- let needsReload = false;
- if (params.repo !== this._repoName) {
- this._repoName = params.repo || '';
- // Reloads the admin menu.
- needsReload = true;
- }
- if (params.groupId !== this._groupId) {
- this._groupId = params.groupId || '';
- // Reloads the admin menu.
- needsReload = true;
- }
- if (this._breadcrumbParentName && !params.groupId && !params.repo) {
- needsReload = true;
- }
- if (!needsReload) { return; }
- this.reload();
- }
-
- // TODO (beckysiegel): Update these functions after router abstraction is
- // updated. They are currently copied from gr-dropdown (and should be
- // updated there as well once complete).
- _computeURLHelper(host, path) {
- return '//' + host + this.getBaseUrl() + path;
- }
-
- _computeRelativeURL(path) {
- const host = window.location.host;
- return this._computeURLHelper(host, path);
- }
-
- _computeLinkURL(link) {
- if (!link || typeof link.url === 'undefined') { return ''; }
- if (link.target || !link.noBaseUrl) {
- return link.url;
- }
- return this._computeRelativeURL(link.url);
- }
-
- /**
- * @param {string} itemView
- * @param {Object} params
- * @param {string=} opt_detailType
- */
- _computeSelectedClass(itemView, params, opt_detailType) {
- if (!params) return '';
- // Group params are structured differently from admin params. Compute
- // selected differently for groups.
- // TODO(wyatta): Simplify this when all routes work like group params.
- if (params.view === Gerrit.Nav.View.GROUP &&
- itemView === Gerrit.Nav.View.GROUP) {
- if (!params.detail && !opt_detailType) { return 'selected'; }
- if (params.detail === opt_detailType) { return 'selected'; }
- return '';
- }
-
- if (params.view === Gerrit.Nav.View.REPO &&
- itemView === Gerrit.Nav.View.REPO) {
- if (!params.detail && !opt_detailType) { return 'selected'; }
- if (params.detail === opt_detailType) { return 'selected'; }
- return '';
- }
-
- if (params.detailType && params.detailType !== opt_detailType) {
- return '';
- }
- return itemView === params.adminView ? 'selected' : '';
- }
-
- _computeGroupName(groupId) {
- if (!groupId) { return ''; }
-
- const promises = [];
- this.$.restAPI.getGroupConfig(groupId).then(group => {
- if (!group || !group.name) { return; }
-
- this._groupName = group.name;
- this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
- this.reload();
-
- promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- }));
-
- promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
- isOwner => {
- this._groupOwner = isOwner;
- }));
-
- return Promise.all(promises).then(() => {
- this.reload();
- });
- });
- }
-
- _updateGroupName(e) {
- this._groupName = e.detail.name;
- this.reload();
- }
+ _breadcrumbParentName: String,
+ _repoName: String,
+ _groupId: {
+ type: Number,
+ observer: '_computeGroupName',
+ },
+ _groupIsInternal: Boolean,
+ _groupName: String,
+ _groupOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _subsectionLinks: Array,
+ _filteredLinks: Array,
+ _showDownload: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ _showGroup: Boolean,
+ _showGroupAuditLog: Boolean,
+ _showGroupList: Boolean,
+ _showGroupMembers: Boolean,
+ _showRepoAccess: Boolean,
+ _showRepoCommands: Boolean,
+ _showRepoDashboards: Boolean,
+ _showRepoDetailList: Boolean,
+ _showRepoMain: Boolean,
+ _showRepoList: Boolean,
+ _showPluginList: Boolean,
+ };
}
- customElements.define(GrAdminView.is, GrAdminView);
-})();
+ static get observers() {
+ return [
+ '_paramsChanged(params)',
+ ];
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.reload();
+ }
+
+ reload() {
+ const promises = [
+ this.$.restAPI.getAccount(),
+ Gerrit.awaitPluginsLoaded(),
+ ];
+ return Promise.all(promises).then(result => {
+ this._account = result[0];
+ let options;
+ if (this._repoName) {
+ options = {repoName: this._repoName};
+ } else if (this._groupId) {
+ options = {
+ groupId: this._groupId,
+ groupName: this._groupName,
+ groupIsInternal: this._groupIsInternal,
+ isAdmin: this._isAdmin,
+ groupOwner: this._groupOwner,
+ };
+ }
+
+ return this.getAdminLinks(this._account,
+ this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
+ this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
+ options)
+ .then(res => {
+ this._filteredLinks = res.links;
+ this._breadcrumbParentName = res.expandedSection ?
+ res.expandedSection.name : '';
+
+ if (!res.expandedSection) {
+ this._subsectionLinks = [];
+ return;
+ }
+ this._subsectionLinks = [res.expandedSection]
+ .concat(res.expandedSection.children).map(section => {
+ return {
+ text: !section.detailType ? 'Home' : section.name,
+ value: section.view + (section.detailType || ''),
+ view: section.view,
+ url: section.url,
+ detailType: section.detailType,
+ parent: this._groupId || this._repoName || '',
+ };
+ });
+ });
+ });
+ }
+
+ _computeSelectValue(params) {
+ if (!params || !params.view) { return; }
+ return params.view + (params.detail || '');
+ }
+
+ _selectedIsCurrentPage(selected) {
+ return (selected.parent === (this._repoName || this._groupId) &&
+ selected.view === this.params.view &&
+ selected.detailType === this.params.detail);
+ }
+
+ _handleSubsectionChange(e) {
+ const selected = this._subsectionLinks
+ .find(section => section.value === e.detail.value);
+
+ // This is when it gets set initially.
+ if (this._selectedIsCurrentPage(selected)) {
+ return;
+ }
+ Gerrit.Nav.navigateToRelativeUrl(selected.url);
+ }
+
+ _paramsChanged(params) {
+ const isGroupView = params.view === Gerrit.Nav.View.GROUP;
+ const isRepoView = params.view === Gerrit.Nav.View.REPO;
+ const isAdminView = params.view === Gerrit.Nav.View.ADMIN;
+
+ this.set('_showGroup', isGroupView && !params.detail);
+ this.set('_showGroupAuditLog', isGroupView &&
+ params.detail === Gerrit.Nav.GroupDetailView.LOG);
+ this.set('_showGroupMembers', isGroupView &&
+ params.detail === Gerrit.Nav.GroupDetailView.MEMBERS);
+
+ this.set('_showGroupList', isAdminView &&
+ params.adminView === 'gr-admin-group-list');
+
+ this.set('_showRepoAccess', isRepoView &&
+ params.detail === Gerrit.Nav.RepoDetailView.ACCESS);
+ this.set('_showRepoCommands', isRepoView &&
+ params.detail === Gerrit.Nav.RepoDetailView.COMMANDS);
+ this.set('_showRepoDetailList', isRepoView &&
+ (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES ||
+ params.detail === Gerrit.Nav.RepoDetailView.TAGS));
+ this.set('_showRepoDashboards', isRepoView &&
+ params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS);
+ this.set('_showRepoMain', isRepoView && !params.detail);
+
+ this.set('_showRepoList', isAdminView &&
+ params.adminView === 'gr-repo-list');
+
+ this.set('_showPluginList', isAdminView &&
+ params.adminView === 'gr-plugin-list');
+
+ let needsReload = false;
+ if (params.repo !== this._repoName) {
+ this._repoName = params.repo || '';
+ // Reloads the admin menu.
+ needsReload = true;
+ }
+ if (params.groupId !== this._groupId) {
+ this._groupId = params.groupId || '';
+ // Reloads the admin menu.
+ needsReload = true;
+ }
+ if (this._breadcrumbParentName && !params.groupId && !params.repo) {
+ needsReload = true;
+ }
+ if (!needsReload) { return; }
+ this.reload();
+ }
+
+ // TODO (beckysiegel): Update these functions after router abstraction is
+ // updated. They are currently copied from gr-dropdown (and should be
+ // updated there as well once complete).
+ _computeURLHelper(host, path) {
+ return '//' + host + this.getBaseUrl() + path;
+ }
+
+ _computeRelativeURL(path) {
+ const host = window.location.host;
+ return this._computeURLHelper(host, path);
+ }
+
+ _computeLinkURL(link) {
+ if (!link || typeof link.url === 'undefined') { return ''; }
+ if (link.target || !link.noBaseUrl) {
+ return link.url;
+ }
+ return this._computeRelativeURL(link.url);
+ }
+
+ /**
+ * @param {string} itemView
+ * @param {Object} params
+ * @param {string=} opt_detailType
+ */
+ _computeSelectedClass(itemView, params, opt_detailType) {
+ if (!params) return '';
+ // Group params are structured differently from admin params. Compute
+ // selected differently for groups.
+ // TODO(wyatta): Simplify this when all routes work like group params.
+ if (params.view === Gerrit.Nav.View.GROUP &&
+ itemView === Gerrit.Nav.View.GROUP) {
+ if (!params.detail && !opt_detailType) { return 'selected'; }
+ if (params.detail === opt_detailType) { return 'selected'; }
+ return '';
+ }
+
+ if (params.view === Gerrit.Nav.View.REPO &&
+ itemView === Gerrit.Nav.View.REPO) {
+ if (!params.detail && !opt_detailType) { return 'selected'; }
+ if (params.detail === opt_detailType) { return 'selected'; }
+ return '';
+ }
+
+ if (params.detailType && params.detailType !== opt_detailType) {
+ return '';
+ }
+ return itemView === params.adminView ? 'selected' : '';
+ }
+
+ _computeGroupName(groupId) {
+ if (!groupId) { return ''; }
+
+ const promises = [];
+ this.$.restAPI.getGroupConfig(groupId).then(group => {
+ if (!group || !group.name) { return; }
+
+ this._groupName = group.name;
+ this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
+ this.reload();
+
+ promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin;
+ }));
+
+ promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
+ isOwner => {
+ this._groupOwner = isOwner;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this.reload();
+ });
+ });
+ }
+
+ _updateGroupName(e) {
+ this._groupName = e.detail.name;
+ this.reload();
+ }
+}
+
+customElements.define(GrAdminView.is, GrAdminView);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
index aae11d3..0bc9431 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
@@ -1,48 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/gr-page-nav-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.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-page-nav/gr-page-nav.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html">
-<link rel="import" href="../gr-group/gr-group.html">
-<link rel="import" href="../gr-group-audit-log/gr-group-audit-log.html">
-<link rel="import" href="../gr-group-members/gr-group-members.html">
-<link rel="import" href="../gr-plugin-list/gr-plugin-list.html">
-<link rel="import" href="../gr-repo/gr-repo.html">
-<link rel="import" href="../gr-repo-access/gr-repo-access.html">
-<link rel="import" href="../gr-repo-commands/gr-repo-commands.html">
-<link rel="import" href="../gr-repo-dashboards/gr-repo-dashboards.html">
-<link rel="import" href="../gr-repo-detail-list/gr-repo-detail-list.html">
-<link rel="import" href="../gr-repo-list/gr-repo-list.html">
-
-<dom-module id="gr-admin-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -84,28 +58,24 @@
<gr-page-nav class="navStyles">
<ul class="sectionContent">
<template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
- <li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
- <a class="title" href="[[_computeLinkURL(item)]]"
- rel="noopener">[[item.name]]</a>
+ <li class\$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
+ <a class="title" href="[[_computeLinkURL(item)]]" rel="noopener">[[item.name]]</a>
</li>
<template is="dom-repeat" items="[[item.children]]" as="child">
- <li class$="[[_computeSelectedClass(child.view, params)]]">
- <a href$="[[_computeLinkURL(child)]]"
- rel="noopener">[[child.name]]</a>
+ <li class\$="[[_computeSelectedClass(child.view, params)]]">
+ <a href\$="[[_computeLinkURL(child)]]" rel="noopener">[[child.name]]</a>
</li>
</template>
<template is="dom-if" if="[[item.subsection]]">
<!--If a section has a subsection, render that.-->
- <li class$="[[_computeSelectedClass(item.subsection.view, params)]]">
- <a class="title" href$="[[_computeLinkURL(item.subsection)]]"
- rel="noopener">
+ <li class\$="[[_computeSelectedClass(item.subsection.view, params)]]">
+ <a class="title" href\$="[[_computeLinkURL(item.subsection)]]" rel="noopener">
[[item.subsection.name]]</a>
</li>
<!--Loop through the links in the sub-section.-->
- <template is="dom-repeat"
- items="[[item.subsection.children]]" as="child">
- <li class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]">
- <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
+ <template is="dom-repeat" items="[[item.subsection.children]]" as="child">
+ <li class\$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]">
+ <a href\$="[[_computeLinkURL(child)]]">[[child.name]]</a>
</li>
</template>
</template>
@@ -118,12 +88,7 @@
<span class="breadcrumbText">[[_breadcrumbParentName]]</span>
<iron-icon icon="gr-icons:chevron-right"></iron-icon>
</span>
- <gr-dropdown-list
- lowercase
- id="pageSelect"
- value="[[_computeSelectValue(params)]]"
- items="[[_subsectionLinks]]"
- on-value-change="_handleSubsectionChange">
+ <gr-dropdown-list lowercase="" id="pageSelect" value="[[_computeSelectValue(params)]]" items="[[_subsectionLinks]]" on-value-change="_handleSubsectionChange">
</gr-dropdown-list>
</section>
</template>
@@ -150,42 +115,32 @@
</template>
<template is="dom-if" if="[[_showGroup]]" restamp="true">
<main class="breadcrumbs">
- <gr-group
- group-id="[[params.groupId]]"
- on-name-changed="_updateGroupName"></gr-group>
+ <gr-group group-id="[[params.groupId]]" on-name-changed="_updateGroupName"></gr-group>
</main>
</template>
<template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
<main class="breadcrumbs">
- <gr-group-members
- group-id="[[params.groupId]]"></gr-group-members>
+ <gr-group-members group-id="[[params.groupId]]"></gr-group-members>
</main>
</template>
<template is="dom-if" if="[[_showRepoDetailList]]" restamp="true">
<main class="table breadcrumbs">
- <gr-repo-detail-list
- params="[[params]]"
- class="table"></gr-repo-detail-list>
+ <gr-repo-detail-list params="[[params]]" class="table"></gr-repo-detail-list>
</main>
</template>
<template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true">
<main class="table breadcrumbs">
- <gr-group-audit-log
- group-id="[[params.groupId]]"
- class="table"></gr-group-audit-log>
+ <gr-group-audit-log group-id="[[params.groupId]]" class="table"></gr-group-audit-log>
</main>
</template>
<template is="dom-if" if="[[_showRepoCommands]]" restamp="true">
<main class="breadcrumbs">
- <gr-repo-commands
- repo="[[params.repo]]"></gr-repo-commands>
+ <gr-repo-commands repo="[[params.repo]]"></gr-repo-commands>
</main>
</template>
<template is="dom-if" if="[[_showRepoAccess]]" restamp="true">
<main class="breadcrumbs">
- <gr-repo-access
- path="[[path]]"
- repo="[[params.repo]]"></gr-repo-access>
+ <gr-repo-access path="[[path]]" repo="[[params.repo]]"></gr-repo-access>
</main>
</template>
<template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
@@ -195,6 +150,4 @@
</template>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-admin-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 416099d..584eb88 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_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-admin-view</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-admin-view.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-admin-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-admin-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,645 +40,648 @@
</template>
</test-fixture>
-<script>
- suite('gr-admin-view 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-admin-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-admin-view tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-rest-api-interface', {
- getProjectConfig() {
- return Promise.resolve({});
- },
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-rest-api-interface', {
+ getProjectConfig() {
+ return Promise.resolve({});
+ },
+ });
+ const pluginsLoaded = Promise.resolve();
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(pluginsLoaded);
+ pluginsLoaded.then(() => flush(done));
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeURLHelper', () => {
+ const path = '/test';
+ const host = 'http://www.testsite.com';
+ const computedPath = element._computeURLHelper(host, path);
+ assert.equal(computedPath, '//http://www.testsite.com/test');
+ });
+
+ test('link URLs', () => {
+ assert.equal(
+ element._computeLinkURL({url: '/test', noBaseUrl: true}),
+ '//' + window.location.host + '/test');
+
+ sandbox.stub(element, 'getBaseUrl').returns('/foo');
+ assert.equal(
+ element._computeLinkURL({url: '/test', noBaseUrl: true}),
+ '//' + window.location.host + '/foo/test');
+ assert.equal(element._computeLinkURL({url: '/test'}), '/test');
+ assert.equal(
+ element._computeLinkURL({url: '/test', target: '_blank'}),
+ '/test');
+ });
+
+ test('current page gets selected and is displayed', () => {
+ element._filteredLinks = [{
+ name: 'Repositories',
+ url: '/admin/repos',
+ view: 'gr-repo-list',
+ }];
+
+ element.params = {
+ view: 'admin',
+ adminView: 'gr-repo-list',
+ };
+
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root).querySelectorAll(
+ '.selected').length, 1);
+ assert.ok(element.shadowRoot
+ .querySelector('gr-repo-list'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-admin-create-repo'));
+ });
+
+ test('_filteredLinks admin', done => {
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ })
+ );
+ element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 3);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Groups
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Plugins
+ assert.isNotOk(element._filteredLinks[0].subsection);
+ done();
+ });
+ });
+
+ test('_filteredLinks non admin authenticated', done => {
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({})
+ );
+ element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 2);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Groups
+ assert.isNotOk(element._filteredLinks[0].subsection);
+ done();
+ });
+ });
+
+ test('_filteredLinks non admin unathenticated', done => {
+ element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 1);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+ done();
+ });
+ });
+
+ test('_filteredLinks from plugin', () => {
+ sandbox.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([
+ {text: 'internal link text', url: '/internal/link/url'},
+ {text: 'external link text', url: 'http://external/link/url'},
+ ]);
+ return element.reload().then(() => {
+ assert.equal(element._filteredLinks.length, 3);
+ assert.deepEqual(element._filteredLinks[1], {
+ capability: null,
+ url: '/internal/link/url',
+ name: 'internal link text',
+ noBaseUrl: true,
+ view: null,
+ viewableToAll: true,
+ target: null,
});
- const pluginsLoaded = Promise.resolve();
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(pluginsLoaded);
- pluginsLoaded.then(() => flush(done));
+ assert.deepEqual(element._filteredLinks[2], {
+ capability: null,
+ url: 'http://external/link/url',
+ name: 'external link text',
+ noBaseUrl: false,
+ view: null,
+ viewableToAll: true,
+ target: '_blank',
+ });
});
+ });
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeURLHelper', () => {
- const path = '/test';
- const host = 'http://www.testsite.com';
- const computedPath = element._computeURLHelper(host, path);
- assert.equal(computedPath, '//http://www.testsite.com/test');
- });
-
- test('link URLs', () => {
+ test('Repo shows up in nav', done => {
+ element._repoName = 'Test Repo';
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ element.reload().then(() => {
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('.sectionTitle').length, 3);
+ assert.equal(element.shadowRoot
+ .querySelector('.breadcrumbText').innerText, 'Test Repo');
assert.equal(
- element._computeLinkURL({url: '/test', noBaseUrl: true}),
- '//' + window.location.host + '/test');
-
- sandbox.stub(element, 'getBaseUrl').returns('/foo');
- assert.equal(
- element._computeLinkURL({url: '/test', noBaseUrl: true}),
- '//' + window.location.host + '/foo/test');
- assert.equal(element._computeLinkURL({url: '/test'}), '/test');
- assert.equal(
- element._computeLinkURL({url: '/test', target: '_blank'}),
- '/test');
+ element.shadowRoot.querySelector('#pageSelect').items.length,
+ 6
+ );
+ done();
});
+ });
- test('current page gets selected and is displayed', () => {
- element._filteredLinks = [{
+ test('Group shows up in nav', done => {
+ element._groupId = 'a15262';
+ element._groupName = 'my-group';
+ element._groupIsInternal = true;
+ element._isAdmin = true;
+ element._groupOwner = false;
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ element.reload().then(() => {
+ flushAsynchronousOperations();
+ assert.equal(element._filteredLinks.length, 3);
+
+ // Repos
+ assert.isNotOk(element._filteredLinks[0].subsection);
+
+ // Groups
+ assert.equal(element._filteredLinks[1].subsection.children.length, 2);
+ assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
+
+ // Plugins
+ assert.isNotOk(element._filteredLinks[2].subsection);
+ done();
+ });
+ });
+
+ test('Nav is reloaded when repo changes', () => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+ sandbox.stub(element, 'reload');
+ element.params = {repo: 'Test Repo', adminView: 'gr-repo'};
+ assert.equal(element.reload.callCount, 1);
+ element.params = {repo: 'Test Repo 2',
+ adminView: 'gr-repo'};
+ assert.equal(element.reload.callCount, 2);
+ });
+
+ test('Nav is reloaded when group changes', () => {
+ sandbox.stub(element, '_computeGroupName');
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+ sandbox.stub(element, 'reload');
+ element.params = {groupId: '1', adminView: 'gr-group'};
+ assert.equal(element.reload.callCount, 1);
+ });
+
+ test('Nav is reloaded when group name changes', done => {
+ const newName = 'newName';
+ sandbox.stub(element, '_computeGroupName');
+ sandbox.stub(element, 'reload', () => {
+ assert.equal(element._groupName, newName);
+ assert.isTrue(element.reload.called);
+ done();
+ });
+ element.params = {group: 1, view: Gerrit.Nav.View.GROUP};
+ element._groupName = 'oldName';
+ flushAsynchronousOperations();
+ element.shadowRoot
+ .querySelector('gr-group').fire('name-changed', {name: newName});
+ });
+
+ test('dropdown displays if there is a subsection', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.mainHeader'));
+ element._subsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
+ view: 'repo',
+ url: '',
+ parent: 'my-repo',
+ detailType: undefined,
+ },
+ ];
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('.mainHeader'));
+ element._subsectionLinks = undefined;
+ flushAsynchronousOperations();
+ assert.equal(
+ getComputedStyle(element.shadowRoot
+ .querySelector('.mainHeader')).display,
+ 'none');
+ });
+
+ test('Dropdown only triggers navigation on explicit select', done => {
+ element._repoName = 'my-repo';
+ element.params = {
+ repo: 'my-repo',
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ };
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+ flushAsynchronousOperations();
+ const expectedFilteredLinks = [
+ {
name: 'Repositories',
+ noBaseUrl: true,
url: '/admin/repos',
view: 'gr-repo-list',
- }];
-
- element.params = {
- view: 'admin',
- adminView: 'gr-repo-list',
- };
-
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root).querySelectorAll(
- '.selected').length, 1);
- assert.ok(element.shadowRoot
- .querySelector('gr-repo-list'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-admin-create-repo'));
- });
-
- test('_filteredLinks admin', done => {
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- })
- );
- element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 3);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Groups
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Plugins
- assert.isNotOk(element._filteredLinks[0].subsection);
- done();
- });
- });
-
- test('_filteredLinks non admin authenticated', done => {
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({})
- );
- element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 2);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Groups
- assert.isNotOk(element._filteredLinks[0].subsection);
- done();
- });
- });
-
- test('_filteredLinks non admin unathenticated', done => {
- element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 1);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
- done();
- });
- });
-
- test('_filteredLinks from plugin', () => {
- sandbox.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([
- {text: 'internal link text', url: '/internal/link/url'},
- {text: 'external link text', url: 'http://external/link/url'},
- ]);
- return element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 3);
- assert.deepEqual(element._filteredLinks[1], {
- capability: null,
- url: '/internal/link/url',
- name: 'internal link text',
- noBaseUrl: true,
- view: null,
- viewableToAll: true,
- target: null,
- });
- assert.deepEqual(element._filteredLinks[2], {
- capability: null,
- url: 'http://external/link/url',
- name: 'external link text',
- noBaseUrl: false,
- view: null,
- viewableToAll: true,
- target: '_blank',
- });
- });
- });
-
- test('Repo shows up in nav', done => {
- element._repoName = 'Test Repo';
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- element.reload().then(() => {
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.sectionTitle').length, 3);
- assert.equal(element.shadowRoot
- .querySelector('.breadcrumbText').innerText, 'Test Repo');
- assert.equal(
- element.shadowRoot.querySelector('#pageSelect').items.length,
- 6
- );
- done();
- });
- });
-
- test('Group shows up in nav', done => {
- element._groupId = 'a15262';
- element._groupName = 'my-group';
- element._groupIsInternal = true;
- element._isAdmin = true;
- element._groupOwner = false;
- sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- element.reload().then(() => {
- flushAsynchronousOperations();
- assert.equal(element._filteredLinks.length, 3);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Groups
- assert.equal(element._filteredLinks[1].subsection.children.length, 2);
- assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
-
- // Plugins
- assert.isNotOk(element._filteredLinks[2].subsection);
- done();
- });
- });
-
- test('Nav is reloaded when repo changes', () => {
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
- sandbox.stub(element, 'reload');
- element.params = {repo: 'Test Repo', adminView: 'gr-repo'};
- assert.equal(element.reload.callCount, 1);
- element.params = {repo: 'Test Repo 2',
- adminView: 'gr-repo'};
- assert.equal(element.reload.callCount, 2);
- });
-
- test('Nav is reloaded when group changes', () => {
- sandbox.stub(element, '_computeGroupName');
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
- sandbox.stub(element, 'reload');
- element.params = {groupId: '1', adminView: 'gr-group'};
- assert.equal(element.reload.callCount, 1);
- });
-
- test('Nav is reloaded when group name changes', done => {
- const newName = 'newName';
- sandbox.stub(element, '_computeGroupName');
- sandbox.stub(element, 'reload', () => {
- assert.equal(element._groupName, newName);
- assert.isTrue(element.reload.called);
- done();
- });
- element.params = {group: 1, view: Gerrit.Nav.View.GROUP};
- element._groupName = 'oldName';
- flushAsynchronousOperations();
- element.shadowRoot
- .querySelector('gr-group').fire('name-changed', {name: newName});
- });
-
- test('dropdown displays if there is a subsection', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('.mainHeader'));
- element._subsectionLinks = [
- {
- text: 'Home',
- value: 'repo',
+ viewableToAll: true,
+ subsection: {
+ name: 'my-repo',
view: 'repo',
url: '',
- parent: 'my-repo',
- detailType: undefined,
+ children: [
+ {
+ name: 'Access',
+ view: 'repo',
+ detailType: 'access',
+ url: '',
+ },
+ {
+ name: 'Commands',
+ view: 'repo',
+ detailType: 'commands',
+ url: '',
+ },
+ {
+ name: 'Branches',
+ view: 'repo',
+ detailType: 'branches',
+ url: '',
+ },
+ {
+ name: 'Tags',
+ view: 'repo',
+ detailType: 'tags',
+ url: '',
+ },
+ {
+ name: 'Dashboards',
+ view: 'repo',
+ detailType: 'dashboards',
+ url: '',
+ },
+ ],
},
- ];
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('.mainHeader'));
- element._subsectionLinks = undefined;
- flushAsynchronousOperations();
- assert.equal(
- getComputedStyle(element.shadowRoot
- .querySelector('.mainHeader')).display,
- 'none');
- });
-
- test('Dropdown only triggers navigation on explicit select', done => {
- element._repoName = 'my-repo';
- element.params = {
- repo: 'my-repo',
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.ACCESS,
- };
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
- flushAsynchronousOperations();
- const expectedFilteredLinks = [
- {
- name: 'Repositories',
- noBaseUrl: true,
- url: '/admin/repos',
- view: 'gr-repo-list',
- viewableToAll: true,
- subsection: {
- name: 'my-repo',
- view: 'repo',
- url: '',
- children: [
- {
- name: 'Access',
- view: 'repo',
- detailType: 'access',
- url: '',
- },
- {
- name: 'Commands',
- view: 'repo',
- detailType: 'commands',
- url: '',
- },
- {
- name: 'Branches',
- view: 'repo',
- detailType: 'branches',
- url: '',
- },
- {
- name: 'Tags',
- view: 'repo',
- detailType: 'tags',
- url: '',
- },
- {
- name: 'Dashboards',
- view: 'repo',
- detailType: 'dashboards',
- url: '',
- },
- ],
- },
- },
- {
- name: 'Groups',
- section: 'Groups',
- noBaseUrl: true,
- url: '/admin/groups',
- view: 'gr-admin-group-list',
- },
- {
- name: 'Plugins',
- capability: 'viewPlugins',
- section: 'Plugins',
- noBaseUrl: true,
- url: '/admin/plugins',
- view: 'gr-plugin-list',
- },
- ];
- const expectedSubsectionLinks = [
- {
- text: 'Home',
- value: 'repo',
- view: 'repo',
- url: '',
- parent: 'my-repo',
- detailType: undefined,
- },
- {
- text: 'Access',
- value: 'repoaccess',
- view: 'repo',
- url: '',
- detailType: 'access',
- parent: 'my-repo',
- },
- {
- text: 'Commands',
- value: 'repocommands',
- view: 'repo',
- url: '',
- detailType: 'commands',
- parent: 'my-repo',
- },
- {
- text: 'Branches',
- value: 'repobranches',
- view: 'repo',
- url: '',
- detailType: 'branches',
- parent: 'my-repo',
- },
- {
- text: 'Tags',
- value: 'repotags',
- view: 'repo',
- url: '',
- detailType: 'tags',
- parent: 'my-repo',
- },
- {
- text: 'Dashboards',
- value: 'repodashboards',
- view: 'repo',
- url: '',
- detailType: 'dashboards',
- parent: 'my-repo',
- },
- ];
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
- sandbox.spy(element, '_selectedIsCurrentPage');
- sandbox.spy(element, '_handleSubsectionChange');
- element.reload().then(() => {
- assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
- assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
- assert.equal(
- element.shadowRoot.querySelector('#pageSelect').value,
- 'repoaccess'
- );
- assert.isTrue(element._selectedIsCurrentPage.calledOnce);
- // Doesn't trigger navigation from the page select menu.
- assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called);
-
- // When explicitly changed, navigation is called
- element.shadowRoot.querySelector('#pageSelect').value = 'repo';
- assert.isTrue(element._selectedIsCurrentPage.calledTwice);
- assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce);
- done();
- });
- });
-
- test('_selectedIsCurrentPage', () => {
- element._repoName = 'my-repo';
- element.params = {view: 'repo', repo: 'my-repo'};
- const selected = {
+ },
+ {
+ name: 'Groups',
+ section: 'Groups',
+ noBaseUrl: true,
+ url: '/admin/groups',
+ view: 'gr-admin-group-list',
+ },
+ {
+ name: 'Plugins',
+ capability: 'viewPlugins',
+ section: 'Plugins',
+ noBaseUrl: true,
+ url: '/admin/plugins',
+ view: 'gr-plugin-list',
+ },
+ ];
+ const expectedSubsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
view: 'repo',
- detailType: undefined,
+ url: '',
parent: 'my-repo',
- };
- assert.isTrue(element._selectedIsCurrentPage(selected));
- selected.parent = 'my-second-repo';
- assert.isFalse(element._selectedIsCurrentPage(selected));
- selected.detailType = 'detailType';
- assert.isFalse(element._selectedIsCurrentPage(selected));
+ detailType: undefined,
+ },
+ {
+ text: 'Access',
+ value: 'repoaccess',
+ view: 'repo',
+ url: '',
+ detailType: 'access',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Commands',
+ value: 'repocommands',
+ view: 'repo',
+ url: '',
+ detailType: 'commands',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Branches',
+ value: 'repobranches',
+ view: 'repo',
+ url: '',
+ detailType: 'branches',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Tags',
+ value: 'repotags',
+ view: 'repo',
+ url: '',
+ detailType: 'tags',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Dashboards',
+ value: 'repodashboards',
+ view: 'repo',
+ url: '',
+ detailType: 'dashboards',
+ parent: 'my-repo',
+ },
+ ];
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+ sandbox.spy(element, '_selectedIsCurrentPage');
+ sandbox.spy(element, '_handleSubsectionChange');
+ element.reload().then(() => {
+ assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
+ assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
+ assert.equal(
+ element.shadowRoot.querySelector('#pageSelect').value,
+ 'repoaccess'
+ );
+ assert.isTrue(element._selectedIsCurrentPage.calledOnce);
+ // Doesn't trigger navigation from the page select menu.
+ assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called);
+
+ // When explicitly changed, navigation is called
+ element.shadowRoot.querySelector('#pageSelect').value = 'repo';
+ assert.isTrue(element._selectedIsCurrentPage.calledTwice);
+ assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce);
+ done();
+ });
+ });
+
+ test('_selectedIsCurrentPage', () => {
+ element._repoName = 'my-repo';
+ element.params = {view: 'repo', repo: 'my-repo'};
+ const selected = {
+ view: 'repo',
+ detailType: undefined,
+ parent: 'my-repo',
+ };
+ assert.isTrue(element._selectedIsCurrentPage(selected));
+ selected.parent = 'my-second-repo';
+ assert.isFalse(element._selectedIsCurrentPage(selected));
+ selected.detailType = 'detailType';
+ assert.isFalse(element._selectedIsCurrentPage(selected));
+ });
+
+ suite('_computeSelectedClass', () => {
+ setup(() => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccountCapabilities',
+ () => Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ }));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getAccount',
+ () => Promise.resolve({_id: 1}));
+
+ return element.reload();
});
- suite('_computeSelectedClass', () => {
+ suite('repos', () => {
setup(() => {
- sandbox.stub(
- element.$.restAPI,
- 'getAccountCapabilities',
- () => Promise.resolve({
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- }));
- sandbox.stub(
- element.$.restAPI,
- 'getAccount',
- () => Promise.resolve({_id: 1}));
+ stub('gr-repo-access', {
+ _repoChanged: () => {},
+ });
+ });
+ test('repo list', () => {
+ element.params = {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-repo-list',
+ openCreateModal: false,
+ };
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Repositories');
+ });
+
+ test('repo', () => {
+ element.params = {
+ view: Gerrit.Nav.View.REPO,
+ repoName: 'foo',
+ };
+ element._repoName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'foo');
+ });
+ });
+
+ test('repo access', () => {
+ element.params = {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ repoName: 'foo',
+ };
+ element._repoName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Access');
+ });
+ });
+
+ test('repo dashboards', () => {
+ element.params = {
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+ repoName: 'foo',
+ };
+ element._repoName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Dashboards');
+ });
+ });
+ });
+
+ suite('groups', () => {
+ setup(() => {
+ stub('gr-group', {
+ _loadGroup: () => Promise.resolve({}),
+ });
+ stub('gr-group-members', {
+ _loadGroupDetails: () => {},
+ });
+
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({
+ name: 'foo',
+ id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
+ }));
+ sandbox.stub(element.$.restAPI, 'getIsGroupOwner')
+ .returns(Promise.resolve(true));
return element.reload();
});
- suite('repos', () => {
- setup(() => {
- stub('gr-repo-access', {
- _repoChanged: () => {},
- });
- });
+ test('group list', () => {
+ element.params = {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ openCreateModal: false,
+ };
+ flushAsynchronousOperations();
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'Groups');
+ });
- test('repo list', () => {
- element.params = {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-repo-list',
- openCreateModal: false,
- };
+ test('internal group', () => {
+ element.params = {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 1234,
+ };
+ element._groupName = 'foo';
+ return element.reload().then(() => {
flushAsynchronousOperations();
+ const subsectionItems = dom(element.root)
+ .querySelectorAll('.subsectionItem');
+ assert.equal(subsectionItems.length, 2);
+ assert.isTrue(element._groupIsInternal);
const selected = element.shadowRoot
.querySelector('gr-page-nav .selected');
assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Repositories');
- });
-
- test('repo', () => {
- element.params = {
- view: Gerrit.Nav.View.REPO,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('repo access', () => {
- element.params = {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.ACCESS,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Access');
- });
- });
-
- test('repo dashboards', () => {
- element.params = {
- view: Gerrit.Nav.View.REPO,
- detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Dashboards');
- });
+ assert.equal(selected.textContent.trim(), 'foo');
});
});
- suite('groups', () => {
- setup(() => {
- stub('gr-group', {
- _loadGroup: () => Promise.resolve({}),
- });
- stub('gr-group-members', {
- _loadGroupDetails: () => {},
- });
-
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({
- name: 'foo',
- id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
- }));
- sandbox.stub(element.$.restAPI, 'getIsGroupOwner')
- .returns(Promise.resolve(true));
- return element.reload();
+ test('external group', () => {
+ element.$.restAPI.getGroupConfig.restore();
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({
+ name: 'foo',
+ id: 'external-id',
+ }));
+ element.params = {
+ view: Gerrit.Nav.View.GROUP,
+ groupId: 1234,
+ };
+ element._groupName = 'foo';
+ return element.reload().then(() => {
+ flushAsynchronousOperations();
+ const subsectionItems = dom(element.root)
+ .querySelectorAll('.subsectionItem');
+ assert.equal(subsectionItems.length, 0);
+ assert.isFalse(element._groupIsInternal);
+ const selected = element.shadowRoot
+ .querySelector('gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent.trim(), 'foo');
});
+ });
- test('group list', () => {
- element.params = {
- view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-group-list',
- openCreateModal: false,
- };
+ test('group members', () => {
+ element.params = {
+ view: Gerrit.Nav.View.GROUP,
+ detail: Gerrit.Nav.GroupDetailView.MEMBERS,
+ groupId: 1234,
+ };
+ element._groupName = 'foo';
+ return element.reload().then(() => {
flushAsynchronousOperations();
const selected = element.shadowRoot
.querySelector('gr-page-nav .selected');
assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Groups');
- });
-
- test('internal group', () => {
- element.params = {
- view: Gerrit.Nav.View.GROUP,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const subsectionItems = Polymer.dom(element.root)
- .querySelectorAll('.subsectionItem');
- assert.equal(subsectionItems.length, 2);
- assert.isTrue(element._groupIsInternal);
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('external group', () => {
- element.$.restAPI.getGroupConfig.restore();
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({
- name: 'foo',
- id: 'external-id',
- }));
- element.params = {
- view: Gerrit.Nav.View.GROUP,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const subsectionItems = Polymer.dom(element.root)
- .querySelectorAll('.subsectionItem');
- assert.equal(subsectionItems.length, 0);
- assert.isFalse(element._groupIsInternal);
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('group members', () => {
- element.params = {
- view: Gerrit.Nav.View.GROUP,
- detail: Gerrit.Nav.GroupDetailView.MEMBERS,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flushAsynchronousOperations();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Members');
- });
+ assert.equal(selected.textContent.trim(), 'Members');
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
index 3fde410..5ebf00e 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -14,67 +14,76 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DETAIL_TYPES = {
- BRANCHES: 'branches',
- ID: 'id',
- TAGS: 'tags',
- };
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.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-confirm-delete-item-dialog_html.js';
+
+const DETAIL_TYPES = {
+ BRANCHES: 'branches',
+ ID: 'id',
+ TAGS: 'tags',
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmDeleteItemDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-delete-item-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmDeleteItemDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-delete-item-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- item: String,
- itemType: String,
- };
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- _computeItemName(detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return 'Branch';
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return 'Tag';
- } else if (detailType === DETAIL_TYPES.ID) {
- return 'ID';
- }
- }
+ static get properties() {
+ return {
+ item: String,
+ itemType: String,
+ };
}
- customElements.define(GrConfirmDeleteItemDialog.is,
- GrConfirmDeleteItemDialog);
-})();
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ _computeItemName(detailType) {
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return 'Branch';
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return 'Tag';
+ } else if (detailType === DETAIL_TYPES.ID) {
+ return 'ID';
+ }
+ }
+}
+
+customElements.define(GrConfirmDeleteItemDialog.is,
+ GrConfirmDeleteItemDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
index 9d8ee18..12dc29c 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
@@ -1,38 +1,29 @@
-<!--
-@license
-Copyright (C) 2017 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="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-delete-item-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
width: 30em;
}
</style>
- <gr-dialog
- confirm-label="Delete [[_computeItemName(itemType)]]"
- confirm-on-enter
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Delete [[_computeItemName(itemType)]]" confirm-on-enter="" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div>
<div class="main" slot="main">
<label for="branchInput">
@@ -43,6 +34,4 @@
</div>
</div>
</gr-dialog>
- </template>
- <script src="gr-confirm-delete-item-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
index b937e76..e948a31 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-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-confirm-delete-item-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-confirm-delete-item-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-confirm-delete-item-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-delete-item-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,53 +40,55 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-delete-item-dialog 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-confirm-delete-item-dialog.js';
+suite('gr-confirm-delete-item-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_handleConfirmTap', () => {
- const confirmHandler = sandbox.stub();
- element.addEventListener('confirm', confirmHandler);
- sandbox.spy(element, '_handleConfirmTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('confirm');
- assert.isTrue(confirmHandler.called);
- assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(element._handleConfirmTap.called);
- assert.isTrue(element._handleConfirmTap.calledOnce);
- });
-
- test('_handleCancelTap', () => {
- const cancelHandler = sandbox.stub();
- element.addEventListener('cancel', cancelHandler);
- sandbox.spy(element, '_handleCancelTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('cancel');
- assert.isTrue(cancelHandler.called);
- assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(element._handleCancelTap.called);
- assert.isTrue(element._handleCancelTap.calledOnce);
- });
-
- test('_computeItemName function for branches', () => {
- assert.deepEqual(element._computeItemName('branches'), 'Branch');
- assert.notEqual(element._computeItemName('branches'), 'Tag');
- });
-
- test('_computeItemName function for tags', () => {
- assert.deepEqual(element._computeItemName('tags'), 'Tag');
- assert.notEqual(element._computeItemName('tags'), 'Branch');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sandbox.stub();
+ element.addEventListener('confirm', confirmHandler);
+ sandbox.spy(element, '_handleConfirmTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('confirm');
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(confirmHandler.calledOnce);
+ assert.isTrue(element._handleConfirmTap.called);
+ assert.isTrue(element._handleConfirmTap.calledOnce);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sandbox.stub();
+ element.addEventListener('cancel', cancelHandler);
+ sandbox.spy(element, '_handleCancelTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('cancel');
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(cancelHandler.calledOnce);
+ assert.isTrue(element._handleCancelTap.called);
+ assert.isTrue(element._handleCancelTap.calledOnce);
+ });
+
+ test('_computeItemName function for branches', () => {
+ assert.deepEqual(element._computeItemName('branches'), 'Branch');
+ assert.notEqual(element._computeItemName('branches'), 'Tag');
+ });
+
+ test('_computeItemName function for tags', () => {
+ assert.deepEqual(element._computeItemName('tags'), 'Tag');
+ assert.notEqual(element._computeItemName('tags'), 'Branch');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index 3b85304..94de228 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -14,148 +14,166 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const SUGGESTIONS_LIMIT = 15;
- const REF_PREFIX = 'refs/heads/';
+import '@polymer/iron-input/iron-input.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.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-create-change-dialog_html.js';
+const SUGGESTIONS_LIMIT = 15;
+const REF_PREFIX = 'refs/heads/';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreateChangeDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-repo-commands_test.
*/
- class GrCreateChangeDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-repo-commands_test.
- */
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-change-dialog'; }
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- repoName: String,
- branch: String,
- /** @type {?} */
- _repoConfig: Object,
- subject: String,
- topic: String,
- _query: {
- type: Function,
- value() {
- return this._getRepoBranchesSuggestions.bind(this);
- },
+ static get is() { return 'gr-create-change-dialog'; }
+
+ static get properties() {
+ return {
+ repoName: String,
+ branch: String,
+ /** @type {?} */
+ _repoConfig: Object,
+ subject: String,
+ topic: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getRepoBranchesSuggestions.bind(this);
},
- baseChange: String,
- baseCommit: String,
- privateByDefault: String,
- canCreate: {
- type: Boolean,
- notify: true,
- value: false,
- },
- _privateChangesEnabled: Boolean,
- };
+ },
+ baseChange: String,
+ baseCommit: String,
+ privateByDefault: String,
+ canCreate: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ _privateChangesEnabled: Boolean,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ if (!this.repoName) { return Promise.resolve(); }
+
+ const promises = [];
+
+ promises.push(this.$.restAPI.getProjectConfig(this.repoName)
+ .then(config => {
+ this.privateByDefault = config.private_by_default;
+ }));
+
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ if (!config) { return; }
+
+ this._privateConfig = config && config.change &&
+ config.change.disable_private_changes;
+ }));
+
+ return Promise.all(promises);
+ }
+
+ static get observers() {
+ return [
+ '_allowCreate(branch, subject)',
+ ];
+ }
+
+ _computeBranchClass(baseChange) {
+ return baseChange ? 'hide' : '';
+ }
+
+ _allowCreate(branch, subject) {
+ this.canCreate = !!branch && !!subject;
+ }
+
+ handleCreateChange() {
+ const isPrivate = this.$.privateChangeCheckBox.checked;
+ const isWip = true;
+ return this.$.restAPI.createChange(this.repoName, this.branch,
+ this.subject, this.topic, isPrivate, isWip, this.baseChange,
+ this.baseCommit || null)
+ .then(changeCreated => {
+ if (!changeCreated) { return; }
+ Gerrit.Nav.navigateToChange(changeCreated);
+ });
+ }
+
+ _getRepoBranchesSuggestions(input) {
+ if (input.startsWith(REF_PREFIX)) {
+ input = input.substring(REF_PREFIX.length);
}
-
- /** @override */
- attached() {
- super.attached();
- if (!this.repoName) { return Promise.resolve(); }
-
- const promises = [];
-
- promises.push(this.$.restAPI.getProjectConfig(this.repoName)
- .then(config => {
- this.privateByDefault = config.private_by_default;
- }));
-
- promises.push(this.$.restAPI.getConfig().then(config => {
- if (!config) { return; }
-
- this._privateConfig = config && config.change &&
- config.change.disable_private_changes;
- }));
-
- return Promise.all(promises);
- }
-
- static get observers() {
- return [
- '_allowCreate(branch, subject)',
- ];
- }
-
- _computeBranchClass(baseChange) {
- return baseChange ? 'hide' : '';
- }
-
- _allowCreate(branch, subject) {
- this.canCreate = !!branch && !!subject;
- }
-
- handleCreateChange() {
- const isPrivate = this.$.privateChangeCheckBox.checked;
- const isWip = true;
- return this.$.restAPI.createChange(this.repoName, this.branch,
- this.subject, this.topic, isPrivate, isWip, this.baseChange,
- this.baseCommit || null)
- .then(changeCreated => {
- if (!changeCreated) { return; }
- Gerrit.Nav.navigateToChange(changeCreated);
- });
- }
-
- _getRepoBranchesSuggestions(input) {
- if (input.startsWith(REF_PREFIX)) {
- input = input.substring(REF_PREFIX.length);
- }
- return this.$.restAPI.getRepoBranches(
- input, this.repoName, SUGGESTIONS_LIMIT).then(response => {
- const branches = [];
- let branch;
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- if (response[key].ref.startsWith('refs/heads/')) {
- branch = response[key].ref.substring('refs/heads/'.length);
- } else {
- branch = response[key].ref;
- }
- branches.push({
- name: branch,
- });
- }
- return branches;
- });
- }
-
- _formatBooleanString(config) {
- if (config && config.configured_value === 'TRUE') {
- return true;
- } else if (config && config.configured_value === 'FALSE') {
- return false;
- } else if (config && config.configured_value === 'INHERIT') {
- if (config && config.inherited_value) {
- return true;
+ return this.$.restAPI.getRepoBranches(
+ input, this.repoName, SUGGESTIONS_LIMIT).then(response => {
+ const branches = [];
+ let branch;
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ if (response[key].ref.startsWith('refs/heads/')) {
+ branch = response[key].ref.substring('refs/heads/'.length);
} else {
- return false;
+ branch = response[key].ref;
}
+ branches.push({
+ name: branch,
+ });
+ }
+ return branches;
+ });
+ }
+
+ _formatBooleanString(config) {
+ if (config && config.configured_value === 'TRUE') {
+ return true;
+ } else if (config && config.configured_value === 'FALSE') {
+ return false;
+ } else if (config && config.configured_value === 'INHERIT') {
+ if (config && config.inherited_value) {
+ return true;
} else {
return false;
}
- }
-
- _computePrivateSectionClass(config) {
- return config ? 'hide' : '';
+ } else {
+ return false;
}
}
- customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog);
-})();
+ _computePrivateSectionClass(config) {
+ return config ? 'hide' : '';
+ }
+}
+
+customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
index 1d6e706..8f11af3 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
@@ -1,36 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<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-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-create-change-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -53,76 +39,42 @@
}
</style>
<div class="gr-form-styles">
- <section class$="[[_computeBranchClass(baseChange)]]">
+ <section class\$="[[_computeBranchClass(baseChange)]]">
<span class="title">Select branch for new change</span>
<span class="value">
- <gr-autocomplete
- id="branchInput"
- text="{{branch}}"
- query="[[_query]]"
- placeholder="Destination branch">
+ <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch">
</gr-autocomplete>
</span>
</section>
- <section class$="[[_computeBranchClass(baseChange)]]">
+ <section class\$="[[_computeBranchClass(baseChange)]]">
<span class="title">Provide base commit sha1 for change</span>
<span class="value">
- <iron-input
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
- <input
- is="iron-input"
- id="baseCommitInput"
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
+ <iron-input maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
+ <input is="iron-input" id="baseCommitInput" maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
</iron-input>
</span>
</section>
<section>
<span class="title">Enter topic for new change</span>
<span class="value">
- <iron-input
- maxlength="1024"
- placeholder="(optional)"
- bind-value="{{topic}}">
- <input
- is="iron-input"
- id="tagNameInput"
- maxlength="1024"
- placeholder="(optional)"
- bind-value="{{topic}}">
+ <iron-input maxlength="1024" placeholder="(optional)" bind-value="{{topic}}">
+ <input is="iron-input" id="tagNameInput" maxlength="1024" placeholder="(optional)" bind-value="{{topic}}">
</iron-input>
</span>
</section>
<section id="description">
<span class="title">Description</span>
<span class="value">
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- rows="4"
- max-rows="15"
- bind-value="{{subject}}"
- placeholder="Insert the description of the change.">
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{subject}}" placeholder="Insert the description of the change.">
</iron-autogrow-textarea>
</span>
</section>
- <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
- <label
- class="title"
- for="privateChangeCheckBox">Private change</label>
+ <section class\$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
+ <label class="title" for="privateChangeCheckBox">Private change</label>
<span class="value">
- <input
- type="checkbox"
- id="privateChangeCheckBox"
- checked$="[[_formatBooleanString(privateByDefault)]]">
+ <input type="checkbox" id="privateChangeCheckBox" checked\$="[[_formatBooleanString(privateByDefault)]]">
</span>
</section>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-change-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
index aad2428f..20226dd 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-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-create-change-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-create-change-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-create-change-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-create-change-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,136 +40,138 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-change-dialog 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-create-change-dialog.js';
+suite('gr-create-change-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getRepoBranches(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- ref: 'refs/heads/test-branch',
- revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
- can_delete: true,
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- });
- element = fixture('basic');
- element.repoName = 'test-repo',
- element._repoConfig = {
- private_by_default: {
- configured_value: 'FALSE',
- inherited_value: false,
- },
- };
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getRepoBranches(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ ref: 'refs/heads/test-branch',
+ revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+ can_delete: true,
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
});
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('new change created with default', done => {
- const configInputObj = {
- branch: 'test-branch',
- subject: 'first change created with polygerrit ui',
- topic: 'test-topic',
- is_private: false,
- work_in_progress: true,
- };
-
- const saveStub = sandbox.stub(element.$.restAPI,
- 'createChange', () => Promise.resolve({}));
-
- element.branch = 'test-branch';
- element.topic = 'test-topic';
- element.subject = 'first change created with polygerrit ui';
- assert.isFalse(element.$.privateChangeCheckBox.checked);
-
- element.$.branchInput.bindValue = configInputObj.branch;
- element.$.tagNameInput.bindValue = configInputObj.topic;
- element.$.messageInput.bindValue = configInputObj.subject;
-
- element.handleCreateChange().then(() => {
- // Private change
- assert.isFalse(saveStub.lastCall.args[4]);
- // WIP Change
- assert.isTrue(saveStub.lastCall.args[5]);
- assert.isTrue(saveStub.called);
- done();
- });
- });
-
- test('new change created with private', done => {
- element.privateByDefault = {
- configured_value: 'TRUE',
+ element = fixture('basic');
+ element.repoName = 'test-repo',
+ element._repoConfig = {
+ private_by_default: {
+ configured_value: 'FALSE',
inherited_value: false,
- };
- sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true));
- flushAsynchronousOperations();
+ },
+ };
+ });
- const configInputObj = {
- branch: 'test-branch',
- subject: 'first change created with polygerrit ui',
- topic: 'test-topic',
- is_private: true,
- work_in_progress: true,
- };
+ teardown(() => {
+ sandbox.restore();
+ });
- const saveStub = sandbox.stub(element.$.restAPI,
- 'createChange', () => Promise.resolve({}));
+ test('new change created with default', done => {
+ const configInputObj = {
+ branch: 'test-branch',
+ subject: 'first change created with polygerrit ui',
+ topic: 'test-topic',
+ is_private: false,
+ work_in_progress: true,
+ };
- element.branch = 'test-branch';
- element.topic = 'test-topic';
- element.subject = 'first change created with polygerrit ui';
- assert.isTrue(element.$.privateChangeCheckBox.checked);
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'createChange', () => Promise.resolve({}));
- element.$.branchInput.bindValue = configInputObj.branch;
- element.$.tagNameInput.bindValue = configInputObj.topic;
- element.$.messageInput.bindValue = configInputObj.subject;
+ element.branch = 'test-branch';
+ element.topic = 'test-topic';
+ element.subject = 'first change created with polygerrit ui';
+ assert.isFalse(element.$.privateChangeCheckBox.checked);
- element.handleCreateChange().then(() => {
- // Private change
- assert.isTrue(saveStub.lastCall.args[4]);
- // WIP Change
- assert.isTrue(saveStub.lastCall.args[5]);
- assert.isTrue(saveStub.called);
- done();
- });
- });
+ element.$.branchInput.bindValue = configInputObj.branch;
+ element.$.tagNameInput.bindValue = configInputObj.topic;
+ element.$.messageInput.bindValue = configInputObj.subject;
- test('_getRepoBranchesSuggestions empty', done => {
- element._getRepoBranchesSuggestions('nonexistent').then(branches => {
- assert.equal(branches.length, 0);
- done();
- });
- });
-
- test('_getRepoBranchesSuggestions non-empty', done => {
- element._getRepoBranchesSuggestions('test-branch').then(branches => {
- assert.equal(branches.length, 1);
- assert.equal(branches[0].name, 'test-branch');
- done();
- });
- });
-
- test('_computeBranchClass', () => {
- assert.equal(element._computeBranchClass(true), 'hide');
- assert.equal(element._computeBranchClass(false), '');
- });
-
- test('_computePrivateSectionClass', () => {
- assert.equal(element._computePrivateSectionClass(true), 'hide');
- assert.equal(element._computePrivateSectionClass(false), '');
+ element.handleCreateChange().then(() => {
+ // Private change
+ assert.isFalse(saveStub.lastCall.args[4]);
+ // WIP Change
+ assert.isTrue(saveStub.lastCall.args[5]);
+ assert.isTrue(saveStub.called);
+ done();
});
});
+
+ test('new change created with private', done => {
+ element.privateByDefault = {
+ configured_value: 'TRUE',
+ inherited_value: false,
+ };
+ sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true));
+ flushAsynchronousOperations();
+
+ const configInputObj = {
+ branch: 'test-branch',
+ subject: 'first change created with polygerrit ui',
+ topic: 'test-topic',
+ is_private: true,
+ work_in_progress: true,
+ };
+
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'createChange', () => Promise.resolve({}));
+
+ element.branch = 'test-branch';
+ element.topic = 'test-topic';
+ element.subject = 'first change created with polygerrit ui';
+ assert.isTrue(element.$.privateChangeCheckBox.checked);
+
+ element.$.branchInput.bindValue = configInputObj.branch;
+ element.$.tagNameInput.bindValue = configInputObj.topic;
+ element.$.messageInput.bindValue = configInputObj.subject;
+
+ element.handleCreateChange().then(() => {
+ // Private change
+ assert.isTrue(saveStub.lastCall.args[4]);
+ // WIP Change
+ assert.isTrue(saveStub.lastCall.args[5]);
+ assert.isTrue(saveStub.called);
+ done();
+ });
+ });
+
+ test('_getRepoBranchesSuggestions empty', done => {
+ element._getRepoBranchesSuggestions('nonexistent').then(branches => {
+ assert.equal(branches.length, 0);
+ done();
+ });
+ });
+
+ test('_getRepoBranchesSuggestions non-empty', done => {
+ element._getRepoBranchesSuggestions('test-branch').then(branches => {
+ assert.equal(branches.length, 1);
+ assert.equal(branches[0].name, 'test-branch');
+ done();
+ });
+ });
+
+ test('_computeBranchClass', () => {
+ assert.equal(element._computeBranchClass(true), 'hide');
+ assert.equal(element._computeBranchClass(false), '');
+ });
+
+ test('_computePrivateSectionClass', () => {
+ assert.equal(element._computePrivateSectionClass(true), 'hide');
+ assert.equal(element._computePrivateSectionClass(false), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index 8a4edab..0860fdb 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -14,65 +14,77 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCreateGroupDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-group-dialog'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-create-group-dialog_html.js';
- static get properties() {
- return {
- params: Object,
- hasNewGroupName: {
- type: Boolean,
- notify: true,
- value: false,
- },
- _name: Object,
- _groupCreated: {
- type: Boolean,
- value: false,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreateGroupDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_updateGroupName(_name)',
- ];
- }
+ static get is() { return 'gr-create-group-dialog'; }
- _computeGroupUrl(groupId) {
- return this.getBaseUrl() + '/admin/groups/' +
- this.encodeURL(groupId, true);
- }
-
- _updateGroupName(name) {
- this.hasNewGroupName = !!name;
- }
-
- handleCreateGroup() {
- return this.$.restAPI.createGroup({name: this._name})
- .then(groupRegistered => {
- if (groupRegistered.status !== 201) { return; }
- this._groupCreated = true;
- return this.$.restAPI.getGroupConfig(this._name)
- .then(group => {
- page.show(this._computeGroupUrl(group.group_id));
- });
- });
- }
+ static get properties() {
+ return {
+ params: Object,
+ hasNewGroupName: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ _name: Object,
+ _groupCreated: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog);
-})();
+ static get observers() {
+ return [
+ '_updateGroupName(_name)',
+ ];
+ }
+
+ _computeGroupUrl(groupId) {
+ return this.getBaseUrl() + '/admin/groups/' +
+ this.encodeURL(groupId, true);
+ }
+
+ _updateGroupName(name) {
+ this.hasNewGroupName = !!name;
+ }
+
+ handleCreateGroup() {
+ return this.$.restAPI.createGroup({name: this._name})
+ .then(groupRegistered => {
+ if (groupRegistered.status !== 201) { return; }
+ this._groupCreated = true;
+ return this.$.restAPI.getGroupConfig(this._name)
+ .then(group => {
+ page.show(this._computeGroupUrl(group.group_id));
+ });
+ });
+ }
+}
+
+customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
index d0a1fca..2cdde81 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
@@ -1,31 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-create-group-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -41,16 +32,11 @@
<div id="form">
<section>
<span class="title">Group name</span>
- <iron-input
- bind-value="{{_name}}">
- <input
- is="iron-input"
- bind-value="{{_name}}">
+ <iron-input bind-value="{{_name}}">
+ <input is="iron-input" bind-value="{{_name}}">
</iron-input>
</section>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-group-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
index d630556..e9585d3 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-group-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/page/page.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-create-group-dialog.html">
+<script src="/node_modules/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 type="module" src="./gr-create-group-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-create-group-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,66 +41,68 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-group-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const GROUP_NAME = 'test-group';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-create-group-dialog.js';
+suite('gr-create-group-dialog tests', () => {
+ let element;
+ let sandbox;
+ const GROUP_NAME = 'test-group';
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('name is updated correctly', done => {
- assert.isFalse(element.hasNewGroupName);
+ test('name is updated correctly', done => {
+ assert.isFalse(element.hasNewGroupName);
- const inputEl = element.root.querySelector('iron-input');
- inputEl.bindValue = GROUP_NAME;
+ const inputEl = element.root.querySelector('iron-input');
+ inputEl.bindValue = GROUP_NAME;
- setTimeout(() => {
- assert.isTrue(element.hasNewGroupName);
- assert.deepEqual(element._name, GROUP_NAME);
- done();
- });
- });
-
- test('test for redirecting to group on successful creation', done => {
- sandbox.stub(element.$.restAPI, 'createGroup')
- .returns(Promise.resolve({status: 201}));
-
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({group_id: 551}));
-
- const showStub = sandbox.stub(page, 'show');
- element.handleCreateGroup()
- .then(() => {
- assert.isTrue(showStub.calledWith('/admin/groups/551'));
- done();
- });
- });
-
- test('test for unsuccessful group creation', done => {
- sandbox.stub(element.$.restAPI, 'createGroup')
- .returns(Promise.resolve({status: 409}));
-
- sandbox.stub(element.$.restAPI, 'getGroupConfig')
- .returns(Promise.resolve({group_id: 551}));
-
- const showStub = sandbox.stub(page, 'show');
- element.handleCreateGroup()
- .then(() => {
- assert.isFalse(showStub.called);
- done();
- });
+ setTimeout(() => {
+ assert.isTrue(element.hasNewGroupName);
+ assert.deepEqual(element._name, GROUP_NAME);
+ done();
});
});
+
+ test('test for redirecting to group on successful creation', done => {
+ sandbox.stub(element.$.restAPI, 'createGroup')
+ .returns(Promise.resolve({status: 201}));
+
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({group_id: 551}));
+
+ const showStub = sandbox.stub(page, 'show');
+ element.handleCreateGroup()
+ .then(() => {
+ assert.isTrue(showStub.calledWith('/admin/groups/551'));
+ done();
+ });
+ });
+
+ test('test for unsuccessful group creation', done => {
+ sandbox.stub(element.$.restAPI, 'createGroup')
+ .returns(Promise.resolve({status: 409}));
+
+ sandbox.stub(element.$.restAPI, 'getGroupConfig')
+ .returns(Promise.resolve({group_id: 551}));
+
+ const showStub = sandbox.stub(page, 'show');
+ element.handleCreateGroup()
+ .then(() => {
+ assert.isFalse(showStub.called);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index 2d6b4aa..40ddb66 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -14,89 +14,103 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DETAIL_TYPES = {
- branches: 'branches',
- tags: 'tags',
- };
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.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-create-pointer-dialog_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCreatePointerDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-pointer-dialog'; }
+const DETAIL_TYPES = {
+ branches: 'branches',
+ tags: 'tags',
+};
- static get properties() {
- return {
- detailType: String,
- repoName: String,
- hasNewItemName: {
- type: Boolean,
- notify: true,
- value: false,
- },
- itemDetail: String,
- _itemName: String,
- _itemRevision: String,
- _itemAnnotation: String,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreatePointerDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_updateItemName(_itemName)',
- ];
- }
+ static get is() { return 'gr-create-pointer-dialog'; }
- _updateItemName(name) {
- this.hasNewItemName = !!name;
- }
+ static get properties() {
+ return {
+ detailType: String,
+ repoName: String,
+ hasNewItemName: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ itemDetail: String,
+ _itemName: String,
+ _itemRevision: String,
+ _itemAnnotation: String,
+ };
+ }
- _computeItemUrl(project) {
- if (this.itemDetail === DETAIL_TYPES.branches) {
- return this.getBaseUrl() + '/admin/repos/' +
- this.encodeURL(this.repoName, true) + ',branches';
- } else if (this.itemDetail === DETAIL_TYPES.tags) {
- return this.getBaseUrl() + '/admin/repos/' +
- this.encodeURL(this.repoName, true) + ',tags';
- }
- }
+ static get observers() {
+ return [
+ '_updateItemName(_itemName)',
+ ];
+ }
- handleCreateItem() {
- const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
- if (this.itemDetail === DETAIL_TYPES.branches) {
- return this.$.restAPI.createRepoBranch(this.repoName,
- this._itemName, {revision: USE_HEAD})
- .then(itemRegistered => {
- if (itemRegistered.status === 201) {
- page.show(this._computeItemUrl(this.itemDetail));
- }
- });
- } else if (this.itemDetail === DETAIL_TYPES.tags) {
- return this.$.restAPI.createRepoTag(this.repoName,
- this._itemName,
- {revision: USE_HEAD, message: this._itemAnnotation || null})
- .then(itemRegistered => {
- if (itemRegistered.status === 201) {
- page.show(this._computeItemUrl(this.itemDetail));
- }
- });
- }
- }
+ _updateItemName(name) {
+ this.hasNewItemName = !!name;
+ }
- _computeHideItemClass(type) {
- return type === DETAIL_TYPES.branches ? 'hideItem' : '';
+ _computeItemUrl(project) {
+ if (this.itemDetail === DETAIL_TYPES.branches) {
+ return this.getBaseUrl() + '/admin/repos/' +
+ this.encodeURL(this.repoName, true) + ',branches';
+ } else if (this.itemDetail === DETAIL_TYPES.tags) {
+ return this.getBaseUrl() + '/admin/repos/' +
+ this.encodeURL(this.repoName, true) + ',tags';
}
}
- customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog);
-})();
+ handleCreateItem() {
+ const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
+ if (this.itemDetail === DETAIL_TYPES.branches) {
+ return this.$.restAPI.createRepoBranch(this.repoName,
+ this._itemName, {revision: USE_HEAD})
+ .then(itemRegistered => {
+ if (itemRegistered.status === 201) {
+ page.show(this._computeItemUrl(this.itemDetail));
+ }
+ });
+ } else if (this.itemDetail === DETAIL_TYPES.tags) {
+ return this.$.restAPI.createRepoTag(this.repoName,
+ this._itemName,
+ {revision: USE_HEAD, message: this._itemAnnotation || null})
+ .then(itemRegistered => {
+ if (itemRegistered.status === 201) {
+ page.show(this._computeItemUrl(this.itemDetail));
+ }
+ });
+ }
+ }
+
+ _computeHideItemClass(type) {
+ return type === DETAIL_TYPES.branches ? 'hideItem' : '';
+ }
+}
+
+customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
index d1980a5..3a6df2f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
@@ -1,33 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-create-pointer-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -49,41 +38,23 @@
<div id="form">
<section id="itemNameSection">
<span class="title">[[detailType]] name</span>
- <iron-input
- placeholder="[[detailType]] Name"
- bind-value="{{_itemName}}">
- <input
- is="iron-input"
- placeholder="[[detailType]] Name"
- bind-value="{{_itemName}}">
+ <iron-input placeholder="[[detailType]] Name" bind-value="{{_itemName}}">
+ <input is="iron-input" placeholder="[[detailType]] Name" bind-value="{{_itemName}}">
</iron-input>
</section>
<section id="itemRevisionSection">
<span class="title">Initial Revision</span>
- <iron-input
- placeholder="Revision (Branch or SHA-1)"
- bind-value="{{_itemRevision}}">
- <input
- is="iron-input"
- placeholder="Revision (Branch or SHA-1)"
- bind-value="{{_itemRevision}}">
+ <iron-input placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}">
+ <input is="iron-input" placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}">
</iron-input>
</section>
- <section id="itemAnnotationSection"
- class$="[[_computeHideItemClass(itemDetail)]]">
+ <section id="itemAnnotationSection" class\$="[[_computeHideItemClass(itemDetail)]]">
<span class="title">Annotation</span>
- <iron-input
- placeholder="Annotation (Optional)"
- bind-value="{{_itemAnnotation}}">
- <input
- is="iron-input"
- placeholder="Annotation (Optional)"
- bind-value="{{_itemAnnotation}}">
+ <iron-input placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}">
+ <input is="iron-input" placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}">
</iron-input>
</section>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-pointer-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
index db33587..28cf2e8 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-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-create-pointer-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-create-pointer-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-create-pointer-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-create-pointer-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,103 +40,106 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-pointer-dialog 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-create-pointer-dialog.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-create-pointer-dialog tests', () => {
+ let element;
+ let sandbox;
- const ironInput = function(element) {
- return Polymer.dom(element).querySelector('iron-input');
- };
+ const ironInput = function(element) {
+ return dom(element).querySelector('iron-input');
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('branch created', done => {
- sandbox.stub(
- element.$.restAPI,
- 'createRepoBranch',
- () => Promise.resolve({}));
+ test('branch created', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'createRepoBranch',
+ () => Promise.resolve({}));
- assert.isFalse(element.hasNewItemName);
+ assert.isFalse(element.hasNewItemName);
- element._itemName = 'test-branch';
- element.itemDetail = 'branches';
+ element._itemName = 'test-branch';
+ element.itemDetail = 'branches';
- ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+ ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
+ ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
- setTimeout(() => {
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-branch2');
- assert.equal(element._itemRevision, 'HEAD');
- done();
- });
- });
-
- test('tag created', done => {
- sandbox.stub(
- element.$.restAPI,
- 'createRepoTag',
- () => Promise.resolve({}));
-
- assert.isFalse(element.hasNewItemName);
-
- element._itemName = 'test-tag';
- element.itemDetail = 'tags';
-
- ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
-
- setTimeout(() => {
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-tag2');
- assert.equal(element._itemRevision, 'HEAD');
- done();
- });
- });
-
- test('tag created with annotations', done => {
- sandbox.stub(
- element.$.restAPI,
- 'createRepoTag',
- () => Promise.resolve({}));
-
- assert.isFalse(element.hasNewItemName);
-
- element._itemName = 'test-tag';
- element._itemAnnotation = 'test-message';
- element.itemDetail = 'tags';
-
- ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
- ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
-
- setTimeout(() => {
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-tag2');
- assert.equal(element._itemAnnotation, 'test-message2');
- assert.equal(element._itemRevision, 'HEAD');
- done();
- });
- });
-
- test('_computeHideItemClass returns hideItem if type is branches', () => {
- assert.equal(element._computeHideItemClass('branches'), 'hideItem');
- });
-
- test('_computeHideItemClass returns strings if not branches', () => {
- assert.equal(element._computeHideItemClass('tags'), '');
+ setTimeout(() => {
+ assert.isTrue(element.hasNewItemName);
+ assert.equal(element._itemName, 'test-branch2');
+ assert.equal(element._itemRevision, 'HEAD');
+ done();
});
});
+
+ test('tag created', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'createRepoTag',
+ () => Promise.resolve({}));
+
+ assert.isFalse(element.hasNewItemName);
+
+ element._itemName = 'test-tag';
+ element.itemDetail = 'tags';
+
+ ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+ ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+
+ setTimeout(() => {
+ assert.isTrue(element.hasNewItemName);
+ assert.equal(element._itemName, 'test-tag2');
+ assert.equal(element._itemRevision, 'HEAD');
+ done();
+ });
+ });
+
+ test('tag created with annotations', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'createRepoTag',
+ () => Promise.resolve({}));
+
+ assert.isFalse(element.hasNewItemName);
+
+ element._itemName = 'test-tag';
+ element._itemAnnotation = 'test-message';
+ element.itemDetail = 'tags';
+
+ ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
+ ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
+ ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+
+ setTimeout(() => {
+ assert.isTrue(element.hasNewItemName);
+ assert.equal(element._itemName, 'test-tag2');
+ assert.equal(element._itemAnnotation, 'test-message2');
+ assert.equal(element._itemRevision, 'HEAD');
+ done();
+ });
+ });
+
+ test('_computeHideItemClass returns hideItem if type is branches', () => {
+ assert.equal(element._computeHideItemClass('branches'), 'hideItem');
+ });
+
+ test('_computeHideItemClass returns strings if not branches', () => {
+ assert.equal(element._computeHideItemClass('tags'), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index 290f025..7a77874 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -14,130 +14,145 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCreateRepoDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-create-repo-dialog'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.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-create-repo-dialog_html.js';
- static get properties() {
- return {
- params: Object,
- hasNewRepoName: {
- type: Boolean,
- notify: true,
- value: false,
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCreateRepoDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-create-repo-dialog'; }
+
+ static get properties() {
+ return {
+ params: Object,
+ hasNewRepoName: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+
+ /** @type {?} */
+ _repoConfig: {
+ type: Object,
+ value: () => {
+ // Set default values for dropdowns.
+ return {
+ create_empty_commit: true,
+ permissions_only: false,
+ };
},
+ },
+ _repoCreated: {
+ type: Boolean,
+ value: false,
+ },
+ _repoOwner: String,
+ _repoOwnerId: {
+ type: String,
+ observer: '_repoOwnerIdUpdate',
+ },
- /** @type {?} */
- _repoConfig: {
- type: Object,
- value: () => {
- // Set default values for dropdowns.
- return {
- create_empty_commit: true,
- permissions_only: false,
- };
- },
+ _query: {
+ type: Function,
+ value() {
+ return this._getRepoSuggestions.bind(this);
},
- _repoCreated: {
- type: Boolean,
- value: false,
+ },
+ _queryGroups: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- _repoOwner: String,
- _repoOwnerId: {
- type: String,
- observer: '_repoOwnerIdUpdate',
- },
+ },
+ };
+ }
- _query: {
- type: Function,
- value() {
- return this._getRepoSuggestions.bind(this);
- },
- },
- _queryGroups: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- };
- }
+ static get observers() {
+ return [
+ '_updateRepoName(_repoConfig.name)',
+ ];
+ }
- static get observers() {
- return [
- '_updateRepoName(_repoConfig.name)',
- ];
- }
+ _computeRepoUrl(repoName) {
+ return this.getBaseUrl() + '/admin/repos/' +
+ this.encodeURL(repoName, true);
+ }
- _computeRepoUrl(repoName) {
- return this.getBaseUrl() + '/admin/repos/' +
- this.encodeURL(repoName, true);
- }
+ _updateRepoName(name) {
+ this.hasNewRepoName = !!name;
+ }
- _updateRepoName(name) {
- this.hasNewRepoName = !!name;
- }
-
- _repoOwnerIdUpdate(id) {
- if (id) {
- this.set('_repoConfig.owners', [id]);
- } else {
- this.set('_repoConfig.owners', undefined);
- }
- }
-
- handleCreateRepo() {
- return this.$.restAPI.createRepo(this._repoConfig)
- .then(repoRegistered => {
- if (repoRegistered.status === 201) {
- this._repoCreated = true;
- page.show(this._computeRepoUrl(this._repoConfig.name));
- }
- });
- }
-
- _getRepoSuggestions(input) {
- return this.$.restAPI.getSuggestedProjects(input)
- .then(response => {
- const repos = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- repos.push({
- name: key,
- value: response[key],
- });
- }
- return repos;
- });
- }
-
- _getGroupSuggestions(input) {
- return this.$.restAPI.getSuggestedGroups(input)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: decodeURIComponent(response[key].id),
- });
- }
- return groups;
- });
+ _repoOwnerIdUpdate(id) {
+ if (id) {
+ this.set('_repoConfig.owners', [id]);
+ } else {
+ this.set('_repoConfig.owners', undefined);
}
}
- customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog);
-})();
+ handleCreateRepo() {
+ return this.$.restAPI.createRepo(this._repoConfig)
+ .then(repoRegistered => {
+ if (repoRegistered.status === 201) {
+ this._repoCreated = true;
+ page.show(this._computeRepoUrl(this._repoConfig.name));
+ }
+ });
+ }
+
+ _getRepoSuggestions(input) {
+ return this.$.restAPI.getSuggestedProjects(input)
+ .then(response => {
+ const repos = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ repos.push({
+ name: key,
+ value: response[key],
+ });
+ }
+ return repos;
+ });
+ }
+
+ _getGroupSuggestions(input) {
+ return this.$.restAPI.getSuggestedGroups(input)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ return groups;
+ });
+ }
+}
+
+customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
index b78090c..65666ea 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
@@ -1,34 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-create-repo-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -48,42 +36,28 @@
<div id="form">
<section>
<span class="title">Repository name</span>
- <iron-input autocomplete="on"
- bind-value="{{_repoConfig.name}}">
- <input is="iron-input"
- id="repoNameInput"
- autocomplete="on"
- bind-value="{{_repoConfig.name}}">
+ <iron-input autocomplete="on" bind-value="{{_repoConfig.name}}">
+ <input is="iron-input" id="repoNameInput" autocomplete="on" bind-value="{{_repoConfig.name}}">
</iron-input>
</section>
<section>
<span class="title">Rights inherit from</span>
<span class="value">
- <gr-autocomplete
- id="rightsInheritFromInput"
- text="{{_repoConfig.parent}}"
- query="[[_query]]"
- placeholder="Optional, defaults to 'All-Projects'">
+ <gr-autocomplete id="rightsInheritFromInput" text="{{_repoConfig.parent}}" query="[[_query]]" placeholder="Optional, defaults to 'All-Projects'">
</gr-autocomplete>
</span>
</section>
<section>
<span class="title">Owner</span>
<span class="value">
- <gr-autocomplete
- id="ownerInput"
- text="{{_repoOwner}}"
- value="{{_repoOwnerId}}"
- query="[[_queryGroups]]">
+ <gr-autocomplete id="ownerInput" text="{{_repoOwner}}" value="{{_repoOwnerId}}" query="[[_queryGroups]]">
</gr-autocomplete>
</span>
</section>
<section>
<span class="title">Create initial empty commit</span>
<span class="value">
- <gr-select
- id="initialCommit"
- bind-value="{{_repoConfig.create_empty_commit}}">
+ <gr-select id="initialCommit" bind-value="{{_repoConfig.create_empty_commit}}">
<select>
<option value="false">False</option>
<option value="true">True</option>
@@ -94,9 +68,7 @@
<section>
<span class="title">Only serve as parent for other repositories</span>
<span class="value">
- <gr-select
- id="parentRepo"
- bind-value="{{_repoConfig.permissions_only}}">
+ <gr-select id="parentRepo" bind-value="{{_repoConfig.permissions_only}}">
<select>
<option value="false">False</option>
<option value="true">True</option>
@@ -107,6 +79,4 @@
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-create-repo-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
index 578c074..09bb63e 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-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-create-repo-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-create-repo-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-create-repo-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-create-repo-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,74 +40,76 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-repo-dialog 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-create-repo-dialog.js';
+suite('gr-create-repo-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('default values are populated', () => {
- assert.isTrue(element.$.initialCommit.bindValue);
- assert.isFalse(element.$.parentRepo.bindValue);
- });
+ test('default values are populated', () => {
+ assert.isTrue(element.$.initialCommit.bindValue);
+ assert.isFalse(element.$.parentRepo.bindValue);
+ });
- test('repo created', done => {
- const configInputObj = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- owners: ['testId'],
- };
+ test('repo created', done => {
+ const configInputObj = {
+ name: 'test-repo',
+ create_empty_commit: true,
+ parent: 'All-Project',
+ permissions_only: false,
+ owners: ['testId'],
+ };
- const saveStub = sandbox.stub(element.$.restAPI,
- 'createRepo', () => Promise.resolve({}));
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'createRepo', () => Promise.resolve({}));
- assert.isFalse(element.hasNewRepoName);
+ assert.isFalse(element.hasNewRepoName);
- element._repoConfig = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
+ element._repoConfig = {
+ name: 'test-repo',
+ create_empty_commit: true,
+ parent: 'All-Project',
+ permissions_only: false,
+ };
- element._repoOwner = 'test';
- element._repoOwnerId = 'testId';
+ element._repoOwner = 'test';
+ element._repoOwnerId = 'testId';
- element.$.repoNameInput.bindValue = configInputObj.name;
- element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
- element.$.ownerInput.text = configInputObj.owners[0];
- element.$.initialCommit.bindValue =
- configInputObj.create_empty_commit;
- element.$.parentRepo.bindValue =
- configInputObj.permissions_only;
+ element.$.repoNameInput.bindValue = configInputObj.name;
+ element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
+ element.$.ownerInput.text = configInputObj.owners[0];
+ element.$.initialCommit.bindValue =
+ configInputObj.create_empty_commit;
+ element.$.parentRepo.bindValue =
+ configInputObj.permissions_only;
- assert.isTrue(element.hasNewRepoName);
+ assert.isTrue(element.hasNewRepoName);
- assert.deepEqual(element._repoConfig, configInputObj);
+ assert.deepEqual(element._repoConfig, configInputObj);
- element.handleCreateRepo().then(() => {
- assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
- done();
- });
- });
-
- test('testing observer of _repoOwner', () => {
- element._repoOwnerId = 'test-5';
- assert.deepEqual(element._repoConfig.owners, ['test-5']);
+ element.handleCreateRepo().then(() => {
+ assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
+ done();
});
});
+
+ test('testing observer of _repoOwner', () => {
+ element._repoOwnerId = 'test-5';
+ assert.deepEqual(element._repoConfig.owners, ['test-5']);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
index 11517d6..81c9cde 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -14,113 +14,127 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
- const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-account-link/gr-account-link.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-group-audit-log_html.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
- */
- class GrGroupAuditLog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-group-audit-log'; }
+const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
- static get properties() {
- return {
- groupId: String,
- _auditLog: Array,
- _loading: {
- type: Boolean,
- value: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrGroupAuditLog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.fire('title-change', {title: 'Audit Log'});
- }
+ static get is() { return 'gr-group-audit-log'; }
- /** @override */
- ready() {
- super.ready();
- this._getAuditLogs();
- }
-
- _getAuditLogs() {
- if (!this.groupId) { return ''; }
-
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- return this.$.restAPI.getGroupAuditLog(this.groupId, errFn)
- .then(auditLog => {
- if (!auditLog) {
- this._auditLog = [];
- return;
- }
- this._auditLog = auditLog;
- this._loading = false;
- });
- }
-
- _status(item) {
- return item.disabled ? 'Disabled' : 'Enabled';
- }
-
- itemType(type) {
- let item;
- switch (type) {
- case 'ADD_GROUP':
- case 'ADD_USER':
- item = 'Added';
- break;
- case 'REMOVE_GROUP':
- case 'REMOVE_USER':
- item = 'Removed';
- break;
- default:
- item = '';
- }
- return item;
- }
-
- _isGroupEvent(type) {
- return GROUP_EVENTS.indexOf(type) !== -1;
- }
-
- _computeGroupUrl(group) {
- if (group && group.url && group.id) {
- return Gerrit.Nav.getUrlForGroup(group.id);
- }
-
- return '';
- }
-
- _getIdForUser(account) {
- return account._account_id ? ' (' + account._account_id + ')' : '';
- }
-
- _getNameForGroup(group) {
- if (group && group.name) {
- return group.name;
- } else if (group && group.id) {
- // The URL encoded id of the member
- return decodeURIComponent(group.id);
- }
-
- return '';
- }
+ static get properties() {
+ return {
+ groupId: String,
+ _auditLog: Array,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ };
}
- customElements.define(GrGroupAuditLog.is, GrGroupAuditLog);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.fire('title-change', {title: 'Audit Log'});
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ this._getAuditLogs();
+ }
+
+ _getAuditLogs() {
+ if (!this.groupId) { return ''; }
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ return this.$.restAPI.getGroupAuditLog(this.groupId, errFn)
+ .then(auditLog => {
+ if (!auditLog) {
+ this._auditLog = [];
+ return;
+ }
+ this._auditLog = auditLog;
+ this._loading = false;
+ });
+ }
+
+ _status(item) {
+ return item.disabled ? 'Disabled' : 'Enabled';
+ }
+
+ itemType(type) {
+ let item;
+ switch (type) {
+ case 'ADD_GROUP':
+ case 'ADD_USER':
+ item = 'Added';
+ break;
+ case 'REMOVE_GROUP':
+ case 'REMOVE_USER':
+ item = 'Removed';
+ break;
+ default:
+ item = '';
+ }
+ return item;
+ }
+
+ _isGroupEvent(type) {
+ return GROUP_EVENTS.indexOf(type) !== -1;
+ }
+
+ _computeGroupUrl(group) {
+ if (group && group.url && group.id) {
+ return Gerrit.Nav.getUrlForGroup(group.id);
+ }
+
+ return '';
+ }
+
+ _getIdForUser(account) {
+ return account._account_id ? ' (' + account._account_id + ')' : '';
+ }
+
+ _getNameForGroup(group) {
+ if (group && group.name) {
+ return group.name;
+ } else if (group && group.id) {
+ // The URL encoded id of the member
+ return decodeURIComponent(group.id);
+ }
+
+ return '';
+ }
+}
+
+customElements.define(GrGroupAuditLog.is, GrGroupAuditLog);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
index 4ed751d..0958e7c 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
@@ -1,32 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-list-view-behavior/gr-list-view-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="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-
-<dom-module id="gr-group-audit-log">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -38,28 +28,26 @@
}
</style>
<table id="list" class="genericList">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="date topHeader">Date</th>
<th class="type topHeader">Type</th>
<th class="member topHeader">Member</th>
<th class="by-user topHeader">By User</th>
</tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_auditLog]]">
<tr class="table">
<td class="date">
- <gr-date-formatter
- has-tooltip
- date-str="[[item.date]]">
+ <gr-date-formatter has-tooltip="" date-str="[[item.date]]">
</gr-date-formatter>
</td>
<td class="type">[[itemType(item.type)]]</td>
<td class="member">
<template is="dom-if" if="[[_isGroupEvent(item.type)]]">
- <a href$="[[_computeGroupUrl(item.member)]]">
+ <a href\$="[[_computeGroupUrl(item.member)]]">
[[_getNameForGroup(item.member)]]
</a>
</template>
@@ -77,6 +65,4 @@
</tbody>
</table>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group-audit-log.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
index 3a75611..d62874d 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_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-group-audit-log</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-group-audit-log.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-group-audit-log.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-group-audit-log.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,85 +40,87 @@
</template>
</test-fixture>
-<script>
- suite('gr-group-audit-log 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-group-audit-log.js';
+suite('gr-group-audit-log tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('members', () => {
+ test('test _getNameForGroup', () => {
+ let group = {
+ member: {
+ name: 'test-name',
+ },
+ };
+ assert.equal(element._getNameForGroup(group.member), 'test-name');
+
+ group = {
+ member: {
+ id: 'test-id',
+ },
+ };
+ assert.equal(element._getNameForGroup(group.member), 'test-id');
});
- teardown(() => {
- sandbox.restore();
- });
+ test('test _isGroupEvent', () => {
+ assert.isTrue(element._isGroupEvent('ADD_GROUP'));
+ assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
- suite('members', () => {
- test('test _getNameForGroup', () => {
- let group = {
- member: {
- name: 'test-name',
- },
- };
- assert.equal(element._getNameForGroup(group.member), 'test-name');
-
- group = {
- member: {
- id: 'test-id',
- },
- };
- assert.equal(element._getNameForGroup(group.member), 'test-id');
- });
-
- test('test _isGroupEvent', () => {
- assert.isTrue(element._isGroupEvent('ADD_GROUP'));
- assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
-
- assert.isFalse(element._isGroupEvent('ADD_USER'));
- assert.isFalse(element._isGroupEvent('REMOVE_USER'));
- });
- });
-
- suite('users', () => {
- test('test _getIdForUser', () => {
- const account = {
- user: {
- username: 'test-user',
- _account_id: 12,
- },
- };
- assert.equal(element._getIdForUser(account.user), ' (12)');
- });
-
- test('test _account_id not present', () => {
- const account = {
- user: {
- username: 'test-user',
- },
- };
- assert.equal(element._getIdForUser(account.user), '');
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- element.groupId = 1;
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getGroupAuditLog', (group, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._getAuditLogs();
- });
+ assert.isFalse(element._isGroupEvent('ADD_USER'));
+ assert.isFalse(element._isGroupEvent('REMOVE_USER'));
});
});
+
+ suite('users', () => {
+ test('test _getIdForUser', () => {
+ const account = {
+ user: {
+ username: 'test-user',
+ _account_id: 12,
+ },
+ };
+ assert.equal(element._getIdForUser(account.user), ' (12)');
+ });
+
+ test('test _account_id not present', () => {
+ const account = {
+ user: {
+ username: 'test-user',
+ },
+ };
+ assert.equal(element._getIdForUser(account.user), '');
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ element.groupId = 1;
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getGroupAuditLog', (group, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._getAuditLogs();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 8c29f73..fc9e4a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -14,280 +14,300 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const SUGGESTIONS_LIMIT = 15;
- const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
- 'permission to add it';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.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-group-members_html.js';
- const URL_REGEX = '^(?:[a-z]+:)?//';
+const SUGGESTIONS_LIMIT = 15;
+const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
+ 'permission to add it';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrGroupMembers extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-group-members'; }
+const URL_REGEX = '^(?:[a-z]+:)?//';
- static get properties() {
- return {
- groupId: Number,
- _groupMemberSearchId: String,
- _groupMemberSearchName: String,
- _includedGroupSearchId: String,
- _includedGroupSearchName: String,
- _loading: {
- type: Boolean,
- value: true,
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrGroupMembers extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-group-members'; }
+
+ static get properties() {
+ return {
+ groupId: Number,
+ _groupMemberSearchId: String,
+ _groupMemberSearchName: String,
+ _includedGroupSearchId: String,
+ _includedGroupSearchName: String,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _groupName: String,
+ _groupMembers: Object,
+ _includedGroups: Object,
+ _itemName: String,
+ _itemType: String,
+ _queryMembers: {
+ type: Function,
+ value() {
+ return this._getAccountSuggestions.bind(this);
},
- _groupName: String,
- _groupMembers: Object,
- _includedGroups: Object,
- _itemName: String,
- _itemType: String,
- _queryMembers: {
- type: Function,
- value() {
- return this._getAccountSuggestions.bind(this);
- },
+ },
+ _queryIncludedGroup: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- _queryIncludedGroup: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- _groupOwner: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- };
- }
+ },
+ _groupOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- this._loadGroupDetails();
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadGroupDetails();
- this.fire('title-change', {title: 'Members'});
- }
+ this.fire('title-change', {title: 'Members'});
+ }
- _loadGroupDetails() {
- if (!this.groupId) { return; }
+ _loadGroupDetails() {
+ if (!this.groupId) { return; }
- const promises = [];
+ const promises = [];
- const errFn = response => {
- this.fire('page-error', {response});
- };
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
- return this.$.restAPI.getGroupConfig(this.groupId, errFn)
- .then(config => {
- if (!config || !config.name) { return Promise.resolve(); }
+ return this.$.restAPI.getGroupConfig(this.groupId, errFn)
+ .then(config => {
+ if (!config || !config.name) { return Promise.resolve(); }
- this._groupName = config.name;
+ this._groupName = config.name;
- promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin ? true : false;
- }));
+ promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin ? true : false;
+ }));
- promises.push(this.$.restAPI.getIsGroupOwner(config.name)
- .then(isOwner => {
- this._groupOwner = isOwner ? true : false;
- }));
-
- promises.push(this.$.restAPI.getGroupMembers(config.name).then(
- members => {
- this._groupMembers = members;
- }));
-
- promises.push(this.$.restAPI.getIncludedGroup(config.name)
- .then(includedGroup => {
- this._includedGroups = includedGroup;
- }));
-
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _computeGroupUrl(url) {
- if (!url) { return; }
-
- const r = new RegExp(URL_REGEX, 'i');
- if (r.test(url)) {
- return url;
- }
-
- // For GWT compatibility
- if (url.startsWith('#')) {
- return this.getBaseUrl() + url.slice(1);
- }
- return this.getBaseUrl() + url;
- }
-
- _handleSavingGroupMember() {
- return this.$.restAPI.saveGroupMembers(this._groupName,
- this._groupMemberSearchId).then(config => {
- if (!config) {
- return;
- }
- this.$.restAPI.getGroupMembers(this._groupName).then(members => {
- this._groupMembers = members;
- });
- this._groupMemberSearchName = '';
- this._groupMemberSearchId = '';
- });
- }
-
- _handleDeleteConfirm() {
- this.$.overlay.close();
- if (this._itemType === 'member') {
- return this.$.restAPI.deleteGroupMembers(this._groupName,
- this._itemId)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this.$.restAPI.getGroupMembers(this._groupName)
- .then(members => {
- this._groupMembers = members;
- });
- }
- });
- } else if (this._itemType === 'includedGroup') {
- return this.$.restAPI.deleteIncludedGroup(this._groupName,
- this._itemId)
- .then(itemDeleted => {
- if (itemDeleted.status === 204 || itemDeleted.status === 205) {
- this.$.restAPI.getIncludedGroup(this._groupName)
- .then(includedGroup => {
- this._includedGroups = includedGroup;
- });
- }
- });
- }
- }
-
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
- }
-
- _handleDeleteMember(e) {
- const id = e.model.get('item._account_id');
- const name = e.model.get('item.name');
- const username = e.model.get('item.username');
- const email = e.model.get('item.email');
- const item = username || name || email || id;
- if (!item) {
- return '';
- }
- this._itemName = item;
- this._itemId = id;
- this._itemType = 'member';
- this.$.overlay.open();
- }
-
- _handleSavingIncludedGroups() {
- return this.$.restAPI.saveIncludedGroup(this._groupName,
- this._includedGroupSearchId.replace(/\+/g, ' '), err => {
- if (err.status === 404) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: SAVING_ERROR_TEXT},
- bubbles: true,
- composed: true,
+ promises.push(this.$.restAPI.getIsGroupOwner(config.name)
+ .then(isOwner => {
+ this._groupOwner = isOwner ? true : false;
}));
- return err;
- }
- throw Error(err.statusText);
- })
- .then(config => {
- if (!config) {
- return;
- }
- this.$.restAPI.getIncludedGroup(this._groupName)
- .then(includedGroup => {
- this._includedGroups = includedGroup;
- });
- this._includedGroupSearchName = '';
- this._includedGroupSearchId = '';
+
+ promises.push(this.$.restAPI.getGroupMembers(config.name).then(
+ members => {
+ this._groupMembers = members;
+ }));
+
+ promises.push(this.$.restAPI.getIncludedGroup(config.name)
+ .then(includedGroup => {
+ this._includedGroups = includedGroup;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
});
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _computeGroupUrl(url) {
+ if (!url) { return; }
+
+ const r = new RegExp(URL_REGEX, 'i');
+ if (r.test(url)) {
+ return url;
}
- _handleDeleteIncludedGroup(e) {
- const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' ');
- const name = e.model.get('item.name');
- const item = name || id;
- if (!item) { return ''; }
- this._itemName = item;
- this._itemId = id;
- this._itemType = 'includedGroup';
- this.$.overlay.open();
+ // For GWT compatibility
+ if (url.startsWith('#')) {
+ return this.getBaseUrl() + url.slice(1);
}
+ return this.getBaseUrl() + url;
+ }
- _getAccountSuggestions(input) {
- if (input.length === 0) { return Promise.resolve([]); }
- return this.$.restAPI.getSuggestedAccounts(
- input, SUGGESTIONS_LIMIT).then(accounts => {
- const accountSuggestions = [];
- let nameAndEmail;
- if (!accounts) { return []; }
- for (const key in accounts) {
- if (!accounts.hasOwnProperty(key)) { continue; }
- if (accounts[key].email !== undefined) {
- nameAndEmail = accounts[key].name +
- ' <' + accounts[key].email + '>';
- } else {
- nameAndEmail = accounts[key].name;
- }
- accountSuggestions.push({
- name: nameAndEmail,
- value: accounts[key]._account_id,
- });
- }
- return accountSuggestions;
+ _handleSavingGroupMember() {
+ return this.$.restAPI.saveGroupMembers(this._groupName,
+ this._groupMemberSearchId).then(config => {
+ if (!config) {
+ return;
+ }
+ this.$.restAPI.getGroupMembers(this._groupName).then(members => {
+ this._groupMembers = members;
});
- }
+ this._groupMemberSearchName = '';
+ this._groupMemberSearchId = '';
+ });
+ }
- _getGroupSuggestions(input) {
- return this.$.restAPI.getSuggestedGroups(input)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: decodeURIComponent(response[key].id),
- });
+ _handleDeleteConfirm() {
+ this.$.overlay.close();
+ if (this._itemType === 'member') {
+ return this.$.restAPI.deleteGroupMembers(this._groupName,
+ this._itemId)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this.$.restAPI.getGroupMembers(this._groupName)
+ .then(members => {
+ this._groupMembers = members;
+ });
}
- return groups;
});
- }
-
- _computeHideItemClass(owner, admin) {
- return admin || owner ? '' : 'canModify';
+ } else if (this._itemType === 'includedGroup') {
+ return this.$.restAPI.deleteIncludedGroup(this._groupName,
+ this._itemId)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204 || itemDeleted.status === 205) {
+ this.$.restAPI.getIncludedGroup(this._groupName)
+ .then(includedGroup => {
+ this._includedGroups = includedGroup;
+ });
+ }
+ });
}
}
- customElements.define(GrGroupMembers.is, GrGroupMembers);
-})();
+ _handleConfirmDialogCancel() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteMember(e) {
+ const id = e.model.get('item._account_id');
+ const name = e.model.get('item.name');
+ const username = e.model.get('item.username');
+ const email = e.model.get('item.email');
+ const item = username || name || email || id;
+ if (!item) {
+ return '';
+ }
+ this._itemName = item;
+ this._itemId = id;
+ this._itemType = 'member';
+ this.$.overlay.open();
+ }
+
+ _handleSavingIncludedGroups() {
+ return this.$.restAPI.saveIncludedGroup(this._groupName,
+ this._includedGroupSearchId.replace(/\+/g, ' '), err => {
+ if (err.status === 404) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: SAVING_ERROR_TEXT},
+ bubbles: true,
+ composed: true,
+ }));
+ return err;
+ }
+ throw Error(err.statusText);
+ })
+ .then(config => {
+ if (!config) {
+ return;
+ }
+ this.$.restAPI.getIncludedGroup(this._groupName)
+ .then(includedGroup => {
+ this._includedGroups = includedGroup;
+ });
+ this._includedGroupSearchName = '';
+ this._includedGroupSearchId = '';
+ });
+ }
+
+ _handleDeleteIncludedGroup(e) {
+ const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' ');
+ const name = e.model.get('item.name');
+ const item = name || id;
+ if (!item) { return ''; }
+ this._itemName = item;
+ this._itemId = id;
+ this._itemType = 'includedGroup';
+ this.$.overlay.open();
+ }
+
+ _getAccountSuggestions(input) {
+ if (input.length === 0) { return Promise.resolve([]); }
+ return this.$.restAPI.getSuggestedAccounts(
+ input, SUGGESTIONS_LIMIT).then(accounts => {
+ const accountSuggestions = [];
+ let nameAndEmail;
+ if (!accounts) { return []; }
+ for (const key in accounts) {
+ if (!accounts.hasOwnProperty(key)) { continue; }
+ if (accounts[key].email !== undefined) {
+ nameAndEmail = accounts[key].name +
+ ' <' + accounts[key].email + '>';
+ } else {
+ nameAndEmail = accounts[key].name;
+ }
+ accountSuggestions.push({
+ name: nameAndEmail,
+ value: accounts[key]._account_id,
+ });
+ }
+ return accountSuggestions;
+ });
+ }
+
+ _getGroupSuggestions(input) {
+ return this.$.restAPI.getSuggestedGroups(input)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ return groups;
+ });
+ }
+
+ _computeHideItemClass(owner, admin) {
+ return admin || owner ? '' : 'canModify';
+ }
+}
+
+customElements.define(GrGroupMembers.is, GrGroupMembers);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
index cf24793..79a88fd 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
@@ -1,38 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
-
-<dom-module id="gr-group-members">
- <template>
+export const htmlTemplate = html`
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -72,37 +56,29 @@
display: none;
}
</style>
- <main class$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
+ <main class\$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<h1 id="Title">[[_groupName]]</h1>
<div id="form">
<h3 id="members">Members</h3>
<fieldset>
<span class="value">
- <gr-autocomplete
- id="groupMemberSearchInput"
- text="{{_groupMemberSearchName}}"
- value="{{_groupMemberSearchId}}"
- query="[[_queryMembers]]"
- placeholder="Name Or Email">
+ <gr-autocomplete id="groupMemberSearchInput" text="{{_groupMemberSearchName}}" value="{{_groupMemberSearchId}}" query="[[_queryMembers]]" placeholder="Name Or Email">
</gr-autocomplete>
</span>
- <gr-button
- id="saveGroupMember"
- on-click="_handleSavingGroupMember"
- disabled="[[!_groupMemberSearchId]]">
+ <gr-button id="saveGroupMember" on-click="_handleSavingGroupMember" disabled="[[!_groupMemberSearchId]]">
Add
</gr-button>
<table id="groupMembers">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="nameHeader">Name</th>
<th class="emailAddressHeader">Email Address</th>
<th class="deleteHeader">Delete Member</th>
</tr>
- <tbody>
+ </tbody><tbody>
<template is="dom-repeat" items="[[_groupMembers]]">
<tr>
<td class="nameColumn">
@@ -110,9 +86,7 @@
</td>
<td>[[item.email]]</td>
<td class="deleteColumn">
- <gr-button
- class="deleteMembersButton"
- on-click="_handleDeleteMember">
+ <gr-button class="deleteMembersButton" on-click="_handleDeleteMember">
Delete
</gr-button>
</td>
@@ -124,35 +98,26 @@
<h3 id="includedGroups">Included Groups</h3>
<fieldset>
<span class="value">
- <gr-autocomplete
- id="includedGroupSearchInput"
- text="{{_includedGroupSearchName}}"
- value="{{_includedGroupSearchId}}"
- query="[[_queryIncludedGroup]]"
- placeholder="Group Name">
+ <gr-autocomplete id="includedGroupSearchInput" text="{{_includedGroupSearchName}}" value="{{_includedGroupSearchId}}" query="[[_queryIncludedGroup]]" placeholder="Group Name">
</gr-autocomplete>
</span>
- <gr-button
- id="saveIncludedGroups"
- on-click="_handleSavingIncludedGroups"
- disabled="[[!_includedGroupSearchId]]">
+ <gr-button id="saveIncludedGroups" on-click="_handleSavingIncludedGroups" disabled="[[!_includedGroupSearchId]]">
Add
</gr-button>
<table id="includedGroups">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="groupNameHeader">Group Name</th>
<th class="descriptionHeader">Description</th>
<th class="deleteIncludedHeader">
Delete Group
</th>
</tr>
- <tbody>
+ </tbody><tbody>
<template is="dom-repeat" items="[[_includedGroups]]">
<tr>
<td class="nameColumn">
<template is="dom-if" if="[[item.url]]">
- <a href$="[[_computeGroupUrl(item.url)]]"
- rel="noopener">
+ <a href\$="[[_computeGroupUrl(item.url)]]" rel="noopener">
[[item.name]]
</a>
</template>
@@ -162,9 +127,7 @@
</td>
<td>[[item.description]]</td>
<td class="deleteColumn">
- <gr-button
- class="deleteIncludedGroupButton"
- on-click="_handleDeleteIncludedGroup">
+ <gr-button class="deleteIncludedGroupButton" on-click="_handleDeleteIncludedGroup">
Delete
</gr-button>
</td>
@@ -176,15 +139,8 @@
</div>
</div>
</main>
- <gr-overlay id="overlay" with-backdrop>
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_itemName]]"
- item-type="[[_itemType]]"></gr-confirm-delete-item-dialog>
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_itemName]]" item-type="[[_itemType]]"></gr-confirm-delete-item-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group-members.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index ec9a80c..9380b86 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_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-group-members</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-group-members.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-group-members.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-group-members.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,342 +40,345 @@
</template>
</test-fixture>
-<script>
- suite('gr-group-members tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let groups;
- let groupMembers;
- let includedGroups;
- let groupStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-group-members.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-group-members tests', () => {
+ let element;
+ let sandbox;
+ let groups;
+ let groupMembers;
+ let includedGroups;
+ let groupStub;
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- groups = {
- name: 'Administrators',
- owner: 'Administrators',
- group_id: 1,
- };
+ groups = {
+ name: 'Administrators',
+ owner: 'Administrators',
+ group_id: 1,
+ };
- groupMembers = [
- {
- _account_id: 1000097,
- name: 'Jane Roe',
- email: 'jane.roe@example.com',
- username: 'jane',
- },
- {
- _account_id: 1000096,
- name: 'Test User',
- email: 'john.doe@example.com',
- },
- {
- _account_id: 1000095,
- name: 'Gerrit',
- },
- {
- _account_id: 1000098,
- },
- ];
-
- includedGroups = [{
- url: 'https://group/url',
- options: {},
- id: 'testId',
- name: 'testName',
+ groupMembers = [
+ {
+ _account_id: 1000097,
+ name: 'Jane Roe',
+ email: 'jane.roe@example.com',
+ username: 'jane',
},
{
- url: '/group/url',
- options: {},
- id: 'testId2',
- name: 'testName2',
+ _account_id: 1000096,
+ name: 'Test User',
+ email: 'john.doe@example.com',
},
{
- url: '#/group/url',
- options: {},
- id: 'testId3',
- name: 'testName3',
+ _account_id: 1000095,
+ name: 'Gerrit',
},
- ];
+ {
+ _account_id: 1000098,
+ },
+ ];
- stub('gr-rest-api-interface', {
- getSuggestedAccounts(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- _account_id: 1000096,
- name: 'test-account',
- email: 'test.account@example.com',
- username: 'test123',
- },
- {
- _account_id: 1001439,
- name: 'test-admin',
- email: 'test.admin@example.com',
- username: 'test_admin',
- },
- {
- _account_id: 1001439,
- name: 'test-git',
- username: 'test_git',
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- getSuggestedGroups(input) {
- if (input.startsWith('test')) {
- return Promise.resolve({
- 'test-admin': {
- id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
- },
- 'test/Administrator (admin)': {
- id: 'test%3Aadmin',
- },
- });
- } else {
- return Promise.resolve({});
- }
- },
- getLoggedIn() { return Promise.resolve(true); },
- getConfig() {
- return Promise.resolve();
- },
- getGroupMembers() {
- return Promise.resolve(groupMembers);
- },
- getIsGroupOwner() {
- return Promise.resolve(true);
- },
- getIncludedGroup() {
- return Promise.resolve(includedGroups);
- },
- getAccountCapabilities() {
- return Promise.resolve();
- },
- });
- element = fixture('basic');
- sandbox.stub(element, 'getBaseUrl').returns('https://test/site');
- element.groupId = 1;
- groupStub = sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve(groups));
- return element._loadGroupDetails();
+ includedGroups = [{
+ url: 'https://group/url',
+ options: {},
+ id: 'testId',
+ name: 'testName',
+ },
+ {
+ url: '/group/url',
+ options: {},
+ id: 'testId2',
+ name: 'testName2',
+ },
+ {
+ url: '#/group/url',
+ options: {},
+ id: 'testId3',
+ name: 'testName3',
+ },
+ ];
+
+ stub('gr-rest-api-interface', {
+ getSuggestedAccounts(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ _account_id: 1000096,
+ name: 'test-account',
+ email: 'test.account@example.com',
+ username: 'test123',
+ },
+ {
+ _account_id: 1001439,
+ name: 'test-admin',
+ email: 'test.admin@example.com',
+ username: 'test_admin',
+ },
+ {
+ _account_id: 1001439,
+ name: 'test-git',
+ username: 'test_git',
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
+ getSuggestedGroups(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve({
+ 'test-admin': {
+ id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
+ },
+ 'test/Administrator (admin)': {
+ id: 'test%3Aadmin',
+ },
+ });
+ } else {
+ return Promise.resolve({});
+ }
+ },
+ getLoggedIn() { return Promise.resolve(true); },
+ getConfig() {
+ return Promise.resolve();
+ },
+ getGroupMembers() {
+ return Promise.resolve(groupMembers);
+ },
+ getIsGroupOwner() {
+ return Promise.resolve(true);
+ },
+ getIncludedGroup() {
+ return Promise.resolve(includedGroups);
+ },
+ getAccountCapabilities() {
+ return Promise.resolve();
+ },
});
+ element = fixture('basic');
+ sandbox.stub(element, 'getBaseUrl').returns('https://test/site');
+ element.groupId = 1;
+ groupStub = sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve(groups));
+ return element._loadGroupDetails();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('_includedGroups', () => {
- assert.equal(element._includedGroups.length, 3);
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url);
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.nameColumn a')[1].href,
- 'https://test/site/group/url');
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('.nameColumn a')[2].href,
- 'https://test/site/group/url');
- });
+ test('_includedGroups', () => {
+ assert.equal(element._includedGroups.length, 3);
+ assert.equal(dom(element.root)
+ .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url);
+ assert.equal(dom(element.root)
+ .querySelectorAll('.nameColumn a')[1].href,
+ 'https://test/site/group/url');
+ assert.equal(dom(element.root)
+ .querySelectorAll('.nameColumn a')[2].href,
+ 'https://test/site/group/url');
+ });
- test('save members correctly', () => {
- element._groupOwner = true;
+ test('save members correctly', () => {
+ element._groupOwner = true;
- const memberName = 'test-admin';
+ const memberName = 'test-admin';
- const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers',
- () => Promise.resolve({}));
+ const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+ () => Promise.resolve({}));
- const button = element.$.saveGroupMember;
+ const button = element.$.saveGroupMember;
+ assert.isTrue(button.hasAttribute('disabled'));
+
+ element.$.groupMemberSearchInput.text = memberName;
+ element.$.groupMemberSearchInput.value = 1234;
+
+ assert.isFalse(button.hasAttribute('disabled'));
+
+ return element._handleSavingGroupMember().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
-
- element.$.groupMemberSearchInput.text = memberName;
- element.$.groupMemberSearchInput.value = 1234;
-
- assert.isFalse(button.hasAttribute('disabled'));
-
- return element._handleSavingGroupMember().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators',
- 1234));
- });
- });
-
- test('save included groups correctly', () => {
- element._groupOwner = true;
-
- const includedGroupName = 'testName';
-
- const saveIncludedGroupStub = sandbox.stub(
- element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({}));
-
- const button = element.$.saveIncludedGroups;
-
- assert.isTrue(button.hasAttribute('disabled'));
-
- element.$.includedGroupSearchInput.text = includedGroupName;
- element.$.includedGroupSearchInput.value = 'testId';
-
- assert.isFalse(button.hasAttribute('disabled'));
-
- return element._handleSavingIncludedGroups().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
- assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
- });
- });
-
- test('add included group 404 shows helpful error text', () => {
- element._groupOwner = true;
-
- const memberName = 'bad-name';
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- const error = new Error('error');
- error.status = 404;
- sandbox.stub(element.$.restAPI, 'saveGroupMembers',
- () => Promise.reject(error));
-
- element.$.groupMemberSearchInput.text = memberName;
- element.$.groupMemberSearchInput.value = 1234;
-
- return element._handleSavingIncludedGroups().then(() => {
- assert.isTrue(alertStub.called);
- });
- });
-
- test('_getAccountSuggestions empty', done => {
- element
- ._getAccountSuggestions('nonexistent').then(accounts => {
- assert.equal(accounts.length, 0);
- done();
- });
- });
-
- test('_getAccountSuggestions non-empty', done => {
- element
- ._getAccountSuggestions('test-').then(accounts => {
- assert.equal(accounts.length, 3);
- assert.equal(accounts[0].name,
- 'test-account <test.account@example.com>');
- assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
- assert.equal(accounts[2].name, 'test-git');
- done();
- });
- });
-
- test('_getGroupSuggestions empty', done => {
- element
- ._getGroupSuggestions('nonexistent').then(groups => {
- assert.equal(groups.length, 0);
- done();
- });
- });
-
- test('_getGroupSuggestions non-empty', done => {
- element
- ._getGroupSuggestions('test').then(groups => {
- assert.equal(groups.length, 2);
- assert.equal(groups[0].name, 'test-admin');
- assert.equal(groups[1].name, 'test/Administrator (admin)');
- done();
- });
- });
-
- test('_computeHideItemClass returns string for admin', () => {
- const admin = true;
- const owner = false;
- assert.equal(element._computeHideItemClass(owner, admin), '');
- });
-
- test('_computeHideItemClass returns hideItem for admin and owner', () => {
- const admin = false;
- const owner = false;
- assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
- });
-
- test('_computeHideItemClass returns string for owner', () => {
- const admin = false;
- const owner = true;
- assert.equal(element._computeHideItemClass(owner, admin), '');
- });
-
- test('delete member', () => {
- const deletelBtns = Polymer.dom(element.root)
- .querySelectorAll('.deleteMembersButton');
- MockInteractions.tap(deletelBtns[0]);
- assert.equal(element._itemId, '1000097');
- assert.equal(element._itemName, 'jane');
- MockInteractions.tap(deletelBtns[1]);
- assert.equal(element._itemId, '1000096');
- assert.equal(element._itemName, 'Test User');
- MockInteractions.tap(deletelBtns[2]);
- assert.equal(element._itemId, '1000095');
- assert.equal(element._itemName, 'Gerrit');
- MockInteractions.tap(deletelBtns[3]);
- assert.equal(element._itemId, '1000098');
- assert.equal(element._itemName, '1000098');
- });
-
- test('delete included groups', () => {
- const deletelBtns = Polymer.dom(element.root)
- .querySelectorAll('.deleteIncludedGroupButton');
- MockInteractions.tap(deletelBtns[0]);
- assert.equal(element._itemId, 'testId');
- assert.equal(element._itemName, 'testName');
- MockInteractions.tap(deletelBtns[1]);
- assert.equal(element._itemId, 'testId2');
- assert.equal(element._itemName, 'testName2');
- MockInteractions.tap(deletelBtns[2]);
- assert.equal(element._itemId, 'testId3');
- assert.equal(element._itemName, 'testName3');
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
-
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('_computeGroupUrl', () => {
- assert.isUndefined(element._computeGroupUrl(undefined));
-
- assert.isUndefined(element._computeGroupUrl(false));
-
- let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
- assert.equal(element._computeGroupUrl(url),
- 'https://test/site/admin/groups/' +
- 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498');
-
- url = 'https://gerrit.local/admin/groups/' +
- 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
- assert.equal(element._computeGroupUrl(url), url);
- });
-
- test('fires page-error', done => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getGroupConfig', (group, errFn) => {
- errFn(response);
- });
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadGroupDetails();
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators',
+ 1234));
});
});
+
+ test('save included groups correctly', () => {
+ element._groupOwner = true;
+
+ const includedGroupName = 'testName';
+
+ const saveIncludedGroupStub = sandbox.stub(
+ element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({}));
+
+ const button = element.$.saveIncludedGroups;
+
+ assert.isTrue(button.hasAttribute('disabled'));
+
+ element.$.includedGroupSearchInput.text = includedGroupName;
+ element.$.includedGroupSearchInput.value = 'testId';
+
+ assert.isFalse(button.hasAttribute('disabled'));
+
+ return element._handleSavingIncludedGroups().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
+ assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
+ });
+ });
+
+ test('add included group 404 shows helpful error text', () => {
+ element._groupOwner = true;
+
+ const memberName = 'bad-name';
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+ const error = new Error('error');
+ error.status = 404;
+ sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+ () => Promise.reject(error));
+
+ element.$.groupMemberSearchInput.text = memberName;
+ element.$.groupMemberSearchInput.value = 1234;
+
+ return element._handleSavingIncludedGroups().then(() => {
+ assert.isTrue(alertStub.called);
+ });
+ });
+
+ test('_getAccountSuggestions empty', done => {
+ element
+ ._getAccountSuggestions('nonexistent').then(accounts => {
+ assert.equal(accounts.length, 0);
+ done();
+ });
+ });
+
+ test('_getAccountSuggestions non-empty', done => {
+ element
+ ._getAccountSuggestions('test-').then(accounts => {
+ assert.equal(accounts.length, 3);
+ assert.equal(accounts[0].name,
+ 'test-account <test.account@example.com>');
+ assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
+ assert.equal(accounts[2].name, 'test-git');
+ done();
+ });
+ });
+
+ test('_getGroupSuggestions empty', done => {
+ element
+ ._getGroupSuggestions('nonexistent').then(groups => {
+ assert.equal(groups.length, 0);
+ done();
+ });
+ });
+
+ test('_getGroupSuggestions non-empty', done => {
+ element
+ ._getGroupSuggestions('test').then(groups => {
+ assert.equal(groups.length, 2);
+ assert.equal(groups[0].name, 'test-admin');
+ assert.equal(groups[1].name, 'test/Administrator (admin)');
+ done();
+ });
+ });
+
+ test('_computeHideItemClass returns string for admin', () => {
+ const admin = true;
+ const owner = false;
+ assert.equal(element._computeHideItemClass(owner, admin), '');
+ });
+
+ test('_computeHideItemClass returns hideItem for admin and owner', () => {
+ const admin = false;
+ const owner = false;
+ assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
+ });
+
+ test('_computeHideItemClass returns string for owner', () => {
+ const admin = false;
+ const owner = true;
+ assert.equal(element._computeHideItemClass(owner, admin), '');
+ });
+
+ test('delete member', () => {
+ const deletelBtns = dom(element.root)
+ .querySelectorAll('.deleteMembersButton');
+ MockInteractions.tap(deletelBtns[0]);
+ assert.equal(element._itemId, '1000097');
+ assert.equal(element._itemName, 'jane');
+ MockInteractions.tap(deletelBtns[1]);
+ assert.equal(element._itemId, '1000096');
+ assert.equal(element._itemName, 'Test User');
+ MockInteractions.tap(deletelBtns[2]);
+ assert.equal(element._itemId, '1000095');
+ assert.equal(element._itemName, 'Gerrit');
+ MockInteractions.tap(deletelBtns[3]);
+ assert.equal(element._itemId, '1000098');
+ assert.equal(element._itemName, '1000098');
+ });
+
+ test('delete included groups', () => {
+ const deletelBtns = dom(element.root)
+ .querySelectorAll('.deleteIncludedGroupButton');
+ MockInteractions.tap(deletelBtns[0]);
+ assert.equal(element._itemId, 'testId');
+ assert.equal(element._itemName, 'testName');
+ MockInteractions.tap(deletelBtns[1]);
+ assert.equal(element._itemId, 'testId2');
+ assert.equal(element._itemName, 'testName2');
+ MockInteractions.tap(deletelBtns[2]);
+ assert.equal(element._itemId, 'testId3');
+ assert.equal(element._itemName, 'testName3');
+ });
+
+ test('_computeLoadingClass', () => {
+ assert.equal(element._computeLoadingClass(true), 'loading');
+
+ assert.equal(element._computeLoadingClass(false), '');
+ });
+
+ test('_computeGroupUrl', () => {
+ assert.isUndefined(element._computeGroupUrl(undefined));
+
+ assert.isUndefined(element._computeGroupUrl(false));
+
+ let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
+ assert.equal(element._computeGroupUrl(url),
+ 'https://test/site/admin/groups/' +
+ 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498');
+
+ url = 'https://gerrit.local/admin/groups/' +
+ 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
+ assert.equal(element._computeGroupUrl(url), url);
+ });
+
+ test('fires page-error', done => {
+ groupStub.restore();
+
+ element.groupId = 1;
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getGroupConfig', (group, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._loadGroupDetails();
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 42846f4..5127733 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -14,238 +14,253 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
- const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.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-group_html.js';
- const OPTIONS = {
- submitFalse: {
- value: false,
- label: 'False',
- },
- submitTrue: {
- value: true,
- label: 'True',
- },
- };
+const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+const OPTIONS = {
+ submitFalse: {
+ value: false,
+ label: 'False',
+ },
+ submitTrue: {
+ value: true,
+ label: 'True',
+ },
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrGroup extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-group'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the group name changes.
+ *
+ * @event name-changed
*/
- class GrGroup extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-group'; }
- /**
- * Fired when the group name changes.
- *
- * @event name-changed
- */
- static get properties() {
- return {
- groupId: Number,
- _rename: {
- type: Boolean,
- value: false,
+ static get properties() {
+ return {
+ groupId: Number,
+ _rename: {
+ type: Boolean,
+ value: false,
+ },
+ _groupIsInternal: Boolean,
+ _description: {
+ type: Boolean,
+ value: false,
+ },
+ _owner: {
+ type: Boolean,
+ value: false,
+ },
+ _options: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {?} */
+ _groupConfig: Object,
+ _groupConfigOwner: String,
+ _groupName: Object,
+ _groupOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _submitTypes: {
+ type: Array,
+ value() {
+ return Object.values(OPTIONS);
},
- _groupIsInternal: Boolean,
- _description: {
- type: Boolean,
- value: false,
+ },
+ _query: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- _owner: {
- type: Boolean,
- value: false,
- },
- _options: {
- type: Boolean,
- value: false,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- /** @type {?} */
- _groupConfig: Object,
- _groupConfigOwner: String,
- _groupName: Object,
- _groupOwner: {
- type: Boolean,
- value: false,
- },
- _submitTypes: {
- type: Array,
- value() {
- return Object.values(OPTIONS);
- },
- },
- _query: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- static get observers() {
- return [
- '_handleConfigName(_groupConfig.name)',
- '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)',
- '_handleConfigDescription(_groupConfig.description)',
- '_handleConfigOptions(_groupConfig.options.visible_to_all)',
- ];
- }
-
- /** @override */
- attached() {
- super.attached();
- this._loadGroup();
- }
-
- _loadGroup() {
- if (!this.groupId) { return; }
-
- const promises = [];
-
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- return this.$.restAPI.getGroupConfig(this.groupId, errFn)
- .then(config => {
- if (!config || !config.name) { return Promise.resolve(); }
-
- this._groupName = config.name;
- this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
-
- promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin ? true : false;
- }));
-
- promises.push(this.$.restAPI.getIsGroupOwner(config.name)
- .then(isOwner => {
- this._groupOwner = isOwner ? true : false;
- }));
-
- // If visible to all is undefined, set to false. If it is defined
- // as false, setting to false is fine. If any optional values
- // are added with a default of true, then this would need to be an
- // undefined check and not a truthy/falsy check.
- if (!config.options.visible_to_all) {
- config.options.visible_to_all = false;
- }
- this._groupConfig = config;
-
- this.fire('title-change', {title: config.name});
-
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _handleSaveName() {
- return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
- .then(config => {
- if (config.status === 200) {
- this._groupName = this._groupConfig.name;
- this.fire('name-changed', {name: this._groupConfig.name,
- external: this._groupIsExtenral});
- this._rename = false;
- }
- });
- }
-
- _handleSaveOwner() {
- let owner = this._groupConfig.owner;
- if (this._groupConfigOwner) {
- owner = decodeURIComponent(this._groupConfigOwner);
- }
- return this.$.restAPI.saveGroupOwner(this.groupId,
- owner).then(config => {
- this._owner = false;
- });
- }
-
- _handleSaveDescription() {
- return this.$.restAPI.saveGroupDescription(this.groupId,
- this._groupConfig.description).then(config => {
- this._description = false;
- });
- }
-
- _handleSaveOptions() {
- const visible = this._groupConfig.options.visible_to_all;
-
- const options = {visible_to_all: visible};
-
- return this.$.restAPI.saveGroupOptions(this.groupId,
- options).then(config => {
- this._options = false;
- });
- }
-
- _handleConfigName() {
- if (this._isLoading()) { return; }
- this._rename = true;
- }
-
- _handleConfigOwner() {
- if (this._isLoading()) { return; }
- this._owner = true;
- }
-
- _handleConfigDescription() {
- if (this._isLoading()) { return; }
- this._description = true;
- }
-
- _handleConfigOptions() {
- if (this._isLoading()) { return; }
- this._options = true;
- }
-
- _computeHeaderClass(configChanged) {
- return configChanged ? 'edited' : '';
- }
-
- _getGroupSuggestions(input) {
- return this.$.restAPI.getSuggestedGroups(input)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: decodeURIComponent(response[key].id),
- });
- }
- return groups;
- });
- }
-
- _computeGroupDisabled(owner, admin, groupIsInternal) {
- return groupIsInternal && (admin || owner) ? false : true;
- }
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrGroup.is, GrGroup);
-})();
+ static get observers() {
+ return [
+ '_handleConfigName(_groupConfig.name)',
+ '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)',
+ '_handleConfigDescription(_groupConfig.description)',
+ '_handleConfigOptions(_groupConfig.options.visible_to_all)',
+ ];
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadGroup();
+ }
+
+ _loadGroup() {
+ if (!this.groupId) { return; }
+
+ const promises = [];
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ return this.$.restAPI.getGroupConfig(this.groupId, errFn)
+ .then(config => {
+ if (!config || !config.name) { return Promise.resolve(); }
+
+ this._groupName = config.name;
+ this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
+
+ promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin ? true : false;
+ }));
+
+ promises.push(this.$.restAPI.getIsGroupOwner(config.name)
+ .then(isOwner => {
+ this._groupOwner = isOwner ? true : false;
+ }));
+
+ // If visible to all is undefined, set to false. If it is defined
+ // as false, setting to false is fine. If any optional values
+ // are added with a default of true, then this would need to be an
+ // undefined check and not a truthy/falsy check.
+ if (!config.options.visible_to_all) {
+ config.options.visible_to_all = false;
+ }
+ this._groupConfig = config;
+
+ this.fire('title-change', {title: config.name});
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
+ });
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _handleSaveName() {
+ return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
+ .then(config => {
+ if (config.status === 200) {
+ this._groupName = this._groupConfig.name;
+ this.fire('name-changed', {name: this._groupConfig.name,
+ external: this._groupIsExtenral});
+ this._rename = false;
+ }
+ });
+ }
+
+ _handleSaveOwner() {
+ let owner = this._groupConfig.owner;
+ if (this._groupConfigOwner) {
+ owner = decodeURIComponent(this._groupConfigOwner);
+ }
+ return this.$.restAPI.saveGroupOwner(this.groupId,
+ owner).then(config => {
+ this._owner = false;
+ });
+ }
+
+ _handleSaveDescription() {
+ return this.$.restAPI.saveGroupDescription(this.groupId,
+ this._groupConfig.description).then(config => {
+ this._description = false;
+ });
+ }
+
+ _handleSaveOptions() {
+ const visible = this._groupConfig.options.visible_to_all;
+
+ const options = {visible_to_all: visible};
+
+ return this.$.restAPI.saveGroupOptions(this.groupId,
+ options).then(config => {
+ this._options = false;
+ });
+ }
+
+ _handleConfigName() {
+ if (this._isLoading()) { return; }
+ this._rename = true;
+ }
+
+ _handleConfigOwner() {
+ if (this._isLoading()) { return; }
+ this._owner = true;
+ }
+
+ _handleConfigDescription() {
+ if (this._isLoading()) { return; }
+ this._description = true;
+ }
+
+ _handleConfigOptions() {
+ if (this._isLoading()) { return; }
+ this._options = true;
+ }
+
+ _computeHeaderClass(configChanged) {
+ return configChanged ? 'edited' : '';
+ }
+
+ _getGroupSuggestions(input) {
+ return this.$.restAPI.getSuggestedGroups(input)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ return groups;
+ });
+ }
+
+ _computeGroupDisabled(owner, admin, groupIsInternal) {
+ return groupIsInternal && (admin || owner) ? false : true;
+ }
+}
+
+customElements.define(GrGroup.is, GrGroup);
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
index faabe84..dc80235 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
@@ -1,33 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/fire-behavior/fire-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-group">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -44,77 +33,57 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<main class="gr-form-styles read-only">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<h1 id="Title">[[_groupName]]</h1>
<h2 id="configurations">General</h2>
<div id="form">
<fieldset>
<h3 id="groupUUID">Group UUID</h3>
<fieldset>
- <gr-copy-clipboard
- text="[[groupId]]"></gr-copy-clipboard>
+ <gr-copy-clipboard text="[[groupId]]"></gr-copy-clipboard>
</fieldset>
- <h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
+ <h3 id="groupName" class\$="[[_computeHeaderClass(_rename)]]">
Group Name
</h3>
<fieldset>
<span class="value">
- <gr-autocomplete
- id="groupNameInput"
- text="{{_groupConfig.name}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete>
+ <gr-autocomplete id="groupNameInput" text="{{_groupConfig.name}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete>
</span>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- id="inputUpdateNameBtn"
- on-click="_handleSaveName"
- disabled="[[!_rename]]">
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button id="inputUpdateNameBtn" on-click="_handleSaveName" disabled="[[!_rename]]">
Rename Group</gr-button>
</span>
</fieldset>
- <h3 class$="[[_computeHeaderClass(_owner)]]">
+ <h3 class\$="[[_computeHeaderClass(_owner)]]">
Owners
</h3>
<fieldset>
<span class="value">
- <gr-autocomplete
- id="groupOwnerInput"
- text="{{_groupConfig.owner}}"
- value="{{_groupConfigOwner}}"
- query="[[_query]]"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-autocomplete id="groupOwnerInput" text="{{_groupConfig.owner}}" value="{{_groupConfigOwner}}" query="[[_query]]" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
</gr-autocomplete>
</span>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- on-click="_handleSaveOwner"
- disabled="[[!_owner]]">
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button on-click="_handleSaveOwner" disabled="[[!_owner]]">
Change Owners</gr-button>
</span>
</fieldset>
- <h3 class$="[[_computeHeaderClass(_description)]]">
+ <h3 class\$="[[_computeHeaderClass(_description)]]">
Description
</h3>
<fieldset>
<div>
- <iron-autogrow-textarea
- class="description"
- autocomplete="on"
- bind-value="{{_groupConfig.description}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea>
+ <iron-autogrow-textarea class="description" autocomplete="on" bind-value="{{_groupConfig.description}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea>
</div>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- on-click="_handleSaveDescription"
- disabled="[[!_description]]">
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button on-click="_handleSaveDescription" disabled="[[!_description]]">
Save Description
</gr-button>
</span>
</fieldset>
- <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
+ <h3 id="options" class\$="[[_computeHeaderClass(_options)]]">
Group Options
</h3>
<fieldset id="visableToAll">
@@ -123,10 +92,8 @@
Make group visible to all registered users
</span>
<span class="value">
- <gr-select
- id="visibleToAll"
- bind-value="{{_groupConfig.options.visible_to_all}}">
- <select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-select id="visibleToAll" bind-value="{{_groupConfig.options.visible_to_all}}">
+ <select disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<template is="dom-repeat" items="[[_submitTypes]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
@@ -134,10 +101,8 @@
</gr-select>
</span>
</section>
- <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
- <gr-button
- on-click="_handleSaveOptions"
- disabled="[[!_options]]">
+ <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
+ <gr-button on-click="_handleSaveOptions" disabled="[[!_options]]">
Save Group Options
</gr-button>
</span>
@@ -147,6 +112,4 @@
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
index a6aebbf..9f278c4 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_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-group</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-group.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-group.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-group.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,222 +40,224 @@
</template>
</test-fixture>
-<script>
- suite('gr-group tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let groupStub;
- const group = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
- options: {},
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- name: 'Administrators',
- };
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-group.js';
+suite('gr-group tests', () => {
+ let element;
+ let sandbox;
+ let groupStub;
+ const group = {
+ id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ options: {},
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ name: 'Administrators',
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- });
- element = fixture('basic');
- groupStub = sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve(group)
- );
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
});
+ element = fixture('basic');
+ groupStub = sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve(group)
+ );
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('loading displays before group config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
+ test('loading displays before group config is loaded', () => {
+ assert.isTrue(element.$.loading.classList.contains('loading'));
+ assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
+ assert.isTrue(element.$.loadedContent.classList.contains('loading'));
+ assert.isTrue(getComputedStyle(element.$.loadedContent)
+ .display === 'none');
+ });
- test('default values are populated with internal group', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getIsGroupOwner',
- () => Promise.resolve(true));
- element.groupId = 1;
- element._loadGroup().then(() => {
- assert.isTrue(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- done();
- });
- });
-
- test('default values with external group', done => {
- const groupExternal = Object.assign({}, group);
- groupExternal.id = 'external-group-id';
- groupStub.restore();
- groupStub = sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve(groupExternal));
- sandbox.stub(
- element.$.restAPI,
- 'getIsGroupOwner',
- () => Promise.resolve(true));
- element.groupId = 1;
- element._loadGroup().then(() => {
- assert.isFalse(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- done();
- });
- });
-
- test('rename group', done => {
- const groupName = 'test-group';
- const groupName2 = 'test-group2';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupConfigOwner = 'testId';
- element._groupName = groupName;
- element._groupOwner = true;
-
- sandbox.stub(
- element.$.restAPI,
- 'getIsGroupOwner',
- () => Promise.resolve(true));
-
- sandbox.stub(
- element.$.restAPI,
- 'saveGroupName',
- () => Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateNameBtn;
-
- element._loadGroup().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupNameInput.text = groupName2;
-
- element.$.groupOwnerInput.text = 'testId2';
-
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupName.classList.contains('edited'));
-
- element._handleSaveName().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(element._groupName, groupName2);
- done();
- });
-
- element._handleSaveOwner().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(element._groupConfigOwner, 'testId2');
- done();
- });
- });
- });
-
- test('test for undefined group name', done => {
- groupStub.restore();
-
- sandbox.stub(
- element.$.restAPI,
- 'getGroupConfig',
- () => Promise.resolve({}));
-
- assert.isUndefined(element.groupId);
-
- element.groupId = 1;
-
- assert.isDefined(element.groupId);
-
- // Test that loading shows instead of filling
- // in group details
- element._loadGroup().then(() => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
-
- assert.isTrue(element._loading);
-
- done();
- });
- });
-
- test('test fire event', done => {
- element._groupConfig = {
- name: 'test-group',
- };
-
- sandbox.stub(element.$.restAPI, 'saveGroupName')
- .returns(Promise.resolve({status: 200}));
-
- const showStub = sandbox.stub(element, 'fire');
- element._handleSaveName()
- .then(() => {
- assert.isTrue(showStub.called);
- done();
- });
- });
-
- test('_computeGroupDisabled', () => {
- let admin = true;
- let owner = false;
- let groupIsInternal = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- admin = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- owner = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- owner = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- groupIsInternal = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- admin = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('fires page-error', done => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getGroupConfig', (group, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadGroup();
+ test('default values are populated with internal group', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getIsGroupOwner',
+ () => Promise.resolve(true));
+ element.groupId = 1;
+ element._loadGroup().then(() => {
+ assert.isTrue(element._groupIsInternal);
+ assert.isFalse(element.$.visibleToAll.bindValue);
+ done();
});
});
+
+ test('default values with external group', done => {
+ const groupExternal = Object.assign({}, group);
+ groupExternal.id = 'external-group-id';
+ groupStub.restore();
+ groupStub = sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve(groupExternal));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getIsGroupOwner',
+ () => Promise.resolve(true));
+ element.groupId = 1;
+ element._loadGroup().then(() => {
+ assert.isFalse(element._groupIsInternal);
+ assert.isFalse(element.$.visibleToAll.bindValue);
+ done();
+ });
+ });
+
+ test('rename group', done => {
+ const groupName = 'test-group';
+ const groupName2 = 'test-group2';
+ element.groupId = 1;
+ element._groupConfig = {
+ name: groupName,
+ };
+ element._groupConfigOwner = 'testId';
+ element._groupName = groupName;
+ element._groupOwner = true;
+
+ sandbox.stub(
+ element.$.restAPI,
+ 'getIsGroupOwner',
+ () => Promise.resolve(true));
+
+ sandbox.stub(
+ element.$.restAPI,
+ 'saveGroupName',
+ () => Promise.resolve({status: 200}));
+
+ const button = element.$.inputUpdateNameBtn;
+
+ element._loadGroup().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+
+ element.$.groupNameInput.text = groupName2;
+
+ element.$.groupOwnerInput.text = 'testId2';
+
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(element.$.groupName.classList.contains('edited'));
+
+ element._handleSaveName().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.equal(element._groupName, groupName2);
+ done();
+ });
+
+ element._handleSaveOwner().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ assert.equal(element._groupConfigOwner, 'testId2');
+ done();
+ });
+ });
+ });
+
+ test('test for undefined group name', done => {
+ groupStub.restore();
+
+ sandbox.stub(
+ element.$.restAPI,
+ 'getGroupConfig',
+ () => Promise.resolve({}));
+
+ assert.isUndefined(element.groupId);
+
+ element.groupId = 1;
+
+ assert.isDefined(element.groupId);
+
+ // Test that loading shows instead of filling
+ // in group details
+ element._loadGroup().then(() => {
+ assert.isTrue(element.$.loading.classList.contains('loading'));
+
+ assert.isTrue(element._loading);
+
+ done();
+ });
+ });
+
+ test('test fire event', done => {
+ element._groupConfig = {
+ name: 'test-group',
+ };
+
+ sandbox.stub(element.$.restAPI, 'saveGroupName')
+ .returns(Promise.resolve({status: 200}));
+
+ const showStub = sandbox.stub(element, 'fire');
+ element._handleSaveName()
+ .then(() => {
+ assert.isTrue(showStub.called);
+ done();
+ });
+ });
+
+ test('_computeGroupDisabled', () => {
+ let admin = true;
+ let owner = false;
+ let groupIsInternal = true;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), false);
+
+ admin = false;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+
+ owner = true;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), false);
+
+ owner = false;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+
+ groupIsInternal = false;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+
+ admin = true;
+ assert.equal(element._computeGroupDisabled(owner, admin,
+ groupIsInternal), true);
+ });
+
+ test('_computeLoadingClass', () => {
+ assert.equal(element._computeLoadingClass(true), 'loading');
+ assert.equal(element._computeLoadingClass(false), '');
+ });
+
+ test('fires page-error', done => {
+ groupStub.restore();
+
+ element.groupId = 1;
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getGroupConfig', (group, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._loadGroup();
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 508c3a2..4c5bbb8 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -14,301 +14,318 @@
* 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 = 20;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-rule-editor/gr-rule-editor.js';
+import {flush} 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-permission_html.js';
- const RANGE_NAMES = [
- 'QUERY LIMIT',
- 'BATCH CHANGES LIMIT',
- ];
+const MAX_AUTOCOMPLETE_RESULTS = 20;
+const RANGE_NAMES = [
+ 'QUERY LIMIT',
+ 'BATCH CHANGES LIMIT',
+];
+
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.FireMixin
+ */
+/**
+ * Fired when the permission has been modified or removed.
+ *
+ * @event access-modified
+ */
+/**
+ * Fired when a permission that was previously added was removed.
+ *
+ * @event added-permission-removed
+ * @extends Polymer.Element
+ */
+class GrPermission extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
/**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.FireMixin
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-access-section_test.
*/
- /**
- * Fired when the permission has been modified or removed.
- *
- * @event access-modified
- */
- /**
- * Fired when a permission that was previously added was removed.
- *
- * @event added-permission-removed
- * @extends Polymer.Element
- */
- class GrPermission extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-access-section_test.
- */
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-permission'; }
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- labels: Object,
- name: String,
- /** @type {?} */
- permission: {
- type: Object,
- observer: '_sortPermission',
- notify: true,
+ static get is() { return 'gr-permission'; }
+
+ static get properties() {
+ return {
+ labels: Object,
+ name: String,
+ /** @type {?} */
+ permission: {
+ type: Object,
+ observer: '_sortPermission',
+ notify: true,
+ },
+ groups: Object,
+ section: String,
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ _label: {
+ type: Object,
+ computed: '_computeLabel(permission, labels)',
+ },
+ _groupFilter: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
},
- groups: Object,
- section: String,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- _label: {
- type: Object,
- computed: '_computeLabel(permission, labels)',
- },
- _groupFilter: String,
- _query: {
- type: Function,
- value() {
- return this._getGroupSuggestions.bind(this);
- },
- },
- _rules: Array,
- _groupsWithRules: Object,
- _deleted: {
- type: Boolean,
- value: false,
- },
- _originalExclusiveValue: Boolean,
- };
- }
+ },
+ _rules: Array,
+ _groupsWithRules: Object,
+ _deleted: {
+ type: Boolean,
+ value: false,
+ },
+ _originalExclusiveValue: Boolean,
+ };
+ }
- static get observers() {
- return [
- '_handleRulesChanged(_rules.splices)',
- ];
- }
+ static get observers() {
+ return [
+ '_handleRulesChanged(_rules.splices)',
+ ];
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-saved',
+ () => this._handleAccessSaved());
+ }
- /** @override */
- ready() {
- super.ready();
- this._setupValues();
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._setupValues();
+ }
- _setupValues() {
- if (!this.permission) { return; }
- this._originalExclusiveValue = !!this.permission.value.exclusive;
- Polymer.dom.flush();
- }
+ _setupValues() {
+ if (!this.permission) { return; }
+ this._originalExclusiveValue = !!this.permission.value.exclusive;
+ flush();
+ }
- _handleAccessSaved() {
- // Set a new 'original' value to keep track of after the value has been
- // saved.
- this._setupValues();
- }
+ _handleAccessSaved() {
+ // Set a new 'original' value to keep track of after the value has been
+ // saved.
+ this._setupValues();
+ }
- _permissionIsOwnerOrGlobal(permissionId, section) {
- return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
- }
+ _permissionIsOwnerOrGlobal(permissionId, section) {
+ return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
+ }
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld) { return; }
- // Restore original values if no longer editing.
- if (!editing) {
- this._deleted = false;
- delete this.permission.value.deleted;
- this._groupFilter = '';
- this._rules = this._rules.filter(rule => !rule.value.added);
- for (const key of Object.keys(this.permission.value.rules)) {
- if (this.permission.value.rules[key].added) {
- delete this.permission.value.rules[key];
- }
- }
-
- // Restore exclusive bit to original.
- this.set(['permission', 'value', 'exclusive'],
- this._originalExclusiveValue);
- }
- }
-
- _handleAddedRuleRemoved(e) {
- const index = e.model.index;
- this._rules = this._rules.slice(0, index)
- .concat(this._rules.slice(index + 1, this._rules.length));
- }
-
- _handleValueChange() {
- this.permission.value.modified = true;
- // Allows overall access page to know a change has been made.
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleRemovePermission() {
- if (this.permission.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-permission-removed', {bubbles: true, composed: true}));
- }
- this._deleted = true;
- this.permission.value.deleted = true;
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleRulesChanged(changeRecord) {
- // Update the groups to exclude in the autocomplete.
- this._groupsWithRules = this._computeGroupsWithRules(this._rules);
- }
-
- _sortPermission(permission) {
- this._rules = this.toSortedArray(permission.value.rules);
- }
-
- _computeSectionClass(editing, deleted) {
- const classList = [];
- if (editing) {
- classList.push('editing');
- }
- if (deleted) {
- classList.push('deleted');
- }
- return classList.join(' ');
- }
-
- _handleUndoRemove() {
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld) { return; }
+ // Restore original values if no longer editing.
+ if (!editing) {
this._deleted = false;
delete this.permission.value.deleted;
- }
-
- _computeLabel(permission, labels) {
- if (!labels || !permission ||
- !permission.value || !permission.value.label) { return; }
-
- const labelName = permission.value.label;
-
- // It is possible to have a label name that is not included in the
- // 'labels' object. In this case, treat it like anything else.
- if (!labels[labelName]) { return; }
- const label = {
- name: labelName,
- values: this._computeLabelValues(labels[labelName].values),
- };
- return label;
- }
-
- _computeLabelValues(values) {
- const valuesArr = [];
- const keys = Object.keys(values)
- .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
-
- for (const key of keys) {
- let text = values[key];
- if (!text) { text = ''; }
- // The value from the server being used to choose which item is
- // selected is in integer form, so this must be converted.
- valuesArr.push({value: parseInt(key, 10), text});
- }
- return valuesArr;
- }
-
- /**
- * @param {!Array} rules
- * @return {!Object} Object with groups with rues as keys, and true as
- * value.
- */
- _computeGroupsWithRules(rules) {
- const groups = {};
- for (const rule of rules) {
- groups[rule.id] = true;
- }
- return groups;
- }
-
- _computeGroupName(groups, groupId) {
- return groups && groups[groupId] && groups[groupId].name ?
- groups[groupId].name : groupId;
- }
-
- _getGroupSuggestions() {
- return this.$.restAPI.getSuggestedGroups(
- this._groupFilter,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(response => {
- const groups = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- groups.push({
- name: key,
- value: response[key],
- });
- }
- // Does not return groups in which we already have rules for.
- return groups
- .filter(group => !this._groupsWithRules[group.value.id]);
- });
- }
-
- /**
- * Handles adding a skeleton item to the dom-repeat.
- * gr-rule-editor handles setting the default values.
- */
- _handleAddRuleItem(e) {
- // The group id is encoded, but have to decode in order for the access
- // API to work as expected.
- const groupId = decodeURIComponent(e.detail.value.id)
- .replace(/\+/g, ' ');
- // We cannot use "this.set(...)" here, because groupId may contain dots,
- // and dots in property path names are totally unsupported by Polymer.
- // Apparently Polymer picks up this change anyway, otherwise we should
- // have looked at using MutableData:
- // https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
- this.permission.value.rules[groupId] = {};
-
- // Purposely don't recompute sorted array so that the newly added rule
- // is the last item of the array.
- this.push('_rules', {
- id: groupId,
- });
-
- // Add the new group name to the groups object so the name renders
- // correctly.
- if (this.groups && !this.groups[groupId]) {
- this.groups[groupId] = {name: this.$.groupAutocomplete.text};
+ this._groupFilter = '';
+ this._rules = this._rules.filter(rule => !rule.value.added);
+ for (const key of Object.keys(this.permission.value.rules)) {
+ if (this.permission.value.rules[key].added) {
+ delete this.permission.value.rules[key];
+ }
}
- // Wait for new rule to get value populated via gr-rule-editor, and then
- // add to permission values as well, so that the change gets propogated
- // back to the section. Since the rule is inside a dom-repeat, a flush
- // is needed.
- Polymer.dom.flush();
- const value = this._rules[this._rules.length - 1].value;
- value.added = true;
- // See comment above for why we cannot use "this.set(...)" here.
- this.permission.value.rules[groupId] = value;
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _computeHasRange(name) {
- if (!name) { return false; }
-
- return RANGE_NAMES.includes(name.toUpperCase());
+ // Restore exclusive bit to original.
+ this.set(['permission', 'value', 'exclusive'],
+ this._originalExclusiveValue);
}
}
- customElements.define(GrPermission.is, GrPermission);
-})();
+ _handleAddedRuleRemoved(e) {
+ const index = e.model.index;
+ this._rules = this._rules.slice(0, index)
+ .concat(this._rules.slice(index + 1, this._rules.length));
+ }
+
+ _handleValueChange() {
+ this.permission.value.modified = true;
+ // Allows overall access page to know a change has been made.
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleRemovePermission() {
+ if (this.permission.value.added) {
+ this.dispatchEvent(new CustomEvent(
+ 'added-permission-removed', {bubbles: true, composed: true}));
+ }
+ this._deleted = true;
+ this.permission.value.deleted = true;
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleRulesChanged(changeRecord) {
+ // Update the groups to exclude in the autocomplete.
+ this._groupsWithRules = this._computeGroupsWithRules(this._rules);
+ }
+
+ _sortPermission(permission) {
+ this._rules = this.toSortedArray(permission.value.rules);
+ }
+
+ _computeSectionClass(editing, deleted) {
+ const classList = [];
+ if (editing) {
+ classList.push('editing');
+ }
+ if (deleted) {
+ classList.push('deleted');
+ }
+ return classList.join(' ');
+ }
+
+ _handleUndoRemove() {
+ this._deleted = false;
+ delete this.permission.value.deleted;
+ }
+
+ _computeLabel(permission, labels) {
+ if (!labels || !permission ||
+ !permission.value || !permission.value.label) { return; }
+
+ const labelName = permission.value.label;
+
+ // It is possible to have a label name that is not included in the
+ // 'labels' object. In this case, treat it like anything else.
+ if (!labels[labelName]) { return; }
+ const label = {
+ name: labelName,
+ values: this._computeLabelValues(labels[labelName].values),
+ };
+ return label;
+ }
+
+ _computeLabelValues(values) {
+ const valuesArr = [];
+ const keys = Object.keys(values)
+ .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
+
+ for (const key of keys) {
+ let text = values[key];
+ if (!text) { text = ''; }
+ // The value from the server being used to choose which item is
+ // selected is in integer form, so this must be converted.
+ valuesArr.push({value: parseInt(key, 10), text});
+ }
+ return valuesArr;
+ }
+
+ /**
+ * @param {!Array} rules
+ * @return {!Object} Object with groups with rues as keys, and true as
+ * value.
+ */
+ _computeGroupsWithRules(rules) {
+ const groups = {};
+ for (const rule of rules) {
+ groups[rule.id] = true;
+ }
+ return groups;
+ }
+
+ _computeGroupName(groups, groupId) {
+ return groups && groups[groupId] && groups[groupId].name ?
+ groups[groupId].name : groupId;
+ }
+
+ _getGroupSuggestions() {
+ return this.$.restAPI.getSuggestedGroups(
+ this._groupFilter,
+ MAX_AUTOCOMPLETE_RESULTS)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: response[key],
+ });
+ }
+ // Does not return groups in which we already have rules for.
+ return groups
+ .filter(group => !this._groupsWithRules[group.value.id]);
+ });
+ }
+
+ /**
+ * Handles adding a skeleton item to the dom-repeat.
+ * gr-rule-editor handles setting the default values.
+ */
+ _handleAddRuleItem(e) {
+ // The group id is encoded, but have to decode in order for the access
+ // API to work as expected.
+ const groupId = decodeURIComponent(e.detail.value.id)
+ .replace(/\+/g, ' ');
+ // We cannot use "this.set(...)" here, because groupId may contain dots,
+ // and dots in property path names are totally unsupported by Polymer.
+ // Apparently Polymer picks up this change anyway, otherwise we should
+ // have looked at using MutableData:
+ // https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
+ this.permission.value.rules[groupId] = {};
+
+ // Purposely don't recompute sorted array so that the newly added rule
+ // is the last item of the array.
+ this.push('_rules', {
+ id: groupId,
+ });
+
+ // Add the new group name to the groups object so the name renders
+ // correctly.
+ if (this.groups && !this.groups[groupId]) {
+ this.groups[groupId] = {name: this.$.groupAutocomplete.text};
+ }
+
+ // Wait for new rule to get value populated via gr-rule-editor, and then
+ // add to permission values as well, so that the change gets propogated
+ // back to the section. Since the rule is inside a dom-repeat, a flush
+ // is needed.
+ flush();
+ const value = this._rules[this._rules.length - 1].value;
+ value.added = true;
+ // See comment above for why we cannot use "this.set(...)" here.
+ this.permission.value.rules[groupId] = value;
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _computeHasRange(name) {
+ if (!name) { return false; }
+
+ return RANGE_NAMES.includes(name.toUpperCase());
+ }
+}
+
+customElements.define(GrPermission.is, GrPermission);
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
index e07f911..1b57336 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
@@ -1,34 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-access-behavior/gr-access-behavior.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-rule-editor/gr-rule-editor.html">
-
-<dom-module id="gr-permission">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -88,49 +76,23 @@
<style include="gr-menu-page-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <section
- id="permission"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
+ <section id="permission" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
<div id="mainContainer">
<div class="header">
<span class="title">[[name]]</span>
<div class="right">
- <template is=dom-if if="[[!_permissionIsOwnerOrGlobal(permission.id, section)]]">
- <paper-toggle-button
- id="exclusiveToggle"
- checked="{{permission.value.exclusive}}"
- on-change="_handleValueChange"
- disabled$="[[!editing]]"></paper-toggle-button>Exclusive
+ <template is="dom-if" if="[[!_permissionIsOwnerOrGlobal(permission.id, section)]]">
+ <paper-toggle-button id="exclusiveToggle" checked="{{permission.value.exclusive}}" on-change="_handleValueChange" disabled\$="[[!editing]]"></paper-toggle-button>Exclusive
</template>
- <gr-button
- link
- id="removeBtn"
- on-click="_handleRemovePermission">Remove</gr-button>
+ <gr-button link="" id="removeBtn" on-click="_handleRemovePermission">Remove</gr-button>
</div>
</div><!-- end header -->
<div class="rules">
- <template
- is="dom-repeat"
- items="{{_rules}}"
- as="rule">
- <gr-rule-editor
- has-range="[[_computeHasRange(name)]]"
- label="[[_label]]"
- editing="[[editing]]"
- group-id="[[rule.id]]"
- group-name="[[_computeGroupName(groups, rule.id)]]"
- permission="[[permission.id]]"
- rule="{{rule}}"
- section="[[section]]"
- on-added-rule-removed="_handleAddedRuleRemoved"></gr-rule-editor>
+ <template is="dom-repeat" items="{{_rules}}" as="rule">
+ <gr-rule-editor has-range="[[_computeHasRange(name)]]" label="[[_label]]" editing="[[editing]]" group-id="[[rule.id]]" group-name="[[_computeGroupName(groups, rule.id)]]" permission="[[permission.id]]" rule="{{rule}}" section="[[section]]" on-added-rule-removed="_handleAddedRuleRemoved"></gr-rule-editor>
</template>
<div id="addRule">
- <gr-autocomplete
- id="groupAutocomplete"
- text="{{_groupFilter}}"
- query="[[_query]]"
- placeholder="Add group"
- on-commit="_handleAddRuleItem">
+ <gr-autocomplete id="groupAutocomplete" text="{{_groupFilter}}" query="[[_query]]" placeholder="Add group" on-commit="_handleAddRuleItem">
</gr-autocomplete>
</div>
<!-- end addRule -->
@@ -138,13 +100,8 @@
</div><!-- end mainContainer -->
<div id="deletedContainer">
<span>[[name]] was deleted</span>
- <gr-button
- link
- id="undoRemoveBtn"
- on-click="_handleUndoRemove">Undo</gr-button>
+ <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
</div><!-- end deletedContainer -->
</section>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-permission.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index f3c1e4f..6f05029 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-permission</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/page/page.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-permission.html">
+<script src="/node_modules/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 type="module" src="./gr-permission.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-permission.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,399 +41,401 @@
</template>
</test-fixture>
-<script>
- suite('gr-permission 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-permission.js';
+suite('gr-permission tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
- Promise.resolve({
- 'Administrators': {
- id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
+ Promise.resolve({
+ 'Administrators': {
+ id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
+ },
+ 'Anonymous Users': {
+ id: 'global%3AAnonymous-Users',
+ },
+ }));
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('unit tests', () => {
+ test('_sortPermission', () => {
+ const permission = {
+ id: 'submit',
+ value: {
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
},
- 'Anonymous Users': {
- id: 'global%3AAnonymous-Users',
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
},
- }));
+ },
+ },
+ };
+
+ const expectedRules = [
+ {
+ id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
+ value: {action: 'ALLOW', force: false},
+ },
+ {
+ id: 'global:Project-Owners',
+ value: {action: 'ALLOW', force: false},
+ },
+ ];
+
+ element._sortPermission(permission);
+ assert.deepEqual(element._rules, expectedRules);
});
- teardown(() => {
- sandbox.restore();
+ test('_computeLabel and _computeLabelValues', () => {
+ const labels = {
+ 'Code-Review': {
+ default_value: 0,
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ },
+ };
+ let permission = {
+ id: 'label-Code-Review',
+ value: {
+ label: 'Code-Review',
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ },
+ },
+ };
+
+ const expectedLabelValues = [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: 0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ];
+
+ const expectedLabel = {
+ name: 'Code-Review',
+ values: expectedLabelValues,
+ };
+
+ assert.deepEqual(element._computeLabelValues(
+ labels['Code-Review'].values), expectedLabelValues);
+
+ assert.deepEqual(element._computeLabel(permission, labels),
+ expectedLabel);
+
+ permission = {
+ id: 'label-reviewDB',
+ value: {
+ label: 'reviewDB',
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
+ },
+ },
+ },
+ };
+
+ assert.isNotOk(element._computeLabel(permission, labels));
});
- suite('unit tests', () => {
- test('_sortPermission', () => {
- const permission = {
- id: 'submit',
- value: {
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- },
- },
- },
- };
+ test('_computeSectionClass', () => {
+ let deleted = true;
+ let editing = false;
+ assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
- const expectedRules = [
+ deleted = false;
+ assert.equal(element._computeSectionClass(editing, deleted), '');
+
+ editing = true;
+ assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+
+ deleted = true;
+ assert.equal(element._computeSectionClass(editing, deleted),
+ 'editing deleted');
+ });
+
+ test('_computeGroupName', () => {
+ const groups = {
+ abc123: {name: 'test group'},
+ bcd234: {},
+ };
+ assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
+ assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
+ });
+
+ test('_computeGroupsWithRules', () => {
+ const rules = [
+ {
+ id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
+ value: {action: 'ALLOW', force: false},
+ },
+ {
+ id: 'global:Project-Owners',
+ value: {action: 'ALLOW', force: false},
+ },
+ ];
+ const groupsWithRules = {
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': true,
+ 'global:Project-Owners': true,
+ };
+ assert.deepEqual(element._computeGroupsWithRules(rules),
+ groupsWithRules);
+ });
+
+ test('_getGroupSuggestions without existing rules', done => {
+ element._groupsWithRules = {};
+
+ element._getGroupSuggestions().then(groups => {
+ assert.deepEqual(groups, [
{
- id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
- value: {action: 'ALLOW', force: false},
- },
- {
- id: 'global:Project-Owners',
- value: {action: 'ALLOW', force: false},
- },
- ];
-
- element._sortPermission(permission);
- assert.deepEqual(element._rules, expectedRules);
- });
-
- test('_computeLabel and _computeLabelValues', () => {
- const labels = {
- 'Code-Review': {
- default_value: 0,
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- },
- };
- let permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- },
- },
- };
-
- const expectedLabelValues = [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: 0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ];
-
- const expectedLabel = {
- name: 'Code-Review',
- values: expectedLabelValues,
- };
-
- assert.deepEqual(element._computeLabelValues(
- labels['Code-Review'].values), expectedLabelValues);
-
- assert.deepEqual(element._computeLabel(permission, labels),
- expectedLabel);
-
- permission = {
- id: 'label-reviewDB',
- value: {
- label: 'reviewDB',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- },
- },
- },
- };
-
- assert.isNotOk(element._computeLabel(permission, labels));
- });
-
- test('_computeSectionClass', () => {
- let deleted = true;
- let editing = false;
- assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
-
- deleted = false;
- assert.equal(element._computeSectionClass(editing, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, deleted), 'editing');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, deleted),
- 'editing deleted');
- });
-
- test('_computeGroupName', () => {
- const groups = {
- abc123: {name: 'test group'},
- bcd234: {},
- };
- assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
- assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
- });
-
- test('_computeGroupsWithRules', () => {
- const rules = [
- {
- id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
- value: {action: 'ALLOW', force: false},
- },
- {
- id: 'global:Project-Owners',
- value: {action: 'ALLOW', force: false},
- },
- ];
- const groupsWithRules = {
- '4c97682e6ce6b7247f3381b6f1789356666de7f': true,
- 'global:Project-Owners': true,
- };
- assert.deepEqual(element._computeGroupsWithRules(rules),
- groupsWithRules);
- });
-
- test('_getGroupSuggestions without existing rules', done => {
- element._groupsWithRules = {};
-
- element._getGroupSuggestions().then(groups => {
- assert.deepEqual(groups, [
- {
- name: 'Administrators',
- value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
- }, {
- name: 'Anonymous Users',
- value: {id: 'global%3AAnonymous-Users'},
- },
- ]);
- done();
- });
- });
-
- test('_getGroupSuggestions with existing rules filters them', done => {
- element._groupsWithRules = {
- '4c97682e6ce61b7247f3381b6f1789356666de7f': true,
- };
-
- element._getGroupSuggestions().then(groups => {
- assert.deepEqual(groups, [{
+ name: 'Administrators',
+ value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
+ }, {
name: 'Anonymous Users',
value: {id: 'global%3AAnonymous-Users'},
- }]);
- done();
- });
- });
-
- test('_handleRemovePermission', () => {
- element.editing = true;
- element.permission = {value: {rules: {}}};
- element._handleRemovePermission();
- assert.isTrue(element._deleted);
- assert.isTrue(element.permission.value.deleted);
-
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.permission.value.deleted);
- });
-
- test('_handleUndoRemove', () => {
- element.permission = {value: {deleted: true, rules: {}}};
- element._handleUndoRemove();
- assert.isFalse(element._deleted);
- assert.isNotOk(element.permission.value.deleted);
- });
-
- test('_computeHasRange', () => {
- assert.isTrue(element._computeHasRange('Query Limit'));
-
- assert.isTrue(element._computeHasRange('Batch Changes Limit'));
-
- assert.isFalse(element._computeHasRange('test'));
+ },
+ ]);
+ done();
});
});
- suite('interactions', () => {
- setup(() => {
- sandbox.spy(element, '_computeLabel');
- element.name = 'Priority';
- element.section = 'refs/*';
- element.labels = {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- default_value: 0,
- },
- };
- element.permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- },
- },
- };
- element._setupValues();
- flushAsynchronousOperations();
+ test('_getGroupSuggestions with existing rules filters them', done => {
+ element._groupsWithRules = {
+ '4c97682e6ce61b7247f3381b6f1789356666de7f': true,
+ };
+
+ element._getGroupSuggestions().then(groups => {
+ assert.deepEqual(groups, [{
+ name: 'Anonymous Users',
+ value: {id: 'global%3AAnonymous-Users'},
+ }]);
+ done();
});
+ });
- test('adding a rule', () => {
- element.name = 'Priority';
- element.section = 'refs/*';
- element.groups = {};
- element.$.groupAutocomplete.text = 'ldap/tests te.st';
- const e = {
- detail: {
- value: {
- id: 'ldap:CN=test+te.st',
- },
- },
- };
- element.editing = true;
- assert.equal(element._rules.length, 2);
- assert.equal(Object.keys(element._groupsWithRules).length, 2);
- element._handleAddRuleItem(e);
- flushAsynchronousOperations();
- assert.deepEqual(element.groups, {'ldap:CN=test te.st': {
- name: 'ldap/tests te.st'}});
- assert.equal(element._rules.length, 3);
- assert.equal(Object.keys(element._groupsWithRules).length, 3);
- assert.deepEqual(element.permission.value.rules['ldap:CN=test te.st'],
- {action: 'ALLOW', min: -2, max: 2, added: true});
- // New rule should be removed if cancel from editing.
- element.editing = false;
- assert.equal(element._rules.length, 2);
- assert.equal(Object.keys(element.permission.value.rules).length, 2);
- });
+ test('_handleRemovePermission', () => {
+ element.editing = true;
+ element.permission = {value: {rules: {}}};
+ element._handleRemovePermission();
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.permission.value.deleted);
- test('removing an added rule', () => {
- element.name = 'Priority';
- element.section = 'refs/*';
- element.groups = {};
- element.$.groupAutocomplete.text = 'new group name';
- assert.equal(element._rules.length, 2);
- element.shadowRoot
- .querySelector('gr-rule-editor').fire('added-rule-removed');
- flushAsynchronousOperations();
- assert.equal(element._rules.length, 1);
- });
+ element.editing = false;
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.permission.value.deleted);
+ });
- test('removing an added permission', () => {
- const removeStub = sandbox.stub();
- element.addEventListener('added-permission-removed', removeStub);
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
- element.permission.value.added = true;
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(removeStub.called);
- });
+ test('_handleUndoRemove', () => {
+ element.permission = {value: {deleted: true, rules: {}}};
+ element._handleUndoRemove();
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.permission.value.deleted);
+ });
- test('removing the permission', () => {
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
+ test('_computeHasRange', () => {
+ assert.isTrue(element._computeHasRange('Query Limit'));
- const removeStub = sandbox.stub();
- element.addEventListener('added-permission-removed', removeStub);
+ assert.isTrue(element._computeHasRange('Batch Changes Limit'));
- assert.isFalse(element.$.permission.classList.contains('deleted'));
- assert.isFalse(element._deleted);
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(element.$.permission.classList.contains('deleted'));
- assert.isTrue(element._deleted);
- MockInteractions.tap(element.$.undoRemoveBtn);
- assert.isFalse(element.$.permission.classList.contains('deleted'));
- assert.isFalse(element._deleted);
- assert.isFalse(removeStub.called);
- });
-
- test('modify a permission', () => {
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
-
- assert.isFalse(element._originalExclusiveValue);
- assert.isNotOk(element.permission.value.modified);
- MockInteractions.tap(element.shadowRoot
- .querySelector('#exclusiveToggle'));
- flushAsynchronousOperations();
- assert.isTrue(element.permission.value.exclusive);
- assert.isTrue(element.permission.value.modified);
- assert.isFalse(element._originalExclusiveValue);
- element.editing = false;
- assert.isFalse(element.permission.value.exclusive);
- });
-
- test('_handleValueChange', () => {
- const modifiedHandler = sandbox.stub();
- element.permission = {value: {rules: {}}};
- element.addEventListener('access-modified', modifiedHandler);
- assert.isNotOk(element.permission.value.modified);
- element._handleValueChange();
- assert.isTrue(element.permission.value.modified);
- assert.isTrue(modifiedHandler.called);
- });
-
- test('Exclusive hidden for owner permission', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'flex');
- element.set(['permission', 'id'], 'owner');
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'none');
- });
-
- test('Exclusive hidden for any global permissions', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'flex');
- element.section = 'GLOBAL_CAPABILITIES';
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'none');
- });
+ assert.isFalse(element._computeHasRange('test'));
});
});
+
+ suite('interactions', () => {
+ setup(() => {
+ sandbox.spy(element, '_computeLabel');
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ };
+ element.permission = {
+ id: 'label-Code-Review',
+ value: {
+ label: 'Code-Review',
+ rules: {
+ 'global:Project-Owners': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: 'ALLOW',
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ },
+ },
+ };
+ element._setupValues();
+ flushAsynchronousOperations();
+ });
+
+ test('adding a rule', () => {
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.groups = {};
+ element.$.groupAutocomplete.text = 'ldap/tests te.st';
+ const e = {
+ detail: {
+ value: {
+ id: 'ldap:CN=test+te.st',
+ },
+ },
+ };
+ element.editing = true;
+ assert.equal(element._rules.length, 2);
+ assert.equal(Object.keys(element._groupsWithRules).length, 2);
+ element._handleAddRuleItem(e);
+ flushAsynchronousOperations();
+ assert.deepEqual(element.groups, {'ldap:CN=test te.st': {
+ name: 'ldap/tests te.st'}});
+ assert.equal(element._rules.length, 3);
+ assert.equal(Object.keys(element._groupsWithRules).length, 3);
+ assert.deepEqual(element.permission.value.rules['ldap:CN=test te.st'],
+ {action: 'ALLOW', min: -2, max: 2, added: true});
+ // New rule should be removed if cancel from editing.
+ element.editing = false;
+ assert.equal(element._rules.length, 2);
+ assert.equal(Object.keys(element.permission.value.rules).length, 2);
+ });
+
+ test('removing an added rule', () => {
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.groups = {};
+ element.$.groupAutocomplete.text = 'new group name';
+ assert.equal(element._rules.length, 2);
+ element.shadowRoot
+ .querySelector('gr-rule-editor').fire('added-rule-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._rules.length, 1);
+ });
+
+ test('removing an added permission', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.permission.value.added = true;
+ MockInteractions.tap(element.$.removeBtn);
+ assert.isTrue(removeStub.called);
+ });
+
+ test('removing the permission', () => {
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*';
+
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+
+ assert.isFalse(element.$.permission.classList.contains('deleted'));
+ assert.isFalse(element._deleted);
+ MockInteractions.tap(element.$.removeBtn);
+ assert.isTrue(element.$.permission.classList.contains('deleted'));
+ assert.isTrue(element._deleted);
+ MockInteractions.tap(element.$.undoRemoveBtn);
+ assert.isFalse(element.$.permission.classList.contains('deleted'));
+ assert.isFalse(element._deleted);
+ assert.isFalse(removeStub.called);
+ });
+
+ test('modify a permission', () => {
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*';
+
+ assert.isFalse(element._originalExclusiveValue);
+ assert.isNotOk(element.permission.value.modified);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#exclusiveToggle'));
+ flushAsynchronousOperations();
+ assert.isTrue(element.permission.value.exclusive);
+ assert.isTrue(element.permission.value.modified);
+ assert.isFalse(element._originalExclusiveValue);
+ element.editing = false;
+ assert.isFalse(element.permission.value.exclusive);
+ });
+
+ test('_handleValueChange', () => {
+ const modifiedHandler = sandbox.stub();
+ element.permission = {value: {rules: {}}};
+ element.addEventListener('access-modified', modifiedHandler);
+ assert.isNotOk(element.permission.value.modified);
+ element._handleValueChange();
+ assert.isTrue(element.permission.value.modified);
+ assert.isTrue(modifiedHandler.called);
+ });
+
+ test('Exclusive hidden for owner permission', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'flex');
+ element.set(['permission', 'id'], 'owner');
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'none');
+ });
+
+ test('Exclusive hidden for any global permissions', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'flex');
+ element.section = 'GLOBAL_CAPABILITIES';
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#exclusiveToggle')).display,
+ 'none');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
index 92a8655..318c2c3 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -14,84 +14,95 @@
* 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 GrPluginConfigArrayEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-plugin-config-array-editor'; }
- /**
- * Fired when the plugin config option changes.
- *
- * @event plugin-config-option-changed
- */
+import '@polymer/iron-input/iron-input.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-plugin-config-array-editor_html.js';
- static get properties() {
- return {
+/** @extends Polymer.Element */
+class GrPluginConfigArrayEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-plugin-config-array-editor'; }
+ /**
+ * Fired when the plugin config option changes.
+ *
+ * @event plugin-config-option-changed
+ */
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ pluginOption: Object,
+ /** @type {boolean} */
+ disabled: {
+ type: Boolean,
+ computed: '_computeDisabled(pluginOption.*)',
+ },
/** @type {?} */
- pluginOption: Object,
- /** @type {boolean} */
- disabled: {
- type: Boolean,
- computed: '_computeDisabled(pluginOption.*)',
- },
- /** @type {?} */
- _newValue: {
- type: String,
- value: '',
- },
- };
- }
+ _newValue: {
+ type: String,
+ value: '',
+ },
+ };
+ }
- _computeDisabled(record) {
- return !(record && record.base && record.base.info &&
- record.base.info.editable);
- }
+ _computeDisabled(record) {
+ return !(record && record.base && record.base.info &&
+ record.base.info.editable);
+ }
- _handleAddTap(e) {
+ _handleAddTap(e) {
+ e.preventDefault();
+ this._handleAdd();
+ }
+
+ _handleInputKeydown(e) {
+ // Enter.
+ if (e.keyCode === 13) {
e.preventDefault();
this._handleAdd();
}
-
- _handleInputKeydown(e) {
- // Enter.
- if (e.keyCode === 13) {
- e.preventDefault();
- this._handleAdd();
- }
- }
-
- _handleAdd() {
- if (!this._newValue.length) { return; }
- this._dispatchChanged(
- this.pluginOption.info.values.concat([this._newValue]));
- this._newValue = '';
- }
-
- _handleDelete(e) {
- const value = Polymer.dom(e).localTarget.dataset.item;
- this._dispatchChanged(
- this.pluginOption.info.values.filter(str => str !== value));
- }
-
- _dispatchChanged(values) {
- const {_key, info} = this.pluginOption;
- const detail = {
- _key,
- info: Object.assign(info, {values}, {}),
- notifyPath: `${_key}.values`,
- };
- this.dispatchEvent(
- new CustomEvent('plugin-config-option-changed', {detail}));
- }
-
- _computeShowInputRow(disabled) {
- return disabled ? 'hide' : '';
- }
}
- customElements.define(GrPluginConfigArrayEditor.is,
- GrPluginConfigArrayEditor);
-})();
+ _handleAdd() {
+ if (!this._newValue.length) { return; }
+ this._dispatchChanged(
+ this.pluginOption.info.values.concat([this._newValue]));
+ this._newValue = '';
+ }
+
+ _handleDelete(e) {
+ const value = dom(e).localTarget.dataset.item;
+ this._dispatchChanged(
+ this.pluginOption.info.values.filter(str => str !== value));
+ }
+
+ _dispatchChanged(values) {
+ const {_key, info} = this.pluginOption;
+ const detail = {
+ _key,
+ info: Object.assign(info, {values}, {}),
+ notifyPath: `${_key}.values`,
+ };
+ this.dispatchEvent(
+ new CustomEvent('plugin-config-option-changed', {detail}));
+ }
+
+ _computeShowInputRow(disabled) {
+ return disabled ? 'hide' : '';
+ }
+}
+
+customElements.define(GrPluginConfigArrayEditor.is,
+ GrPluginConfigArrayEditor);
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
index f6c744b..d97e2b37 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
@@ -1,30 +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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-
-<dom-module id="gr-plugin-config-array-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -72,11 +64,7 @@
<template is="dom-repeat" items="[[pluginOption.info.values]]">
<div class="row">
<span>[[item]]</span>
- <gr-button
- link
- disabled$="[[disabled]]"
- data-item$="[[item]]"
- on-click="_handleDelete">Delete</gr-button>
+ <gr-button link="" disabled\$="[[disabled]]" data-item\$="[[item]]" on-click="_handleDelete">Delete</gr-button>
</div>
</template>
</div>
@@ -84,23 +72,11 @@
<template is="dom-if" if="[[!pluginOption.info.values.length]]">
<div class="row placeholder">None configured.</div>
</template>
- <div class$="row [[_computeShowInputRow(disabled)]]">
- <iron-input
- on-keydown="_handleInputKeydown"
- bind-value="{{_newValue}}">
- <input
- is="iron-input"
- id="input"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newValue}}">
+ <div class\$="row [[_computeShowInputRow(disabled)]]">
+ <iron-input on-keydown="_handleInputKeydown" bind-value="{{_newValue}}">
+ <input is="iron-input" id="input" on-keydown="_handleInputKeydown" bind-value="{{_newValue}}">
</iron-input>
- <gr-button
- id="addButton"
- disabled$="[[!_newValue.length]]"
- link
- on-click="_handleAddTap">Add</gr-button>
+ <gr-button id="addButton" disabled\$="[[!_newValue.length]]" link="" on-click="_handleAddTap">Add</gr-button>
</div>
</div>
- </template>
- <script src="gr-plugin-config-array-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
index 3342967..f66346e 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_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-plugin-config-array-editor</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-plugin-config-array-editor.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-plugin-config-array-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-plugin-config-array-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,112 +40,115 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-config-array-editor tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let dispatchStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-plugin-config-array-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-plugin-config-array-editor tests', () => {
+ let element;
+ let sandbox;
+ let dispatchStub;
- const getAll = str => Polymer.dom(element.root).querySelectorAll(str);
+ const getAll = str => dom(element.root).querySelectorAll(str);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.pluginOption = {
+ _key: 'test-key',
+ info: {
+ values: [],
+ },
+ };
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('_computeShowInputRow', () => {
+ assert.equal(element._computeShowInputRow(true), 'hide');
+ assert.equal(element._computeShowInputRow(false), '');
+ });
+
+ test('_computeDisabled', () => {
+ assert.isTrue(element._computeDisabled({}));
+ assert.isTrue(element._computeDisabled({base: {}}));
+ assert.isTrue(element._computeDisabled({base: {info: {}}}));
+ assert.isTrue(
+ element._computeDisabled({base: {info: {editable: false}}}));
+ assert.isFalse(
+ element._computeDisabled({base: {info: {editable: true}}}));
+ });
+
+ suite('adding', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.pluginOption = {
- _key: 'test-key',
- info: {
- values: [],
- },
- };
- });
-
- teardown(() => sandbox.restore());
-
- test('_computeShowInputRow', () => {
- assert.equal(element._computeShowInputRow(true), 'hide');
- assert.equal(element._computeShowInputRow(false), '');
- });
-
- test('_computeDisabled', () => {
- assert.isTrue(element._computeDisabled({}));
- assert.isTrue(element._computeDisabled({base: {}}));
- assert.isTrue(element._computeDisabled({base: {info: {}}}));
- assert.isTrue(
- element._computeDisabled({base: {info: {editable: false}}}));
- assert.isFalse(
- element._computeDisabled({base: {info: {editable: true}}}));
- });
-
- suite('adding', () => {
- setup(() => {
- dispatchStub = sandbox.stub(element, '_dispatchChanged');
- });
-
- test('with enter', () => {
- element._newValue = '';
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
- flushAsynchronousOperations();
-
- assert.isFalse(dispatchStub.called);
- element._newValue = 'test';
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
- flushAsynchronousOperations();
-
- assert.isTrue(dispatchStub.called);
- assert.equal(dispatchStub.lastCall.args[0], 'test');
- assert.equal(element._newValue, '');
- });
-
- test('with add btn', () => {
- element._newValue = '';
- MockInteractions.tap(element.$.addButton);
- flushAsynchronousOperations();
-
- assert.isFalse(dispatchStub.called);
- element._newValue = 'test';
- MockInteractions.tap(element.$.addButton);
- flushAsynchronousOperations();
-
- assert.isTrue(dispatchStub.called);
- assert.equal(dispatchStub.lastCall.args[0], 'test');
- assert.equal(element._newValue, '');
- });
- });
-
- test('deleting', () => {
dispatchStub = sandbox.stub(element, '_dispatchChanged');
- element.pluginOption = {info: {values: ['test', 'test2']}};
- flushAsynchronousOperations();
+ });
- const rows = getAll('.existingItems .row');
- assert.equal(rows.length, 2);
- const button = rows[0].querySelector('gr-button');
-
- MockInteractions.tap(button);
+ test('with enter', () => {
+ element._newValue = '';
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isFalse(dispatchStub.called);
- element.pluginOption.info.editable = true;
- element.notifyPath('pluginOption.info.editable');
- flushAsynchronousOperations();
-
- MockInteractions.tap(button);
+ element._newValue = 'test';
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isTrue(dispatchStub.called);
- assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
+ assert.equal(dispatchStub.lastCall.args[0], 'test');
+ assert.equal(element._newValue, '');
});
- test('_dispatchChanged', () => {
- const eventStub = sandbox.stub(element, 'dispatchEvent');
- element._dispatchChanged(['new-test-value']);
+ test('with add btn', () => {
+ element._newValue = '';
+ MockInteractions.tap(element.$.addButton);
+ flushAsynchronousOperations();
- assert.isTrue(eventStub.called);
- const {detail} = eventStub.lastCall.args[0];
- assert.equal(detail._key, 'test-key');
- assert.deepEqual(detail.info, {values: ['new-test-value']});
- assert.equal(detail.notifyPath, 'test-key.values');
+ assert.isFalse(dispatchStub.called);
+ element._newValue = 'test';
+ MockInteractions.tap(element.$.addButton);
+ flushAsynchronousOperations();
+
+ assert.isTrue(dispatchStub.called);
+ assert.equal(dispatchStub.lastCall.args[0], 'test');
+ assert.equal(element._newValue, '');
});
});
+
+ test('deleting', () => {
+ dispatchStub = sandbox.stub(element, '_dispatchChanged');
+ element.pluginOption = {info: {values: ['test', 'test2']}};
+ flushAsynchronousOperations();
+
+ const rows = getAll('.existingItems .row');
+ assert.equal(rows.length, 2);
+ const button = rows[0].querySelector('gr-button');
+
+ MockInteractions.tap(button);
+ flushAsynchronousOperations();
+
+ assert.isFalse(dispatchStub.called);
+ element.pluginOption.info.editable = true;
+ element.notifyPath('pluginOption.info.editable');
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(button);
+ flushAsynchronousOperations();
+
+ assert.isTrue(dispatchStub.called);
+ assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
+ });
+
+ test('_dispatchChanged', () => {
+ const eventStub = sandbox.stub(element, 'dispatchEvent');
+ element._dispatchChanged(['new-test-value']);
+
+ assert.isTrue(eventStub.called);
+ const {detail} = eventStub.lastCall.args[0];
+ assert.equal(detail._key, 'test-key');
+ assert.deepEqual(detail.info, {values: ['new-test-value']});
+ assert.equal(detail.notifyPath, 'test-key.values');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
index 5dd6ec2..d5a4e08 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -14,110 +14,122 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
- */
- class GrPluginList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-plugin-list'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-plugin-list_html.js';
- static get properties() {
- return {
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrPluginList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-plugin-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
/**
- * URL params passed from the router.
+ * Offset of currently visible query results.
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- /**
- * Offset of currently visible query results.
- */
- _offset: {
- type: Number,
- value: 0,
- },
- _path: {
- type: String,
- readOnly: true,
- value: '/admin/plugins',
- },
- _plugins: Array,
- /**
- * Because we request one more than the pluginsPerPage, _shownPlugins
- * maybe one less than _plugins.
- * */
- _shownPlugins: {
- type: Array,
- computed: 'computeShownItems(_plugins)',
- },
- _pluginsPerPage: {
- type: Number,
- value: 25,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: {
- type: String,
- value: '',
- },
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this.fire('title-change', {title: 'Plugins'});
- }
-
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getPlugins(this._filter, this._pluginsPerPage,
- this._offset);
- }
-
- _getPlugins(filter, pluginsPerPage, offset) {
- const errFn = response => {
- this.fire('page-error', {response});
- };
- return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn)
- .then(plugins => {
- if (!plugins) {
- this._plugins = [];
- return;
- }
- this._plugins = Object.keys(plugins)
- .map(key => {
- const plugin = plugins[key];
- plugin.name = key;
- return plugin;
- });
- this._loading = false;
- });
- }
-
- _status(item) {
- return item.disabled === true ? 'Disabled' : 'Enabled';
- }
-
- _computePluginUrl(id) {
- return this.getUrl('/', id);
- }
+ _offset: {
+ type: Number,
+ value: 0,
+ },
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/admin/plugins',
+ },
+ _plugins: Array,
+ /**
+ * Because we request one more than the pluginsPerPage, _shownPlugins
+ * maybe one less than _plugins.
+ * */
+ _shownPlugins: {
+ type: Array,
+ computed: 'computeShownItems(_plugins)',
+ },
+ _pluginsPerPage: {
+ type: Number,
+ value: 25,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrPluginList.is, GrPluginList);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.fire('title-change', {title: 'Plugins'});
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getPlugins(this._filter, this._pluginsPerPage,
+ this._offset);
+ }
+
+ _getPlugins(filter, pluginsPerPage, offset) {
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+ return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn)
+ .then(plugins => {
+ if (!plugins) {
+ this._plugins = [];
+ return;
+ }
+ this._plugins = Object.keys(plugins)
+ .map(key => {
+ const plugin = plugins[key];
+ plugin.name = key;
+ return plugin;
+ });
+ this._loading = false;
+ });
+ }
+
+ _status(item) {
+ return item.disabled === true ? 'Disabled' : 'Enabled';
+ }
+
+ _computePluginUrl(id) {
+ return this.getUrl('/', id);
+ }
+}
+
+customElements.define(GrPluginList.is, GrPluginList);
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
index b056f92..90192c4 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
@@ -1,58 +1,44 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-plugin-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <gr-list-view
- filter="[[_filter]]"
- items-per-page="[[_pluginsPerPage]]"
- items="[[_plugins]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- path="[[_path]]">
+ <gr-list-view filter="[[_filter]]" items-per-page="[[_pluginsPerPage]]" items="[[_plugins]]" loading="[[_loading]]" offset="[[_offset]]" path="[[_path]]">
<table id="list" class="genericList">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="name topHeader">Plugin Name</th>
<th class="version topHeader">Version</th>
<th class="status topHeader">Status</th>
</tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownPlugins]]">
<tr class="table">
<td class="name">
<template is="dom-if" if="[[item.index_url]]">
- <a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
+ <a href\$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
</template>
<template is="dom-if" if="[[!item.index_url]]">
[[item.id]]
@@ -66,6 +52,4 @@
</table>
</gr-list-view>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-plugin-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
index 67a6c3f..f89eb8e 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-list</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/page/page.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-plugin-list.html">
+<script src="/node_modules/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 type="module" src="./gr-plugin-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-plugin-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,151 +41,154 @@
</template>
</test-fixture>
-<script>
- let counter;
- const pluginGenerator = () => {
- const plugin = {
- id: `test${++counter}`,
- version: '3.0-SNAPSHOT',
- disabled: false,
- };
-
- if (counter !== 2) {
- plugin.index_url = `plugins/test${counter}/`;
- }
- return plugin;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-plugin-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+let counter;
+const pluginGenerator = () => {
+ const plugin = {
+ id: `test${++counter}`,
+ version: '3.0-SNAPSHOT',
+ disabled: false,
};
- suite('gr-plugin-list tests', async () => {
- await readyToTest();
- let element;
- let plugins;
- let sandbox;
- let value;
+ if (counter !== 2) {
+ plugin.index_url = `plugins/test${counter}/`;
+ }
+ return plugin;
+};
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- counter = 0;
+suite('gr-plugin-list tests', () => {
+ let element;
+ let plugins;
+ let sandbox;
+ let value;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ counter = 0;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with plugins', () => {
+ setup(done => {
+ plugins = _.times(26, pluginGenerator);
+
+ stub('gr-rest-api-interface', {
+ getPlugins(num, offset) {
+ return Promise.resolve(plugins);
+ },
+ });
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with plugins', () => {
- setup(done => {
- plugins = _.times(26, pluginGenerator);
-
- stub('gr-rest-api-interface', {
- getPlugins(num, offset) {
- return Promise.resolve(plugins);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('plugin in the list is formatted correctly', done => {
- flush(() => {
- assert.equal(element._plugins[2].id, 'test3');
- assert.equal(element._plugins[2].index_url, 'plugins/test3/');
- assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
- assert.equal(element._plugins[2].disabled, false);
- done();
- });
- });
-
- test('with and without urls', done => {
- flush(() => {
- const names = Polymer.dom(element.root).querySelectorAll('.name');
- assert.isOk(names[1].querySelector('a'));
- assert.equal(names[1].querySelector('a').innerText, 'test1');
- assert.isNotOk(names[2].querySelector('a'));
- assert.equal(names[2].innerText, 'test2');
- done();
- });
- });
-
- test('_shownPlugins', () => {
- assert.equal(element._shownPlugins.length, 25);
+ test('plugin in the list is formatted correctly', done => {
+ flush(() => {
+ assert.equal(element._plugins[2].id, 'test3');
+ assert.equal(element._plugins[2].index_url, 'plugins/test3/');
+ assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
+ assert.equal(element._plugins[2].disabled, false);
+ done();
});
});
- suite('list with less then 26 plugins', () => {
- setup(done => {
- plugins = _.times(25, pluginGenerator);
-
- stub('gr-rest-api-interface', {
- getPlugins(num, offset) {
- return Promise.resolve(plugins);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('_shownPlugins', () => {
- assert.equal(element._shownPlugins.length, 25);
+ test('with and without urls', done => {
+ flush(() => {
+ const names = dom(element.root).querySelectorAll('.name');
+ assert.isOk(names[1].querySelector('a'));
+ assert.equal(names[1].querySelector('a').innerText, 'test1');
+ assert.isNotOk(names[2].querySelector('a'));
+ assert.equal(names[2].innerText, 'test2');
+ done();
});
});
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getPlugins',
- () => Promise.resolve(plugins));
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value).then(() => {
- assert.equal(element.$.restAPI.getPlugins.lastCall.args[0],
- 'test');
- assert.equal(element.$.restAPI.getPlugins.lastCall.args[1],
- 25);
- assert.equal(element.$.restAPI.getPlugins.lastCall.args[2],
- 25);
- done();
- });
+ test('_shownPlugins', () => {
+ assert.equal(element._shownPlugins.length, 25);
+ });
+ });
+
+ suite('list with less then 26 plugins', () => {
+ setup(done => {
+ plugins = _.times(25, pluginGenerator);
+
+ stub('gr-rest-api-interface', {
+ getPlugins(num, offset) {
+ return Promise.resolve(plugins);
+ },
});
+
+ element._paramsChanged(value).then(() => { flush(done); });
});
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._plugins = _.times(25, pluginGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
+ test('_shownPlugins', () => {
+ assert.equal(element._shownPlugins.length, 25);
});
+ });
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getPlugins',
- (filter, pluginsPerPage, opt_offset, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value);
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getPlugins',
+ () => Promise.resolve(plugins));
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value).then(() => {
+ assert.equal(element.$.restAPI.getPlugins.lastCall.args[0],
+ 'test');
+ assert.equal(element.$.restAPI.getPlugins.lastCall.args[1],
+ 25);
+ assert.equal(element.$.restAPI.getPlugins.lastCall.args[2],
+ 25);
+ done();
});
});
});
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._plugins = _.times(25, pluginGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getPlugins',
+ (filter, pluginsPerPage, opt_offset, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index 02b62e0..6cfa7ff 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -14,497 +14,515 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const Defs = {};
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-access-section/gr-access-section.js';
+import '../../../scripts/util.js';
+import {flush, 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-repo-access_html.js';
- const NOTHING_TO_SAVE = 'No changes to save.';
+const Defs = {};
- const MAX_AUTOCOMPLETE_RESULTS = 50;
+const NOTHING_TO_SAVE = 'No changes to save.';
- /**
- * Fired when save is a no-op
- *
- * @event show-alert
- */
+const MAX_AUTOCOMPLETE_RESULTS = 50;
- /**
- * @typedef {{
- * value: !Object,
- * }}
- */
- Defs.rule;
+/**
+ * Fired when save is a no-op
+ *
+ * @event show-alert
+ */
- /**
- * @typedef {{
- * rules: !Object<string, Defs.rule>
- * }}
- */
- Defs.permission;
+/**
+ * @typedef {{
+ * value: !Object,
+ * }}
+ */
+Defs.rule;
- /**
- * Can be an empty object or consist of permissions.
- *
- * @typedef {{
- * permissions: !Object<string, Defs.permission>
- * }}
- */
- Defs.permissions;
+/**
+ * @typedef {{
+ * rules: !Object<string, Defs.rule>
+ * }}
+ */
+Defs.permission;
- /**
- * Can be an empty object or consist of permissions.
- *
- * @typedef {!Object<string, Defs.permissions>}
- */
- Defs.sections;
+/**
+ * Can be an empty object or consist of permissions.
+ *
+ * @typedef {{
+ * permissions: !Object<string, Defs.permission>
+ * }}
+ */
+Defs.permissions;
- /**
- * @typedef {{
- * remove: !Defs.sections,
- * add: !Defs.sections,
- * }}
- */
- Defs.projectAccessInput;
+/**
+ * Can be an empty object or consist of permissions.
+ *
+ * @typedef {!Object<string, Defs.permissions>}
+ */
+Defs.sections;
- /**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRepoAccess extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-access'; }
+/**
+ * @typedef {{
+ * remove: !Defs.sections,
+ * add: !Defs.sections,
+ * }}
+ */
+Defs.projectAccessInput;
- static get properties() {
- return {
- repo: {
- type: String,
- observer: '_repoChanged',
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRepoAccess extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-access'; }
+
+ static get properties() {
+ return {
+ repo: {
+ type: String,
+ observer: '_repoChanged',
+ },
+ // The current path
+ path: String,
+
+ _canUpload: {
+ type: Boolean,
+ value: false,
+ },
+ _inheritFromFilter: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getInheritFromSuggestions.bind(this);
},
- // The current path
- path: String,
+ },
+ _ownerOf: Array,
+ _capabilities: Object,
+ _groups: Object,
+ /** @type {?} */
+ _inheritsFrom: Object,
+ _labels: Object,
+ _local: Object,
+ _editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ _modified: {
+ type: Boolean,
+ value: false,
+ },
+ _sections: Array,
+ _weblinks: Array,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ };
+ }
- _canUpload: {
- type: Boolean,
- value: false,
- },
- _inheritFromFilter: String,
- _query: {
- type: Function,
- value() {
- return this._getInheritFromSuggestions.bind(this);
- },
- },
- _ownerOf: Array,
- _capabilities: Object,
- _groups: Object,
- /** @type {?} */
- _inheritsFrom: Object,
- _labels: Object,
- _local: Object,
- _editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- _modified: {
- type: Boolean,
- value: false,
- },
- _sections: Array,
- _weblinks: Array,
- _loading: {
- type: Boolean,
- value: true,
- },
- };
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-modified',
+ () =>
+ this._handleAccessModified());
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-modified',
- () =>
- this._handleAccessModified());
- }
+ _handleAccessModified() {
+ this._modified = true;
+ }
- _handleAccessModified() {
- this._modified = true;
- }
+ /**
+ * @param {string} repo
+ * @return {!Promise}
+ */
+ _repoChanged(repo) {
+ this._loading = true;
- /**
- * @param {string} repo
- * @return {!Promise}
- */
- _repoChanged(repo) {
- this._loading = true;
+ if (!repo) { return Promise.resolve(); }
- if (!repo) { return Promise.resolve(); }
+ return this._reload(repo);
+ }
- return this._reload(repo);
- }
+ _reload(repo) {
+ const promises = [];
- _reload(repo) {
- const promises = [];
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
- const errFn = response => {
- this.fire('page-error', {response});
- };
+ this._editing = false;
- this._editing = false;
+ // Always reset sections when a project changes.
+ this._sections = [];
+ promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn)
+ .then(res => {
+ if (!res) { return Promise.resolve(); }
- // Always reset sections when a project changes.
- this._sections = [];
- promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
-
- // Keep a copy of the original inherit from values separate from
- // the ones data bound to gr-autocomplete, so the original value
- // can be restored if the user cancels.
- this._inheritsFrom = res.inherits_from ? Object.assign({},
- res.inherits_from) : null;
- this._originalInheritsFrom = res.inherits_from ? Object.assign({},
- res.inherits_from) : null;
- // Initialize the filter value so when the user clicks edit, the
- // current value appears. If there is no parent repo, it is
- // initialized as an empty string.
- this._inheritFromFilter = res.inherits_from ?
- this._inheritsFrom.name : '';
- this._local = res.local;
- this._groups = res.groups;
- this._weblinks = res.config_web_links || [];
- this._canUpload = res.can_upload;
- this._ownerOf = res.owner_of || [];
- return this.toSortedArray(this._local);
- }));
-
- promises.push(this.$.restAPI.getCapabilities(errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
-
- return res;
- }));
-
- promises.push(this.$.restAPI.getRepo(repo, errFn)
- .then(res => {
- if (!res) { return Promise.resolve(); }
-
- return res.labels;
- }));
-
- return Promise.all(promises).then(([sections, capabilities, labels]) => {
- this._capabilities = capabilities;
- this._labels = labels;
- this._sections = sections;
- this._loading = false;
- });
- }
-
- _handleUpdateInheritFrom(e) {
- if (!this._inheritsFrom) {
- this._inheritsFrom = {};
- }
- this._inheritsFrom.id = e.detail.value;
- this._inheritsFrom.name = this._inheritFromFilter;
- this._handleAccessModified();
- }
-
- _getInheritFromSuggestions() {
- return this.$.restAPI.getRepos(
- this._inheritFromFilter,
- MAX_AUTOCOMPLETE_RESULTS)
- .then(response => {
- const projects = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- projects.push({
- name: response[key].name,
- value: response[key].id,
- });
- }
- return projects;
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _handleEdit() {
- this._editing = !this._editing;
- }
-
- _editOrCancel(editing) {
- return editing ? 'Cancel' : 'Edit';
- }
-
- _computeWebLinkClass(weblinks) {
- return weblinks && weblinks.length ? 'show' : '';
- }
-
- _computeShowInherit(inheritsFrom) {
- return inheritsFrom ? 'show' : '';
- }
-
- _handleAddedSectionRemoved(e) {
- const index = e.model.index;
- this._sections = this._sections.slice(0, index)
- .concat(this._sections.slice(index + 1, this._sections.length));
- }
-
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld || editing) { return; }
- // Remove any unsaved but added refs.
- if (this._sections) {
- this._sections = this._sections.filter(p => !p.value.added);
- }
- // Restore inheritFrom.
- if (this._inheritsFrom) {
- this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
- this._inheritFromFilter = this._inheritsFrom.name;
- }
- for (const key of Object.keys(this._local)) {
- if (this._local[key].added) {
- delete this._local[key];
- }
- }
- }
-
- /**
- * @param {!Defs.projectAccessInput} addRemoveObj
- * @param {!Array} path
- * @param {string} type add or remove
- * @param {!Object=} opt_value value to add if the type is 'add'
- * @return {!Defs.projectAccessInput}
- */
- _updateAddRemoveObj(addRemoveObj, path, type, opt_value) {
- let curPos = addRemoveObj[type];
- for (const item of path) {
- if (!curPos[item]) {
- if (item === path[path.length - 1] && type === 'remove') {
- if (path[path.length - 2] === 'permissions') {
- curPos[item] = {rules: {}};
- } else if (path.length === 1) {
- curPos[item] = {permissions: {}};
- } else {
- curPos[item] = {};
- }
- } else if (item === path[path.length - 1] && type === 'add') {
- curPos[item] = opt_value;
- } else {
- curPos[item] = {};
- }
- }
- curPos = curPos[item];
- }
- return addRemoveObj;
- }
-
- /**
- * Used to recursively remove any objects with a 'deleted' bit.
- */
- _recursivelyRemoveDeleted(obj) {
- for (const k in obj) {
- if (!obj.hasOwnProperty(k)) { continue; }
-
- if (typeof obj[k] == 'object') {
- if (obj[k].deleted) {
- delete obj[k];
- return;
- }
- this._recursivelyRemoveDeleted(obj[k]);
- }
- }
- }
-
- _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
- for (const k in obj) {
- if (!obj.hasOwnProperty(k)) { continue; }
- if (typeof obj[k] == 'object') {
- const updatedId = obj[k].updatedId;
- const ref = updatedId ? updatedId : k;
- if (obj[k].deleted) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(k), 'remove');
- continue;
- } else if (obj[k].modified) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(k), 'remove');
- this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
- obj[k]);
- /* Special case for ref changes because they need to be added and
- removed in a different way. The new ref needs to include all
- changes but also the initial state. To do this, instead of
- continuing with the same recursion, just remove anything that is
- deleted in the current state. */
- if (updatedId && updatedId !== k) {
- this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
- }
- continue;
- } else if (obj[k].added) {
- this._updateAddRemoveObj(addRemoveObj,
- path.concat(ref), 'add', obj[k]);
- /**
- * As add / delete both can happen in the new section,
- * so here to make sure it will remove the deleted ones.
- *
- * @see Issue 11339
- */
- this._recursivelyRemoveDeleted(addRemoveObj.add[k]);
- continue;
- }
- this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
- path.concat(k));
- }
- }
- }
-
- /**
- * Returns an object formatted for saving or submitting access changes for
- * review
- *
- * @return {!Defs.projectAccessInput}
- */
- _computeAddAndRemove() {
- const addRemoveObj = {
- add: {},
- remove: {},
- };
-
- const originalInheritsFromId = this._originalInheritsFrom ?
- this.singleDecodeURL(this._originalInheritsFrom.id) :
- null;
- const inheritsFromId = this._inheritsFrom ?
- this.singleDecodeURL(this._inheritsFrom.id) :
- null;
-
- const inheritFromChanged =
- // Inherit from changed
- (originalInheritsFromId &&
- originalInheritsFromId !== inheritsFromId) ||
- // Inherit from added (did not have one initially);
- (!originalInheritsFromId && inheritsFromId);
-
- this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
-
- if (inheritFromChanged) {
- addRemoveObj.parent = inheritsFromId;
- }
- return addRemoveObj;
- }
-
- _handleCreateSection() {
- let newRef = 'refs/for/*';
- // Avoid using an already used key for the placeholder, since it
- // immediately gets added to an object.
- while (this._local[newRef]) {
- newRef = `${newRef}*`;
- }
- const section = {permissions: {}, added: true};
- this.push('_sections', {id: newRef, value: section});
- this.set(['_local', newRef], section);
- Polymer.dom.flush();
- Polymer.dom(this.root).querySelector('gr-access-section:last-of-type')
- .editReference();
- }
-
- _getObjforSave() {
- const addRemoveObj = this._computeAddAndRemove();
- // If there are no changes, don't actually save.
- if (!Object.keys(addRemoveObj.add).length &&
- !Object.keys(addRemoveObj.remove).length &&
- !addRemoveObj.parent) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: NOTHING_TO_SAVE},
- bubbles: true,
- composed: true,
+ // Keep a copy of the original inherit from values separate from
+ // the ones data bound to gr-autocomplete, so the original value
+ // can be restored if the user cancels.
+ this._inheritsFrom = res.inherits_from ? Object.assign({},
+ res.inherits_from) : null;
+ this._originalInheritsFrom = res.inherits_from ? Object.assign({},
+ res.inherits_from) : null;
+ // Initialize the filter value so when the user clicks edit, the
+ // current value appears. If there is no parent repo, it is
+ // initialized as an empty string.
+ this._inheritFromFilter = res.inherits_from ?
+ this._inheritsFrom.name : '';
+ this._local = res.local;
+ this._groups = res.groups;
+ this._weblinks = res.config_web_links || [];
+ this._canUpload = res.can_upload;
+ this._ownerOf = res.owner_of || [];
+ return this.toSortedArray(this._local);
}));
- return;
- }
- const obj = {
- add: addRemoveObj.add,
- remove: addRemoveObj.remove,
- };
- if (addRemoveObj.parent) {
- obj.parent = addRemoveObj.parent;
- }
- return obj;
- }
- _handleSave(e) {
- const obj = this._getObjforSave();
- if (!obj) { return; }
- const button = e && e.target;
- if (button) {
- button.loading = true;
+ promises.push(this.$.restAPI.getCapabilities(errFn)
+ .then(res => {
+ if (!res) { return Promise.resolve(); }
+
+ return res;
+ }));
+
+ promises.push(this.$.restAPI.getRepo(repo, errFn)
+ .then(res => {
+ if (!res) { return Promise.resolve(); }
+
+ return res.labels;
+ }));
+
+ return Promise.all(promises).then(([sections, capabilities, labels]) => {
+ this._capabilities = capabilities;
+ this._labels = labels;
+ this._sections = sections;
+ this._loading = false;
+ });
+ }
+
+ _handleUpdateInheritFrom(e) {
+ if (!this._inheritsFrom) {
+ this._inheritsFrom = {};
+ }
+ this._inheritsFrom.id = e.detail.value;
+ this._inheritsFrom.name = this._inheritFromFilter;
+ this._handleAccessModified();
+ }
+
+ _getInheritFromSuggestions() {
+ return this.$.restAPI.getRepos(
+ this._inheritFromFilter,
+ MAX_AUTOCOMPLETE_RESULTS)
+ .then(response => {
+ const projects = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ projects.push({
+ name: response[key].name,
+ value: response[key].id,
+ });
+ }
+ return projects;
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _handleEdit() {
+ this._editing = !this._editing;
+ }
+
+ _editOrCancel(editing) {
+ return editing ? 'Cancel' : 'Edit';
+ }
+
+ _computeWebLinkClass(weblinks) {
+ return weblinks && weblinks.length ? 'show' : '';
+ }
+
+ _computeShowInherit(inheritsFrom) {
+ return inheritsFrom ? 'show' : '';
+ }
+
+ _handleAddedSectionRemoved(e) {
+ const index = e.model.index;
+ this._sections = this._sections.slice(0, index)
+ .concat(this._sections.slice(index + 1, this._sections.length));
+ }
+
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld || editing) { return; }
+ // Remove any unsaved but added refs.
+ if (this._sections) {
+ this._sections = this._sections.filter(p => !p.value.added);
+ }
+ // Restore inheritFrom.
+ if (this._inheritsFrom) {
+ this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
+ this._inheritFromFilter = this._inheritsFrom.name;
+ }
+ for (const key of Object.keys(this._local)) {
+ if (this._local[key].added) {
+ delete this._local[key];
}
- return this.$.restAPI.setRepoAccessRights(this.repo, obj)
- .then(() => {
- this._reload(this.repo);
- })
- .finally(() => {
- this._modified = false;
- if (button) {
- button.loading = false;
- }
- });
- }
-
- _handleSaveForReview(e) {
- const obj = this._getObjforSave();
- if (!obj) { return; }
- const button = e && e.target;
- if (button) {
- button.loading = true;
- }
- return this.$.restAPI
- .setRepoAccessRightsForReview(this.repo, obj)
- .then(change => {
- Gerrit.Nav.navigateToChange(change);
- })
- .finally(() => {
- this._modified = false;
- if (button) {
- button.loading = false;
- }
- });
- }
-
- _computeSaveReviewBtnClass(canUpload) {
- return !canUpload ? 'invisible' : '';
- }
-
- _computeSaveBtnClass(ownerOf) {
- return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
- }
-
- _computeMainClass(ownerOf, canUpload, editing) {
- const classList = [];
- if (ownerOf && ownerOf.length > 0 || canUpload) {
- classList.push('admin');
- }
- if (editing) {
- classList.push('editing');
- }
- return classList.join(' ');
- }
-
- _computeParentHref(repoName) {
- return this.getBaseUrl() +
- `/admin/repos/${this.encodeURL(repoName, true)},access`;
}
}
- customElements.define(GrRepoAccess.is, GrRepoAccess);
-})();
+ /**
+ * @param {!Defs.projectAccessInput} addRemoveObj
+ * @param {!Array} path
+ * @param {string} type add or remove
+ * @param {!Object=} opt_value value to add if the type is 'add'
+ * @return {!Defs.projectAccessInput}
+ */
+ _updateAddRemoveObj(addRemoveObj, path, type, opt_value) {
+ let curPos = addRemoveObj[type];
+ for (const item of path) {
+ if (!curPos[item]) {
+ if (item === path[path.length - 1] && type === 'remove') {
+ if (path[path.length - 2] === 'permissions') {
+ curPos[item] = {rules: {}};
+ } else if (path.length === 1) {
+ curPos[item] = {permissions: {}};
+ } else {
+ curPos[item] = {};
+ }
+ } else if (item === path[path.length - 1] && type === 'add') {
+ curPos[item] = opt_value;
+ } else {
+ curPos[item] = {};
+ }
+ }
+ curPos = curPos[item];
+ }
+ return addRemoveObj;
+ }
+
+ /**
+ * Used to recursively remove any objects with a 'deleted' bit.
+ */
+ _recursivelyRemoveDeleted(obj) {
+ for (const k in obj) {
+ if (!obj.hasOwnProperty(k)) { continue; }
+
+ if (typeof obj[k] == 'object') {
+ if (obj[k].deleted) {
+ delete obj[k];
+ return;
+ }
+ this._recursivelyRemoveDeleted(obj[k]);
+ }
+ }
+ }
+
+ _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
+ for (const k in obj) {
+ if (!obj.hasOwnProperty(k)) { continue; }
+ if (typeof obj[k] == 'object') {
+ const updatedId = obj[k].updatedId;
+ const ref = updatedId ? updatedId : k;
+ if (obj[k].deleted) {
+ this._updateAddRemoveObj(addRemoveObj,
+ path.concat(k), 'remove');
+ continue;
+ } else if (obj[k].modified) {
+ this._updateAddRemoveObj(addRemoveObj,
+ path.concat(k), 'remove');
+ this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
+ obj[k]);
+ /* Special case for ref changes because they need to be added and
+ removed in a different way. The new ref needs to include all
+ changes but also the initial state. To do this, instead of
+ continuing with the same recursion, just remove anything that is
+ deleted in the current state. */
+ if (updatedId && updatedId !== k) {
+ this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
+ }
+ continue;
+ } else if (obj[k].added) {
+ this._updateAddRemoveObj(addRemoveObj,
+ path.concat(ref), 'add', obj[k]);
+ /**
+ * As add / delete both can happen in the new section,
+ * so here to make sure it will remove the deleted ones.
+ *
+ * @see Issue 11339
+ */
+ this._recursivelyRemoveDeleted(addRemoveObj.add[k]);
+ continue;
+ }
+ this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
+ path.concat(k));
+ }
+ }
+ }
+
+ /**
+ * Returns an object formatted for saving or submitting access changes for
+ * review
+ *
+ * @return {!Defs.projectAccessInput}
+ */
+ _computeAddAndRemove() {
+ const addRemoveObj = {
+ add: {},
+ remove: {},
+ };
+
+ const originalInheritsFromId = this._originalInheritsFrom ?
+ this.singleDecodeURL(this._originalInheritsFrom.id) :
+ null;
+ const inheritsFromId = this._inheritsFrom ?
+ this.singleDecodeURL(this._inheritsFrom.id) :
+ null;
+
+ const inheritFromChanged =
+ // Inherit from changed
+ (originalInheritsFromId &&
+ originalInheritsFromId !== inheritsFromId) ||
+ // Inherit from added (did not have one initially);
+ (!originalInheritsFromId && inheritsFromId);
+
+ this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
+
+ if (inheritFromChanged) {
+ addRemoveObj.parent = inheritsFromId;
+ }
+ return addRemoveObj;
+ }
+
+ _handleCreateSection() {
+ let newRef = 'refs/for/*';
+ // Avoid using an already used key for the placeholder, since it
+ // immediately gets added to an object.
+ while (this._local[newRef]) {
+ newRef = `${newRef}*`;
+ }
+ const section = {permissions: {}, added: true};
+ this.push('_sections', {id: newRef, value: section});
+ this.set(['_local', newRef], section);
+ flush();
+ dom(this.root).querySelector('gr-access-section:last-of-type')
+ .editReference();
+ }
+
+ _getObjforSave() {
+ const addRemoveObj = this._computeAddAndRemove();
+ // If there are no changes, don't actually save.
+ if (!Object.keys(addRemoveObj.add).length &&
+ !Object.keys(addRemoveObj.remove).length &&
+ !addRemoveObj.parent) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: NOTHING_TO_SAVE},
+ bubbles: true,
+ composed: true,
+ }));
+ return;
+ }
+ const obj = {
+ add: addRemoveObj.add,
+ remove: addRemoveObj.remove,
+ };
+ if (addRemoveObj.parent) {
+ obj.parent = addRemoveObj.parent;
+ }
+ return obj;
+ }
+
+ _handleSave(e) {
+ const obj = this._getObjforSave();
+ if (!obj) { return; }
+ const button = e && e.target;
+ if (button) {
+ button.loading = true;
+ }
+ return this.$.restAPI.setRepoAccessRights(this.repo, obj)
+ .then(() => {
+ this._reload(this.repo);
+ })
+ .finally(() => {
+ this._modified = false;
+ if (button) {
+ button.loading = false;
+ }
+ });
+ }
+
+ _handleSaveForReview(e) {
+ const obj = this._getObjforSave();
+ if (!obj) { return; }
+ const button = e && e.target;
+ if (button) {
+ button.loading = true;
+ }
+ return this.$.restAPI
+ .setRepoAccessRightsForReview(this.repo, obj)
+ .then(change => {
+ Gerrit.Nav.navigateToChange(change);
+ })
+ .finally(() => {
+ this._modified = false;
+ if (button) {
+ button.loading = false;
+ }
+ });
+ }
+
+ _computeSaveReviewBtnClass(canUpload) {
+ return !canUpload ? 'invisible' : '';
+ }
+
+ _computeSaveBtnClass(ownerOf) {
+ return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
+ }
+
+ _computeMainClass(ownerOf, canUpload, editing) {
+ const classList = [];
+ if (ownerOf && ownerOf.length > 0 || canUpload) {
+ classList.push('admin');
+ }
+ if (editing) {
+ classList.push('editing');
+ }
+ return classList.join(' ');
+ }
+
+ _computeParentHref(repoName) {
+ return this.getBaseUrl() +
+ `/admin/repos/${this.encodeURL(repoName, true)},access`;
+ }
+}
+
+customElements.define(GrRepoAccess.is, GrRepoAccess);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
index 54006b5..9a27371 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
@@ -1,38 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-access-section/gr-access-section.html">
-
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-repo-access">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -73,25 +57,18 @@
<style include="gr-menu-page-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <main class$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
+ <main class\$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]">
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
+ <h3 id="inheritsFrom" class\$="[[_computeShowInherit(_inheritsFrom)]]">
<span class="rightsText">Rights Inherit From</span>
- <a
- href$="[[_computeParentHref(_inheritsFrom.name)]]"
- rel="noopener"
- id="inheritFromName">
+ <a href\$="[[_computeParentHref(_inheritsFrom.name)]]" rel="noopener" id="inheritFromName">
[[_inheritsFrom.name]]</a>
- <gr-autocomplete
- id="editInheritFromInput"
- text="{{_inheritFromFilter}}"
- query="[[_query]]"
- on-commit="_handleUpdateInheritFrom"></gr-autocomplete>
+ <gr-autocomplete id="editInheritFromInput" text="{{_inheritFromFilter}}" query="[[_query]]" on-commit="_handleUpdateInheritFrom"></gr-autocomplete>
</h3>
- <div class$="weblinks [[_computeWebLinkClass(_weblinks)]]">
+ <div class\$="weblinks [[_computeWebLinkClass(_weblinks)]]">
History:
<template is="dom-repeat" items="[[_weblinks]]" as="link">
<a href="[[link.url]]" class="weblink" rel="noopener" target="[[link.target]]">
@@ -99,41 +76,16 @@
</a>
</template>
</div>
- <gr-button id="editBtn"
- on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
- <gr-button id="saveBtn"
- primary
- class$="[[_computeSaveBtnClass(_ownerOf)]]"
- on-click="_handleSave"
- disabled="[[!_modified]]">Save</gr-button>
- <gr-button id="saveReviewBtn"
- primary
- class$="[[_computeSaveReviewBtnClass(_canUpload)]]"
- on-click="_handleSaveForReview"
- disabled="[[!_modified]]">Save for review</gr-button>
- <template
- is="dom-repeat"
- items="{{_sections}}"
- initial-count="5"
- target-framerate="60"
- as="section">
- <gr-access-section
- capabilities="[[_capabilities]]"
- section="{{section}}"
- labels="[[_labels]]"
- can-upload="[[_canUpload]]"
- editing="[[_editing]]"
- owner-of="[[_ownerOf]]"
- groups="[[_groups]]"
- on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section>
+ <gr-button id="editBtn" on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
+ <gr-button id="saveBtn" primary="" class\$="[[_computeSaveBtnClass(_ownerOf)]]" on-click="_handleSave" disabled="[[!_modified]]">Save</gr-button>
+ <gr-button id="saveReviewBtn" primary="" class\$="[[_computeSaveReviewBtnClass(_canUpload)]]" on-click="_handleSaveForReview" disabled="[[!_modified]]">Save for review</gr-button>
+ <template is="dom-repeat" items="{{_sections}}" initial-count="5" target-framerate="60" as="section">
+ <gr-access-section capabilities="[[_capabilities]]" section="{{section}}" labels="[[_labels]]" can-upload="[[_canUpload]]" editing="[[_editing]]" owner-of="[[_ownerOf]]" groups="[[_groups]]" on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section>
</template>
<div class="referenceContainer">
- <gr-button id="addReferenceBtn"
- on-click="_handleCreateSection">Add Reference</gr-button>
+ <gr-button id="addReferenceBtn" on-click="_handleCreateSection">Add Reference</gr-button>
</div>
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-access.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
index d89b5df..4a482db 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-access</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/page/page.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-repo-access.html">
+<script src="/node_modules/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 type="module" src="./gr-repo-access.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-access.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,395 +41,432 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-access tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let repoStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-access.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-repo-access tests', () => {
+ let element;
+ let sandbox;
+ let repoStub;
- const accessRes = {
- local: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY'},
- },
+ const accessRes = {
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY'},
},
- read: {
- rules: {
- 234: {action: 'ALLOW'},
+ },
+ read: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ },
+ groups: {
+ Administrators: {
+ name: 'Administrators',
+ },
+ Maintainers: {
+ name: 'Maintainers',
+ },
+ },
+ config_web_links: [{
+ name: 'gitiles',
+ target: '_blank',
+ url: 'https://my/site/+log/123/project.config',
+ }],
+ can_upload: true,
+ };
+ const accessRes2 = {
+ local: {
+ GLOBAL_CAPABILITIES: {
+ permissions: {
+ accessDatabase: {
+ rules: {
+ group1: {
+ action: 'ALLOW',
},
},
},
},
},
- groups: {
- Administrators: {
- name: 'Administrators',
- },
- Maintainers: {
- name: 'Maintainers',
+ },
+ };
+ const repoRes = {
+ labels: {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
},
},
- config_web_links: [{
- name: 'gitiles',
- target: '_blank',
- url: 'https://my/site/+log/123/project.config',
- }],
- can_upload: true,
- };
- const accessRes2 = {
- local: {
- GLOBAL_CAPABILITIES: {
- permissions: {
- accessDatabase: {
- rules: {
- group1: {
- action: 'ALLOW',
- },
- },
- },
- },
- },
- },
- };
- const repoRes = {
- labels: {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- },
- },
- };
+ },
+ };
+ const capabilitiesRes = {
+ accessDatabase: {
+ id: 'accessDatabase',
+ name: 'Access Database',
+ },
+ createAccount: {
+ id: 'createAccount',
+ name: 'Create Account',
+ },
+ };
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ });
+ repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns(
+ Promise.resolve(repoRes));
+ element._loading = false;
+ element._ownerOf = [];
+ element._canUpload = false;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_repoChanged called when repo name changes', () => {
+ sandbox.stub(element, '_repoChanged');
+ element.repo = 'New Repo';
+ assert.isTrue(element._repoChanged.called);
+ });
+
+ test('_repoChanged', done => {
+ const accessStub = sandbox.stub(element.$.restAPI,
+ 'getRepoAccessRights');
+
+ accessStub.withArgs('New Repo').returns(
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
+ accessStub.withArgs('Another New Repo')
+ .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
+ const capabilitiesStub = sandbox.stub(element.$.restAPI,
+ 'getCapabilities');
+ capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
+
+ element._repoChanged('New Repo').then(() => {
+ assert.isTrue(accessStub.called);
+ assert.isTrue(capabilitiesStub.called);
+ assert.isTrue(repoStub.called);
+ assert.isNotOk(element._inheritsFrom);
+ assert.deepEqual(element._local, accessRes.local);
+ assert.deepEqual(element._sections,
+ element.toSortedArray(accessRes.local));
+ assert.deepEqual(element._labels, repoRes.labels);
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.weblinks')).display,
+ 'block');
+ return element._repoChanged('Another New Repo');
+ })
+ .then(() => {
+ assert.deepEqual(element._sections,
+ element.toSortedArray(accessRes2.local));
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.weblinks')).display,
+ 'none');
+ done();
+ });
+ });
+
+ test('_repoChanged when repo changes to undefined returns', done => {
const capabilitiesRes = {
accessDatabase: {
id: 'accessDatabase',
name: 'Access Database',
},
- createAccount: {
- id: 'createAccount',
- name: 'Create Account',
- },
};
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- });
- repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns(
- Promise.resolve(repoRes));
- element._loading = false;
- element._ownerOf = [];
- element._canUpload = false;
+ const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights')
+ .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
+ const capabilitiesStub = sandbox.stub(element.$.restAPI,
+ 'getCapabilities').returns(Promise.resolve(capabilitiesRes));
+
+ element._repoChanged().then(() => {
+ assert.isFalse(accessStub.called);
+ assert.isFalse(capabilitiesStub.called);
+ assert.isFalse(repoStub.called);
+ done();
+ });
+ });
+
+ test('_computeParentHref', () => {
+ const repoName = 'test-repo';
+ assert.equal(element._computeParentHref(repoName),
+ '/admin/repos/test-repo,access');
+ });
+
+ test('_computeMainClass', () => {
+ let ownerOf = ['refs/*'];
+ const editing = true;
+ const canUpload = false;
+ assert.equal(element._computeMainClass(ownerOf, canUpload), 'admin');
+ assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
+ 'admin editing');
+ ownerOf = [];
+ assert.equal(element._computeMainClass(ownerOf, canUpload), '');
+ assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
+ 'editing');
+ });
+
+ test('inherit section', () => {
+ element._local = {};
+ element._ownerOf = [];
+ sandbox.stub(element, '_computeParentHref');
+ // Nothing should appear when no inherit from and not in edit mode.
+ assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ // The autocomplete should be hidden, and the link should be displayed.
+ assert.isFalse(element._computeParentHref.called);
+ // When it edit mode, the autocomplete should appear.
+ element._editing = true;
+ // When editing, the autocomplete should still not be shown.
+ assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ element._editing = false;
+ element._inheritsFrom = {
+ name: 'another-repo',
+ };
+ // When there is a parent project, the link should be displayed.
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.inheritFromName).display,
+ 'none');
+ assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
+ 'none');
+ assert.isTrue(element._computeParentHref.called);
+ element._editing = true;
+ // When editing, the autocomplete should be shown.
+ assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
+ assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display,
+ 'none');
+ });
+
+ test('_handleUpdateInheritFrom', () => {
+ element._inheritFromFilter = 'foo bar baz';
+ element._handleUpdateInheritFrom({detail: {value: 'abc+123'}});
+ assert.isOk(element._inheritsFrom);
+ assert.equal(element._inheritsFrom.id, 'abc+123');
+ assert.equal(element._inheritsFrom.name, 'foo bar baz');
+ });
+
+ test('_computeLoadingClass', () => {
+ assert.equal(element._computeLoadingClass(true), 'loading');
+ assert.equal(element._computeLoadingClass(false), '');
+ });
+
+ test('fires page-error', done => {
+ const response = {status: 404};
+
+ sandbox.stub(
+ element.$.restAPI, 'getRepoAccessRights', (repoName, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
});
- teardown(() => {
- sandbox.restore();
- });
+ element.repo = 'test';
+ });
- test('_repoChanged called when repo name changes', () => {
- sandbox.stub(element, '_repoChanged');
- element.repo = 'New Repo';
- assert.isTrue(element._repoChanged.called);
- });
-
- test('_repoChanged', done => {
- const accessStub = sandbox.stub(element.$.restAPI,
- 'getRepoAccessRights');
-
- accessStub.withArgs('New Repo').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- accessStub.withArgs('Another New Repo')
- .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
- const capabilitiesStub = sandbox.stub(element.$.restAPI,
- 'getCapabilities');
- capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
-
- element._repoChanged('New Repo').then(() => {
- assert.isTrue(accessStub.called);
- assert.isTrue(capabilitiesStub.called);
- assert.isTrue(repoStub.called);
- assert.isNotOk(element._inheritsFrom);
- assert.deepEqual(element._local, accessRes.local);
- assert.deepEqual(element._sections,
- element.toSortedArray(accessRes.local));
- assert.deepEqual(element._labels, repoRes.labels);
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.weblinks')).display,
- 'block');
- return element._repoChanged('Another New Repo');
- })
- .then(() => {
- assert.deepEqual(element._sections,
- element.toSortedArray(accessRes2.local));
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.weblinks')).display,
- 'none');
- done();
- });
- });
-
- test('_repoChanged when repo changes to undefined returns', done => {
- const capabilitiesRes = {
- accessDatabase: {
- id: 'accessDatabase',
- name: 'Access Database',
- },
- };
- const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights')
- .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
- const capabilitiesStub = sandbox.stub(element.$.restAPI,
- 'getCapabilities').returns(Promise.resolve(capabilitiesRes));
-
- element._repoChanged().then(() => {
- assert.isFalse(accessStub.called);
- assert.isFalse(capabilitiesStub.called);
- assert.isFalse(repoStub.called);
- done();
- });
- });
-
- test('_computeParentHref', () => {
- const repoName = 'test-repo';
- assert.equal(element._computeParentHref(repoName),
- '/admin/repos/test-repo,access');
- });
-
- test('_computeMainClass', () => {
- let ownerOf = ['refs/*'];
- const editing = true;
- const canUpload = false;
- assert.equal(element._computeMainClass(ownerOf, canUpload), 'admin');
- assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
- 'admin editing');
- ownerOf = [];
- assert.equal(element._computeMainClass(ownerOf, canUpload), '');
- assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
- 'editing');
- });
-
- test('inherit section', () => {
- element._local = {};
- element._ownerOf = [];
- sandbox.stub(element, '_computeParentHref');
- // Nothing should appear when no inherit from and not in edit mode.
- assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
- // The autocomplete should be hidden, and the link should be displayed.
- assert.isFalse(element._computeParentHref.called);
- // When it edit mode, the autocomplete should appear.
- element._editing = true;
- // When editing, the autocomplete should still not be shown.
- assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
- element._editing = false;
- element._inheritsFrom = {
- name: 'another-repo',
- };
- // When there is a parent project, the link should be displayed.
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
- assert.notEqual(getComputedStyle(element.$.inheritFromName).display,
- 'none');
+ suite('with defined sections', () => {
+ const testEditSaveCancelBtns = (shouldShowSave, shouldShowSaveReview) => {
+ // Edit button is visible and Save button is hidden.
+ assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
+ assert.equal(element.$.editBtn.innerText, 'EDIT');
assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
'none');
- assert.isTrue(element._computeParentHref.called);
- element._editing = true;
- // When editing, the autocomplete should be shown.
- assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
- assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none');
- assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display,
- 'none');
- });
+ element._inheritsFrom = {
+ id: 'test-project',
+ };
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('#editInheritFromInput'))
+ .display, 'none');
- test('_handleUpdateInheritFrom', () => {
- element._inheritFromFilter = 'foo bar baz';
- element._handleUpdateInheritFrom({detail: {value: 'abc+123'}});
- assert.isOk(element._inheritsFrom);
- assert.equal(element._inheritsFrom.id, 'abc+123');
- assert.equal(element._inheritsFrom.name, 'foo bar baz');
- });
+ MockInteractions.tap(element.$.editBtn);
+ flushAsynchronousOperations();
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('fires page-error', done => {
- const response = {status: 404};
-
- sandbox.stub(
- element.$.restAPI, 'getRepoAccessRights', (repoName, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element.repo = 'test';
- });
-
- suite('with defined sections', () => {
- const testEditSaveCancelBtns = (shouldShowSave, shouldShowSaveReview) => {
- // Edit button is visible and Save button is hidden.
- assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
- assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
- assert.equal(element.$.editBtn.innerText, 'EDIT');
- assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
+ // Edit button changes to Cancel button, and Save button is visible but
+ // disabled.
+ assert.equal(element.$.editBtn.innerText, 'CANCEL');
+ if (shouldShowSaveReview) {
+ assert.notEqual(getComputedStyle(element.$.saveReviewBtn).display,
'none');
- element._inheritsFrom = {
- id: 'test-project',
- };
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#editInheritFromInput'))
- .display, 'none');
+ assert.isTrue(element.$.saveReviewBtn.disabled);
+ }
+ if (shouldShowSave) {
+ assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none');
+ assert.isTrue(element.$.saveBtn.disabled);
+ }
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('#editInheritFromInput'))
+ .display, 'none');
- MockInteractions.tap(element.$.editBtn);
- flushAsynchronousOperations();
+ // Save button should be enabled after access is modified
+ element.fire('access-modified');
+ if (shouldShowSaveReview) {
+ assert.isFalse(element.$.saveReviewBtn.disabled);
+ }
+ if (shouldShowSave) {
+ assert.isFalse(element.$.saveBtn.disabled);
+ }
+ };
- // Edit button changes to Cancel button, and Save button is visible but
- // disabled.
- assert.equal(element.$.editBtn.innerText, 'CANCEL');
- if (shouldShowSaveReview) {
- assert.notEqual(getComputedStyle(element.$.saveReviewBtn).display,
- 'none');
- assert.isTrue(element.$.saveReviewBtn.disabled);
- }
- if (shouldShowSave) {
- assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none');
- assert.isTrue(element.$.saveBtn.disabled);
- }
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('#editInheritFromInput'))
- .display, 'none');
+ setup(() => {
+ // Create deep copies of these objects so the originals are not modified
+ // by any tests.
+ element._local = JSON.parse(JSON.stringify(accessRes.local));
+ element._ownerOf = [];
+ element._sections = element.toSortedArray(element._local);
+ element._groups = JSON.parse(JSON.stringify(accessRes.groups));
+ element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
+ element._labels = JSON.parse(JSON.stringify(repoRes.labels));
+ flushAsynchronousOperations();
+ });
- // Save button should be enabled after access is modified
- element.fire('access-modified');
- if (shouldShowSaveReview) {
- assert.isFalse(element.$.saveReviewBtn.disabled);
- }
- if (shouldShowSave) {
- assert.isFalse(element.$.saveBtn.disabled);
- }
+ test('removing an added section', () => {
+ element.editing = true;
+ assert.equal(element._sections.length, 1);
+ element.shadowRoot
+ .querySelector('gr-access-section').fire('added-section-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._sections.length, 0);
+ });
+
+ test('button visibility for non ref owner', () => {
+ assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
+ });
+
+ test('button visibility for non ref owner with upload privilege', () => {
+ element._canUpload = true;
+ testEditSaveCancelBtns(false, true);
+ });
+
+ test('button visibility for ref owner', () => {
+ element._ownerOf = ['refs/for/*'];
+ testEditSaveCancelBtns(true, false);
+ });
+
+ test('button visibility for ref owner and upload', () => {
+ element._ownerOf = ['refs/for/*'];
+ element._canUpload = true;
+ testEditSaveCancelBtns(true, false);
+ });
+
+ test('_handleAccessModified called with event fired', () => {
+ sandbox.spy(element, '_handleAccessModified');
+ element.fire('access-modified');
+ assert.isTrue(element._handleAccessModified.called);
+ });
+
+ test('_handleAccessModified called when parent changes', () => {
+ element._inheritsFrom = {
+ id: 'test-project',
+ };
+ flushAsynchronousOperations();
+ element.shadowRoot.querySelector('#editInheritFromInput').fire('commit');
+ sandbox.spy(element, '_handleAccessModified');
+ element.fire('access-modified');
+ assert.isTrue(element._handleAccessModified.called);
+ });
+
+ test('_handleSaveForReview', () => {
+ const saveStub =
+ sandbox.stub(element.$.restAPI, 'setRepoAccessRightsForReview');
+ sandbox.stub(element, '_computeAddAndRemove').returns({
+ add: {},
+ remove: {},
+ });
+ element._handleSaveForReview();
+ assert.isFalse(saveStub.called);
+ });
+
+ test('_recursivelyRemoveDeleted', () => {
+ const obj = {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY', deleted: true},
+ },
+ },
+ read: {
+ deleted: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ };
+ const expectedResult = {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ };
+ element._recursivelyRemoveDeleted(obj);
+ assert.deepEqual(obj, expectedResult);
+ });
+
+ test('_recursivelyUpdateAddRemoveObj on new added section', () => {
+ const obj = {
+ 'refs/for/*': {
+ permissions: {
+ 'label-Code-Review': {
+ rules: {
+ e798fed07afbc9173a587f876ef8760c78d240c1: {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ added: true,
+ label: 'Code-Review',
+ },
+ 'labelAs-Code-Review': {
+ rules: {
+ 'ldap:gerritcodereview-eng': {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ deleted: true,
+ },
+ },
+ added: true,
+ label: 'Code-Review',
+ },
+ },
+ added: true,
+ },
};
- setup(() => {
- // Create deep copies of these objects so the originals are not modified
- // by any tests.
- element._local = JSON.parse(JSON.stringify(accessRes.local));
- element._ownerOf = [];
- element._sections = element.toSortedArray(element._local);
- element._groups = JSON.parse(JSON.stringify(accessRes.groups));
- element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
- element._labels = JSON.parse(JSON.stringify(repoRes.labels));
- flushAsynchronousOperations();
- });
-
- test('removing an added section', () => {
- element.editing = true;
- assert.equal(element._sections.length, 1);
- element.shadowRoot
- .querySelector('gr-access-section').fire('added-section-removed');
- flushAsynchronousOperations();
- assert.equal(element._sections.length, 0);
- });
-
- test('button visibility for non ref owner', () => {
- assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
- });
-
- test('button visibility for non ref owner with upload privilege', () => {
- element._canUpload = true;
- testEditSaveCancelBtns(false, true);
- });
-
- test('button visibility for ref owner', () => {
- element._ownerOf = ['refs/for/*'];
- testEditSaveCancelBtns(true, false);
- });
-
- test('button visibility for ref owner and upload', () => {
- element._ownerOf = ['refs/for/*'];
- element._canUpload = true;
- testEditSaveCancelBtns(true, false);
- });
-
- test('_handleAccessModified called with event fired', () => {
- sandbox.spy(element, '_handleAccessModified');
- element.fire('access-modified');
- assert.isTrue(element._handleAccessModified.called);
- });
-
- test('_handleAccessModified called when parent changes', () => {
- element._inheritsFrom = {
- id: 'test-project',
- };
- flushAsynchronousOperations();
- element.shadowRoot.querySelector('#editInheritFromInput').fire('commit');
- sandbox.spy(element, '_handleAccessModified');
- element.fire('access-modified');
- assert.isTrue(element._handleAccessModified.called);
- });
-
- test('_handleSaveForReview', () => {
- const saveStub =
- sandbox.stub(element.$.restAPI, 'setRepoAccessRightsForReview');
- sandbox.stub(element, '_computeAddAndRemove').returns({
- add: {},
- remove: {},
- });
- element._handleSaveForReview();
- assert.isFalse(saveStub.called);
- });
-
- test('_recursivelyRemoveDeleted', () => {
- const obj = {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY', deleted: true},
- },
- },
- read: {
- deleted: true,
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- };
- const expectedResult = {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- };
- element._recursivelyRemoveDeleted(obj);
- assert.deepEqual(obj, expectedResult);
- });
-
- test('_recursivelyUpdateAddRemoveObj on new added section', () => {
- const obj = {
+ const expectedResult = {
+ add: {
'refs/for/*': {
permissions: {
'label-Code-Review': {
@@ -440,798 +482,764 @@
label: 'Code-Review',
},
'labelAs-Code-Review': {
- rules: {
- 'ldap:gerritcodereview-eng': {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- deleted: true,
- },
- },
+ rules: {},
added: true,
label: 'Code-Review',
},
},
added: true,
},
- };
+ },
+ remove: {},
+ };
+ const updateObj = {add: {}, remove: {}};
+ element._recursivelyUpdateAddRemoveObj(obj, updateObj);
+ assert.deepEqual(updateObj, expectedResult);
+ });
- const expectedResult = {
- add: {
- 'refs/for/*': {
- permissions: {
- 'label-Code-Review': {
- rules: {
- e798fed07afbc9173a587f876ef8760c78d240c1: {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- },
- },
- added: true,
- label: 'Code-Review',
- },
- 'labelAs-Code-Review': {
- rules: {},
- added: true,
- label: 'Code-Review',
- },
- },
- added: true,
- },
- },
- remove: {},
- };
- const updateObj = {add: {}, remove: {}};
- element._recursivelyUpdateAddRemoveObj(obj, updateObj);
- assert.deepEqual(updateObj, expectedResult);
+ test('_handleSaveForReview with no changes', () => {
+ assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
+ });
+
+ test('_handleSaveForReview parent change', () => {
+ element._inheritsFrom = {
+ id: 'test-project',
+ };
+ element._originalInheritsFrom = {
+ id: 'test-project-original',
+ };
+ assert.deepEqual(element._computeAddAndRemove(), {
+ parent: 'test-project', add: {}, remove: {},
});
+ });
- test('_handleSaveForReview with no changes', () => {
- assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
+ test('_handleSaveForReview new parent with spaces', () => {
+ element._inheritsFrom = {id: 'spaces+in+project+name'};
+ element._originalInheritsFrom = {id: 'old-project'};
+ assert.deepEqual(element._computeAddAndRemove(), {
+ parent: 'spaces in project name', add: {}, remove: {},
});
+ });
- test('_handleSaveForReview parent change', () => {
- element._inheritsFrom = {
- id: 'test-project',
- };
- element._originalInheritsFrom = {
- id: 'test-project-original',
- };
- assert.deepEqual(element._computeAddAndRemove(), {
- parent: 'test-project', add: {}, remove: {},
- });
+ test('_handleSaveForReview rules', () => {
+ // Delete a rule.
+ element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+ let expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
+ },
+ },
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Undo deleting a rule.
+ delete element._local['refs/*'].permissions.owner.rules[123].deleted;
+
+ // Modify a rule.
+ element._local['refs/*'].permissions.owner.rules[123].modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {action: 'DENY', modified: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
+ },
+ },
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove permissions', () => {
+ // Add a new rule to a permission.
+ let expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+
+ element.shadowRoot
+ .querySelector('gr-access-section').shadowRoot
+ .querySelector('gr-permission')
+ ._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Remove the added rule.
+ delete element._local['refs/*'].permissions.owner.rules.Maintainers;
+
+ // Delete a permission.
+ element._local['refs/*'].permissions.owner.deleted = true;
+ expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Undo delete permission.
+ delete element._local['refs/*'].permissions.owner.deleted;
+
+ // Modify a permission.
+ element._local['refs/*'].permissions.owner.modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY'},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove sections', () => {
+ // Add a new permission to a section
+ let expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {},
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ element.shadowRoot
+ .querySelector('gr-access-section')._handleAddPermission();
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a new rule to the new permission.
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ const newPermission =
+ dom(element.shadowRoot
+ .querySelector('gr-access-section').root).querySelectorAll(
+ 'gr-permission')[2];
+ newPermission._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Modify a section reference.
+ element._local['refs/*'].updatedId = 'refs/for/bar';
+ element._local['refs/*'].modified = true;
+ expectedInput = {
+ add: {
+ 'refs/for/bar': {
+ modified: true,
+ updatedId: 'refs/for/bar',
+ permissions: {
+ 'owner': {
+ rules: {
+ 234: {action: 'ALLOW'},
+ 123: {action: 'DENY'},
+ },
+ },
+ 'read': {
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ min: -2,
+ max: 2,
+ action: 'ALLOW',
+ added: true,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Delete a section.
+ element._local['refs/*'].deleted = true;
+ expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove new section', () => {
+ // Add a new permission to a section
+ let expectedInput = {
+ add: {
+ 'refs/for/*': {
+ added: true,
+ permissions: {},
+ },
+ },
+ remove: {},
+ };
+ MockInteractions.tap(element.$.addReferenceBtn);
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ expectedInput = {
+ add: {
+ 'refs/for/*': {
+ added: true,
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {},
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ const newSection = dom(element.root)
+ .querySelectorAll('gr-access-section')[1];
+ newSection._handleAddPermission();
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add rule to the new permission.
+ expectedInput = {
+ add: {
+ 'refs/for/*': {
+ added: true,
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+
+ newSection.shadowRoot
+ .querySelector('gr-permission')._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+
+ flushAsynchronousOperations();
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Modify a the reference from the default value.
+ element._local['refs/for/*'].updatedId = 'refs/for/new';
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {},
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('_computeAddAndRemove combinations', () => {
+ // Modify rule and delete permission that it is inside of.
+ element._local['refs/*'].permissions.owner.rules[123].modified = true;
+ element._local['refs/*'].permissions.owner.deleted = true;
+ let expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ // Delete rule and delete permission that it is inside of.
+ element._local['refs/*'].permissions.owner.rules[123].modified = false;
+ element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Also modify a different rule inside of another permission.
+ element._local['refs/*'].permissions.read.modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ read: {
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ read: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ // Modify both permissions with an exclusive bit. Owner is still
+ // deleted.
+ element._local['refs/*'].permissions.owner.exclusive = true;
+ element._local['refs/*'].permissions.owner.modified = true;
+ element._local['refs/*'].permissions.read.exclusive = true;
+ element._local['refs/*'].permissions.read.modified = true;
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ read: {
+ exclusive: true,
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ read: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a rule to the existing permission;
+ const readPermission =
+ dom(element.shadowRoot
+ .querySelector('gr-access-section').root).querySelectorAll(
+ 'gr-permission')[1];
+ readPermission._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+
+ expectedInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ read: {
+ exclusive: true,
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ Maintainers: {action: 'ALLOW', added: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {}},
+ read: {rules: {}},
+ },
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Change one of the refs
+ element._local['refs/*'].updatedId = 'refs/for/bar';
+ element._local['refs/*'].modified = true;
+
+ expectedInput = {
+ add: {
+ 'refs/for/bar': {
+ modified: true,
+ updatedId: 'refs/for/bar',
+ permissions: {
+ read: {
+ exclusive: true,
+ modified: true,
+ rules: {
+ 234: {action: 'ALLOW'},
+ Maintainers: {action: 'ALLOW', added: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ expectedInput = {
+ add: {},
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ element._local['refs/*'].deleted = true;
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a new section.
+ MockInteractions.tap(element.$.addReferenceBtn);
+ let newSection = dom(element.root)
+ .querySelectorAll('gr-access-section')[1];
+ newSection._handleAddPermission();
+ flushAsynchronousOperations();
+ newSection.shadowRoot
+ .querySelector('gr-permission')._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+ // Modify a the reference from the default value.
+ element._local['refs/for/*'].updatedId = 'refs/for/new';
+
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Modify newly added rule inside new ref.
+ element._local['refs/for/*'].permissions['label-Code-Review'].
+ rules['Maintainers'].modified = true;
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ modified: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+
+ // Add a second new section.
+ MockInteractions.tap(element.$.addReferenceBtn);
+ newSection = dom(element.root)
+ .querySelectorAll('gr-access-section')[2];
+ newSection._handleAddPermission();
+ flushAsynchronousOperations();
+ newSection.shadowRoot
+ .querySelector('gr-permission')._handleAddRuleItem(
+ {detail: {value: {id: 'Maintainers'}}});
+ // Modify a the reference from the default value.
+ element._local['refs/for/**'].updatedId = 'refs/for/new2';
+ expectedInput = {
+ add: {
+ 'refs/for/new': {
+ added: true,
+ updatedId: 'refs/for/new',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ modified: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ 'refs/for/new2': {
+ added: true,
+ updatedId: 'refs/for/new2',
+ permissions: {
+ 'label-Code-Review': {
+ added: true,
+ rules: {
+ Maintainers: {
+ action: 'ALLOW',
+ added: true,
+ max: 2,
+ min: -2,
+ },
+ },
+ label: 'Code-Review',
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {},
+ },
+ },
+ };
+ assert.deepEqual(element._computeAddAndRemove(), expectedInput);
+ });
+
+ test('Unsaved added refs are discarded when edit cancelled', () => {
+ // Unsaved changes are discarded when editing is cancelled.
+ MockInteractions.tap(element.$.editBtn);
+ assert.equal(element._sections.length, 1);
+ assert.equal(Object.keys(element._local).length, 1);
+ MockInteractions.tap(element.$.addReferenceBtn);
+ assert.equal(element._sections.length, 2);
+ assert.equal(Object.keys(element._local).length, 2);
+ MockInteractions.tap(element.$.editBtn);
+ assert.equal(element._sections.length, 1);
+ assert.equal(Object.keys(element._local).length, 1);
+ });
+
+ test('_handleSave', done => {
+ const repoAccessInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {action: 'DENY', modified: true},
+ },
+ },
+ },
+ },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
+ },
+ },
+ },
+ },
+ },
+ };
+ sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ let resolver;
+ const saveStub = sandbox.stub(element.$.restAPI,
+ 'setRepoAccessRights')
+ .returns(new Promise(r => resolver = r));
+
+ element.repo = 'test-repo';
+ sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
+
+ element._modified = true;
+ MockInteractions.tap(element.$.saveBtn);
+ assert.equal(element.$.saveBtn.hasAttribute('loading'), true);
+ resolver({_number: 1});
+ flush(() => {
+ assert.isTrue(saveStub.called);
+ assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
+ done();
});
+ });
- test('_handleSaveForReview new parent with spaces', () => {
- element._inheritsFrom = {id: 'spaces+in+project+name'};
- element._originalInheritsFrom = {id: 'old-project'};
- assert.deepEqual(element._computeAddAndRemove(), {
- parent: 'spaces in project name', add: {}, remove: {},
- });
- });
-
- test('_handleSaveForReview rules', () => {
- // Delete a rule.
- element._local['refs/*'].permissions.owner.rules[123].deleted = true;
- let expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
+ test('_handleSaveForReview', done => {
+ const repoAccessInput = {
+ add: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {action: 'DENY', modified: true},
},
},
},
},
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Undo deleting a rule.
- delete element._local['refs/*'].permissions.owner.rules[123].deleted;
-
- // Modify a rule.
- element._local['refs/*'].permissions.owner.rules[123].modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {action: 'DENY', modified: true},
- },
+ },
+ remove: {
+ 'refs/*': {
+ permissions: {
+ owner: {
+ rules: {
+ 123: {},
},
},
},
},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
- },
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
+ },
+ };
+ sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ let resolver;
+ const saveForReviewStub = sandbox.stub(element.$.restAPI,
+ 'setRepoAccessRightsForReview')
+ .returns(new Promise(r => resolver = r));
- test('_computeAddAndRemove permissions', () => {
- // Add a new rule to a permission.
- let expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- },
- },
- },
- },
- },
- },
- remove: {},
- };
+ element.repo = 'test-repo';
+ sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
- element.shadowRoot
- .querySelector('gr-access-section').shadowRoot
- .querySelector('gr-permission')
- ._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
-
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Remove the added rule.
- delete element._local['refs/*'].permissions.owner.rules.Maintainers;
-
- // Delete a permission.
- element._local['refs/*'].permissions.owner.deleted = true;
- expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Undo delete permission.
- delete element._local['refs/*'].permissions.owner.deleted;
-
- // Modify a permission.
- element._local['refs/*'].permissions.owner.modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY'},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('_computeAddAndRemove sections', () => {
- // Add a new permission to a section
- let expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {},
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- element.shadowRoot
- .querySelector('gr-access-section')._handleAddPermission();
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a new rule to the new permission.
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- const newPermission =
- Polymer.dom(element.shadowRoot
- .querySelector('gr-access-section').root).querySelectorAll(
- 'gr-permission')[2];
- newPermission._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Modify a section reference.
- element._local['refs/*'].updatedId = 'refs/for/bar';
- element._local['refs/*'].modified = true;
- expectedInput = {
- add: {
- 'refs/for/bar': {
- modified: true,
- updatedId: 'refs/for/bar',
- permissions: {
- 'owner': {
- rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY'},
- },
- },
- 'read': {
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- min: -2,
- max: 2,
- action: 'ALLOW',
- added: true,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Delete a section.
- element._local['refs/*'].deleted = true;
- expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('_computeAddAndRemove new section', () => {
- // Add a new permission to a section
- let expectedInput = {
- add: {
- 'refs/for/*': {
- added: true,
- permissions: {},
- },
- },
- remove: {},
- };
- MockInteractions.tap(element.$.addReferenceBtn);
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- expectedInput = {
- add: {
- 'refs/for/*': {
- added: true,
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {},
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- const newSection = Polymer.dom(element.root)
- .querySelectorAll('gr-access-section')[1];
- newSection._handleAddPermission();
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add rule to the new permission.
- expectedInput = {
- add: {
- 'refs/for/*': {
- added: true,
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
-
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
-
- flushAsynchronousOperations();
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Modify a the reference from the default value.
- element._local['refs/for/*'].updatedId = 'refs/for/new';
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {},
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('_computeAddAndRemove combinations', () => {
- // Modify rule and delete permission that it is inside of.
- element._local['refs/*'].permissions.owner.rules[123].modified = true;
- element._local['refs/*'].permissions.owner.deleted = true;
- let expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- // Delete rule and delete permission that it is inside of.
- element._local['refs/*'].permissions.owner.rules[123].modified = false;
- element._local['refs/*'].permissions.owner.rules[123].deleted = true;
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Also modify a different rule inside of another permission.
- element._local['refs/*'].permissions.read.modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- read: {
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- read: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- // Modify both permissions with an exclusive bit. Owner is still
- // deleted.
- element._local['refs/*'].permissions.owner.exclusive = true;
- element._local['refs/*'].permissions.owner.modified = true;
- element._local['refs/*'].permissions.read.exclusive = true;
- element._local['refs/*'].permissions.read.modified = true;
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- read: {
- exclusive: true,
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- read: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a rule to the existing permission;
- const readPermission =
- Polymer.dom(element.shadowRoot
- .querySelector('gr-access-section').root).querySelectorAll(
- 'gr-permission')[1];
- readPermission._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
-
- expectedInput = {
- add: {
- 'refs/*': {
- permissions: {
- read: {
- exclusive: true,
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- Maintainers: {action: 'ALLOW', added: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {rules: {}},
- read: {rules: {}},
- },
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Change one of the refs
- element._local['refs/*'].updatedId = 'refs/for/bar';
- element._local['refs/*'].modified = true;
-
- expectedInput = {
- add: {
- 'refs/for/bar': {
- modified: true,
- updatedId: 'refs/for/bar',
- permissions: {
- read: {
- exclusive: true,
- modified: true,
- rules: {
- 234: {action: 'ALLOW'},
- Maintainers: {action: 'ALLOW', added: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- expectedInput = {
- add: {},
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- element._local['refs/*'].deleted = true;
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a new section.
- MockInteractions.tap(element.$.addReferenceBtn);
- let newSection = Polymer.dom(element.root)
- .querySelectorAll('gr-access-section')[1];
- newSection._handleAddPermission();
- flushAsynchronousOperations();
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
- // Modify a the reference from the default value.
- element._local['refs/for/*'].updatedId = 'refs/for/new';
-
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Modify newly added rule inside new ref.
- element._local['refs/for/*'].permissions['label-Code-Review'].
- rules['Maintainers'].modified = true;
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- modified: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
-
- // Add a second new section.
- MockInteractions.tap(element.$.addReferenceBtn);
- newSection = Polymer.dom(element.root)
- .querySelectorAll('gr-access-section')[2];
- newSection._handleAddPermission();
- flushAsynchronousOperations();
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: {id: 'Maintainers'}}});
- // Modify a the reference from the default value.
- element._local['refs/for/**'].updatedId = 'refs/for/new2';
- expectedInput = {
- add: {
- 'refs/for/new': {
- added: true,
- updatedId: 'refs/for/new',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- modified: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- 'refs/for/new2': {
- added: true,
- updatedId: 'refs/for/new2',
- permissions: {
- 'label-Code-Review': {
- added: true,
- rules: {
- Maintainers: {
- action: 'ALLOW',
- added: true,
- max: 2,
- min: -2,
- },
- },
- label: 'Code-Review',
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {},
- },
- },
- };
- assert.deepEqual(element._computeAddAndRemove(), expectedInput);
- });
-
- test('Unsaved added refs are discarded when edit cancelled', () => {
- // Unsaved changes are discarded when editing is cancelled.
- MockInteractions.tap(element.$.editBtn);
- assert.equal(element._sections.length, 1);
- assert.equal(Object.keys(element._local).length, 1);
- MockInteractions.tap(element.$.addReferenceBtn);
- assert.equal(element._sections.length, 2);
- assert.equal(Object.keys(element._local).length, 2);
- MockInteractions.tap(element.$.editBtn);
- assert.equal(element._sections.length, 1);
- assert.equal(Object.keys(element._local).length, 1);
- });
-
- test('_handleSave', done => {
- const repoAccessInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {action: 'DENY', modified: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
- },
- },
- },
- },
- };
- sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- let resolver;
- const saveStub = sandbox.stub(element.$.restAPI,
- 'setRepoAccessRights')
- .returns(new Promise(r => resolver = r));
-
- element.repo = 'test-repo';
- sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
-
- element._modified = true;
- MockInteractions.tap(element.$.saveBtn);
- assert.equal(element.$.saveBtn.hasAttribute('loading'), true);
- resolver({_number: 1});
- flush(() => {
- assert.isTrue(saveStub.called);
- assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
- done();
- });
- });
-
- test('_handleSaveForReview', done => {
- const repoAccessInput = {
- add: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {action: 'DENY', modified: true},
- },
- },
- },
- },
- },
- remove: {
- 'refs/*': {
- permissions: {
- owner: {
- rules: {
- 123: {},
- },
- },
- },
- },
- },
- };
- sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- let resolver;
- const saveForReviewStub = sandbox.stub(element.$.restAPI,
- 'setRepoAccessRightsForReview')
- .returns(new Promise(r => resolver = r));
-
- element.repo = 'test-repo';
- sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
-
- element._modified = true;
- MockInteractions.tap(element.$.saveReviewBtn);
- assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true);
- resolver({_number: 1});
- flush(() => {
- assert.isTrue(saveForReviewStub.called);
- assert.isTrue(Gerrit.Nav.navigateToChange
- .lastCall.calledWithExactly({_number: 1}));
- done();
- });
+ element._modified = true;
+ MockInteractions.tap(element.$.saveReviewBtn);
+ assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true);
+ resolver({_number: 1});
+ flush(() => {
+ assert.isTrue(saveForReviewStub.called);
+ assert.isTrue(Gerrit.Nav.navigateToChange
+ .lastCall.calledWithExactly({_number: 1}));
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
index 622bfe4..53b4989 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -14,34 +14,41 @@
* 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 GrRepoCommand extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-repo-command'; }
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.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-repo-command_html.js';
- static get properties() {
- return {
- title: String,
- disabled: Boolean,
- tooltip: String,
- };
- }
+/** @extends Polymer.Element */
+class GrRepoCommand extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * Fired when command button is tapped.
- *
- * @event command-tap
- */
+ static get is() { return 'gr-repo-command'; }
- _onCommandTap() {
- this.dispatchEvent(
- new CustomEvent('command-tap', {bubbles: true, composed: true}));
- }
+ static get properties() {
+ return {
+ title: String,
+ disabled: Boolean,
+ tooltip: String,
+ };
}
- customElements.define(GrRepoCommand.is, GrRepoCommand);
-})();
+ /**
+ * Fired when command button is tapped.
+ *
+ * @event command-tap
+ */
+
+ _onCommandTap() {
+ this.dispatchEvent(
+ new CustomEvent('command-tap', {bubbles: true, composed: true}));
+ }
+}
+
+customElements.define(GrRepoCommand.is, GrRepoCommand);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
index 29bc02d..10d22fc 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
@@ -1,25 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-
-<dom-module id="gr-repo-command">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -27,13 +24,7 @@
}
</style>
<h3>[[title]]</h3>
- <gr-button
- title$="[[tooltip]]"
- disabled$="[[disabled]]"
- on-click
- ="_onCommandTap">
+ <gr-button title\$="[[tooltip]]" disabled\$="[[disabled]]" on-click="_onCommandTap">
[[title]]
</gr-button>
- </template>
- <script src="gr-repo-command.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
index f4988a5..a3f507b 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_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-repo-command</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-repo-command.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-repo-command.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-command.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,21 +40,24 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-command tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-command.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-repo-command tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('dispatched command-tap on button tap', done => {
- element.addEventListener('command-tap', () => {
- done();
- });
- MockInteractions.tap(
- Polymer.dom(element.root).querySelector('gr-button'));
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('dispatched command-tap on button tap', done => {
+ element.addEventListener('command-tap', () => {
+ done();
+ });
+ MockInteractions.tap(
+ dom(element.root).querySelector('gr-button'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 80b187a..de9d8e2 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -14,113 +14,131 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const GC_MESSAGE = 'Garbage collection completed successfully.';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-change-dialog/gr-create-change-dialog.js';
+import '../gr-repo-command/gr-repo-command.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-repo-commands_html.js';
- const CONFIG_BRANCH = 'refs/meta/config';
- const CONFIG_PATH = 'project.config';
- const EDIT_CONFIG_SUBJECT = 'Edit Repo Config';
- const INITIAL_PATCHSET = 1;
- const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.';
- const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
+const GC_MESSAGE = 'Garbage collection completed successfully.';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrRepoCommands extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-commands'; }
+const CONFIG_BRANCH = 'refs/meta/config';
+const CONFIG_PATH = 'project.config';
+const EDIT_CONFIG_SUBJECT = 'Edit Repo Config';
+const INITIAL_PATCHSET = 1;
+const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.';
+const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
- static get properties() {
- return {
- params: Object,
- repo: String,
- _loading: {
- type: Boolean,
- value: true,
- },
- /** @type {?} */
- _repoConfig: Object,
- _canCreate: Boolean,
- };
- }
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRepoCommands extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this._loadRepo();
+ static get is() { return 'gr-repo-commands'; }
- this.fire('title-change', {title: 'Repo Commands'});
- }
-
- _loadRepo() {
- if (!this.repo) { return Promise.resolve(); }
-
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- return this.$.restAPI.getProjectConfig(this.repo, errFn)
- .then(config => {
- if (!config) { return Promise.resolve(); }
-
- this._repoConfig = config;
- this._loading = false;
- });
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _handleRunningGC() {
- return this.$.restAPI.runRepoGC(this.repo).then(response => {
- if (response.status === 200) {
- this.dispatchEvent(new CustomEvent(
- 'show-alert',
- {detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
- }
- });
- }
-
- _createNewChange() {
- this.$.createChangeOverlay.open();
- }
-
- _handleCreateChange() {
- this.$.createNewChangeModal.handleCreateChange();
- this._handleCloseCreateChange();
- }
-
- _handleCloseCreateChange() {
- this.$.createChangeOverlay.close();
- }
-
- _handleEditRepoConfig() {
- return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
- EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
- const message = change ?
- CREATE_CHANGE_SUCCEEDED_MESSAGE :
- CREATE_CHANGE_FAILED_MESSAGE;
- this.dispatchEvent(new CustomEvent('show-alert',
- {detail: {message}, bubbles: true, composed: true}));
- if (!change) { return; }
-
- Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
- change, CONFIG_PATH, INITIAL_PATCHSET));
- });
- }
+ static get properties() {
+ return {
+ params: Object,
+ repo: String,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {?} */
+ _repoConfig: Object,
+ _canCreate: Boolean,
+ };
}
- customElements.define(GrRepoCommands.is, GrRepoCommands);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadRepo();
+
+ this.fire('title-change', {title: 'Repo Commands'});
+ }
+
+ _loadRepo() {
+ if (!this.repo) { return Promise.resolve(); }
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ return this.$.restAPI.getProjectConfig(this.repo, errFn)
+ .then(config => {
+ if (!config) { return Promise.resolve(); }
+
+ this._repoConfig = config;
+ this._loading = false;
+ });
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _handleRunningGC() {
+ return this.$.restAPI.runRepoGC(this.repo).then(response => {
+ if (response.status === 200) {
+ this.dispatchEvent(new CustomEvent(
+ 'show-alert',
+ {detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
+ }
+ });
+ }
+
+ _createNewChange() {
+ this.$.createChangeOverlay.open();
+ }
+
+ _handleCreateChange() {
+ this.$.createNewChangeModal.handleCreateChange();
+ this._handleCloseCreateChange();
+ }
+
+ _handleCloseCreateChange() {
+ this.$.createChangeOverlay.close();
+ }
+
+ _handleEditRepoConfig() {
+ return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
+ EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
+ const message = change ?
+ CREATE_CHANGE_SUCCEEDED_MESSAGE :
+ CREATE_CHANGE_FAILED_MESSAGE;
+ this.dispatchEvent(new CustomEvent('show-alert',
+ {detail: {message}, bubbles: true, composed: true}));
+ if (!change) { return; }
+
+ Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
+ change, CONFIG_PATH, INITIAL_PATCHSET));
+ });
+ }
+}
+
+customElements.define(GrRepoCommands.is, GrRepoCommands);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
index b610460..ce19555 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
@@ -1,36 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-change-dialog/gr-create-change-dialog.html">
-<link rel="import" href="../gr-repo-command/gr-repo-command.html">
-
-<dom-module id="gr-repo-commands">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -42,24 +28,15 @@
</style>
<main class="gr-form-styles read-only">
<h1 id="Title">Repository Commands</h1>
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div>
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<h2 id="options">Command</h2>
<div id="form">
- <gr-repo-command
- title="Create change"
- on-command-tap="_createNewChange">
+ <gr-repo-command title="Create change" on-command-tap="_createNewChange">
</gr-repo-command>
- <gr-repo-command
- id="editRepoConfig"
- title="Edit repo config"
- on-command-tap="_handleEditRepoConfig">
+ <gr-repo-command id="editRepoConfig" title="Edit repo config" on-command-tap="_handleEditRepoConfig">
</gr-repo-command>
- <gr-repo-command
- title="[[_repoConfig.actions.gc.label]]"
- tooltip="[[_repoConfig.actions.gc.title]]"
- hidden$="[[!_repoConfig.actions.gc.enabled]]"
- on-command-tap="_handleRunningGC">
+ <gr-repo-command title="[[_repoConfig.actions.gc.label]]" tooltip="[[_repoConfig.actions.gc.title]]" hidden\$="[[!_repoConfig.actions.gc.enabled]]" on-command-tap="_handleRunningGC">
</gr-repo-command>
<gr-endpoint-decorator name="repo-command">
<gr-endpoint-param name="config" value="[[_repoConfig]]">
@@ -70,25 +47,15 @@
</div>
</div>
</main>
- <gr-overlay id="createChangeOverlay" with-backdrop>
- <gr-dialog
- id="createChangeDialog"
- confirm-label="Create"
- disabled="[[!_canCreate]]"
- on-confirm="_handleCreateChange"
- on-cancel="_handleCloseCreateChange">
+ <gr-overlay id="createChangeOverlay" with-backdrop="">
+ <gr-dialog id="createChangeDialog" confirm-label="Create" disabled="[[!_canCreate]]" on-confirm="_handleCreateChange" on-cancel="_handleCloseCreateChange">
<div class="header" slot="header">
Create Change
</div>
<div class="main" slot="main">
- <gr-create-change-dialog
- id="createNewChangeModal"
- can-create="{{_canCreate}}"
- repo-name="[[repo]]"></gr-create-change-dialog>
+ <gr-create-change-dialog id="createNewChangeModal" can-create="{{_canCreate}}" repo-name="[[repo]]"></gr-create-change-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-commands.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
index da8b57f..c2f71e7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-commands</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/page/page.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-repo-commands.html">
+<script src="/node_modules/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 type="module" src="./gr-repo-commands.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-commands.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,111 +41,113 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-commands tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let repoStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-commands.js';
+suite('gr-repo-commands tests', () => {
+ let element;
+ let sandbox;
+ let repoStub;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ repoStub = sandbox.stub(
+ element.$.restAPI,
+ 'getProjectConfig',
+ () => Promise.resolve({}));
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('create new change dialog', () => {
+ test('_createNewChange opens modal', () => {
+ const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
+ element._createNewChange();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateChange called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateChange');
+ element.$.createChangeDialog.fire('confirm');
+ assert.isTrue(element._handleCreateChange.called);
+ });
+
+ test('_handleCloseCreateChange called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreateChange');
+ element.$.createChangeDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreateChange.called);
+ });
+ });
+
+ suite('edit repo config', () => {
+ let createChangeStub;
+ let urlStub;
+ let handleSpy;
+ let alertStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- repoStub = sandbox.stub(
- element.$.restAPI,
- 'getProjectConfig',
- () => Promise.resolve({}));
+ createChangeStub = sandbox.stub(element.$.restAPI, 'createChange');
+ urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+ handleSpy = sandbox.spy(element, '_handleEditRepoConfig');
+ alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
});
- teardown(() => {
- sandbox.restore();
- });
+ test('successful creation of change', () => {
+ const change = {_number: '1'};
+ createChangeStub.returns(Promise.resolve(change));
+ MockInteractions.tap(element.$.editRepoConfig.shadowRoot
+ .querySelector('gr-button'));
+ return handleSpy.lastCall.returnValue.then(() => {
+ flushAsynchronousOperations();
- suite('create new change dialog', () => {
- test('_createNewChange opens modal', () => {
- const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
- element._createNewChange();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateChange called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateChange');
- element.$.createChangeDialog.fire('confirm');
- assert.isTrue(element._handleCreateChange.called);
- });
-
- test('_handleCloseCreateChange called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreateChange');
- element.$.createChangeDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreateChange.called);
+ assert.isTrue(alertStub.called);
+ assert.equal(alertStub.lastCall.args[0].detail.message,
+ 'Navigating to change');
+ assert.isTrue(urlStub.called);
+ assert.deepEqual(urlStub.lastCall.args,
+ [change, 'project.config', 1]);
});
});
- suite('edit repo config', () => {
- let createChangeStub;
- let urlStub;
- let handleSpy;
- let alertStub;
+ test('unsuccessful creation of change', () => {
+ createChangeStub.returns(Promise.resolve(null));
+ MockInteractions.tap(element.$.editRepoConfig.shadowRoot
+ .querySelector('gr-button'));
+ return handleSpy.lastCall.returnValue.then(() => {
+ flushAsynchronousOperations();
- setup(() => {
- createChangeStub = sandbox.stub(element.$.restAPI, 'createChange');
- urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
- handleSpy = sandbox.spy(element, '_handleEditRepoConfig');
- alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- });
-
- test('successful creation of change', () => {
- const change = {_number: '1'};
- createChangeStub.returns(Promise.resolve(change));
- MockInteractions.tap(element.$.editRepoConfig.shadowRoot
- .querySelector('gr-button'));
- return handleSpy.lastCall.returnValue.then(() => {
- flushAsynchronousOperations();
-
- assert.isTrue(alertStub.called);
- assert.equal(alertStub.lastCall.args[0].detail.message,
- 'Navigating to change');
- assert.isTrue(urlStub.called);
- assert.deepEqual(urlStub.lastCall.args,
- [change, 'project.config', 1]);
- });
- });
-
- test('unsuccessful creation of change', () => {
- createChangeStub.returns(Promise.resolve(null));
- MockInteractions.tap(element.$.editRepoConfig.shadowRoot
- .querySelector('gr-button'));
- return handleSpy.lastCall.returnValue.then(() => {
- flushAsynchronousOperations();
-
- assert.isTrue(alertStub.called);
- assert.equal(alertStub.lastCall.args[0].detail.message,
- 'Failed to create change.');
- assert.isFalse(urlStub.called);
- });
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- repoStub.restore();
-
- element.repo = 'test';
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
- errFn(response);
- });
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadRepo();
+ assert.isTrue(alertStub.called);
+ assert.equal(alertStub.lastCall.args[0].detail.message,
+ 'Failed to create change.');
+ assert.isFalse(urlStub.called);
});
});
});
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ repoStub.restore();
+
+ element.repo = 'test';
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element._loadRepo();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index 8e09263..e1f38c9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2018 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,89 +14,100 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrRepoDashboards extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-dashboards'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import {flush} 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-repo-dashboards_html.js';
- static get properties() {
- return {
- repo: {
- type: String,
- observer: '_repoChanged',
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _dashboards: Array,
- };
- }
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRepoDashboards extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _repoChanged(repo) {
- this._loading = true;
- if (!repo) { return Promise.resolve(); }
+ static get is() { return 'gr-repo-dashboards'; }
- const errFn = response => {
- this.fire('page-error', {response});
- };
-
- this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => {
- if (!res) { return Promise.resolve(); }
-
- // Group by ref and sort by id.
- const dashboards = res.concat.apply([], res).sort((a, b) =>
- (a.id < b.id ? -1 : 1));
- const dashboardsByRef = {};
- dashboards.forEach(d => {
- if (!dashboardsByRef[d.ref]) {
- dashboardsByRef[d.ref] = [];
- }
- dashboardsByRef[d.ref].push(d);
- });
-
- const dashboardBuilder = [];
- Object.keys(dashboardsByRef).sort()
- .forEach(ref => {
- dashboardBuilder.push({
- section: ref,
- dashboards: dashboardsByRef[ref],
- });
- });
-
- this._dashboards = dashboardBuilder;
- this._loading = false;
- Polymer.dom.flush();
- });
- }
-
- _getUrl(project, id) {
- if (!project || !id) { return ''; }
-
- return Gerrit.Nav.getUrlForRepoDashboard(project, id);
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _computeInheritedFrom(project, definingProject) {
- return project === definingProject ? '' : definingProject;
- }
-
- _computeIsDefault(isDefault) {
- return isDefault ? '✓' : '';
- }
+ static get properties() {
+ return {
+ repo: {
+ type: String,
+ observer: '_repoChanged',
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _dashboards: Array,
+ };
}
- customElements.define(GrRepoDashboards.is, GrRepoDashboards);
-})();
+ _repoChanged(repo) {
+ this._loading = true;
+ if (!repo) { return Promise.resolve(); }
+
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+
+ this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => {
+ if (!res) { return Promise.resolve(); }
+
+ // Group by ref and sort by id.
+ const dashboards = res.concat.apply([], res).sort((a, b) =>
+ (a.id < b.id ? -1 : 1));
+ const dashboardsByRef = {};
+ dashboards.forEach(d => {
+ if (!dashboardsByRef[d.ref]) {
+ dashboardsByRef[d.ref] = [];
+ }
+ dashboardsByRef[d.ref].push(d);
+ });
+
+ const dashboardBuilder = [];
+ Object.keys(dashboardsByRef).sort()
+ .forEach(ref => {
+ dashboardBuilder.push({
+ section: ref,
+ dashboards: dashboardsByRef[ref],
+ });
+ });
+
+ this._dashboards = dashboardBuilder;
+ this._loading = false;
+ flush();
+ });
+ }
+
+ _getUrl(project, id) {
+ if (!project || !id) { return ''; }
+
+ return Gerrit.Nav.getUrlForRepoDashboard(project, id);
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _computeInheritedFrom(project, definingProject) {
+ return project === definingProject ? '' : definingProject;
+ }
+
+ _computeIsDefault(isDefault) {
+ return isDefault ? '✓' : '';
+ }
+}
+
+customElements.define(GrRepoDashboards.is, GrRepoDashboards);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
index f74f705..3bac16c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
@@ -1,27 +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="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-repo-dashboards">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -38,8 +33,8 @@
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <table id="list" class$="genericList [[_computeLoadingClass(_loading)]]">
- <tr class="headerRow">
+ <table id="list" class\$="genericList [[_computeLoadingClass(_loading)]]">
+ <tbody><tr class="headerRow">
<th class="topHeader">Dashboard name</th>
<th class="topHeader">Dashboard title</th>
<th class="topHeader">Dashboard description</th>
@@ -49,14 +44,14 @@
<tr id="loadingContainer">
<td>Loading...</td>
</tr>
- <tbody id="dashboards">
+ </tbody><tbody id="dashboards">
<template is="dom-repeat" items="[[_dashboards]]">
<tr class="groupHeader">
<td colspan="5">[[item.section]]</td>
</tr>
<template is="dom-repeat" items="[[item.dashboards]]">
<tr class="table">
- <td class="name"><a href$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td>
+ <td class="name"><a href\$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td>
<td class="title">[[item.title]]</td>
<td class="desc">[[item.description]]</td>
<td class="inherited">[[_computeInheritedFrom(item.project, item.defining_project)]]</td>
@@ -67,6 +62,4 @@
</tbody>
</table>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-dashboards.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
index 681ee19..1d6f05e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_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-repo-dashboards</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-repo-dashboards.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-repo-dashboards.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-dashboards.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,128 +40,130 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-dashboards 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-repo-dashboards.js';
+suite('gr-repo-dashboards tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('dashboard table', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
+ Promise.resolve([
+ {
+ id: 'default:contributor',
+ project: 'gerrit',
+ defining_project: 'gerrit',
+ ref: 'default',
+ path: 'contributor',
+ description: 'Own contributions.',
+ foreach: 'owner:self',
+ url: '/dashboard/?params',
+ title: 'Contributor Dashboard',
+ sections: [
+ {
+ name: 'Mine To Rebase',
+ query: 'is:open -is:mergeable',
+ },
+ {
+ name: 'My Recently Merged',
+ query: 'is:merged limit:10',
+ },
+ ],
+ },
+ {
+ id: 'custom:custom2',
+ project: 'gerrit',
+ defining_project: 'Public-Projects',
+ ref: 'custom',
+ path: 'open',
+ description: 'Recent open changes.',
+ url: '/dashboard/?params',
+ title: 'Open Changes',
+ sections: [
+ {
+ name: 'Open Changes',
+ query: 'status:open project:${project} -age:7w',
+ },
+ ],
+ },
+ {
+ id: 'default:abc',
+ project: 'gerrit',
+ ref: 'default',
+ },
+ {
+ id: 'custom:custom1',
+ project: 'gerrit',
+ ref: 'custom',
+ },
+ ]));
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('dashboard table', () => {
- setup(() => {
- sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
- Promise.resolve([
- {
- id: 'default:contributor',
- project: 'gerrit',
- defining_project: 'gerrit',
- ref: 'default',
- path: 'contributor',
- description: 'Own contributions.',
- foreach: 'owner:self',
- url: '/dashboard/?params',
- title: 'Contributor Dashboard',
- sections: [
- {
- name: 'Mine To Rebase',
- query: 'is:open -is:mergeable',
- },
- {
- name: 'My Recently Merged',
- query: 'is:merged limit:10',
- },
- ],
- },
- {
- id: 'custom:custom2',
- project: 'gerrit',
- defining_project: 'Public-Projects',
- ref: 'custom',
- path: 'open',
- description: 'Recent open changes.',
- url: '/dashboard/?params',
- title: 'Open Changes',
- sections: [
- {
- name: 'Open Changes',
- query: 'status:open project:${project} -age:7w',
- },
- ],
- },
- {
- id: 'default:abc',
- project: 'gerrit',
- ref: 'default',
- },
- {
- id: 'custom:custom1',
- project: 'gerrit',
- ref: 'custom',
- },
- ]));
- });
-
- test('loading, sections, and ordering', done => {
- assert.isTrue(element._loading);
- assert.notEqual(getComputedStyle(element.$.loadingContainer).display,
+ test('loading, sections, and ordering', done => {
+ assert.isTrue(element._loading);
+ assert.notEqual(getComputedStyle(element.$.loadingContainer).display,
+ 'none');
+ assert.equal(getComputedStyle(element.$.dashboards).display,
+ 'none');
+ element.repo = 'test';
+ flush(() => {
+ assert.equal(getComputedStyle(element.$.loadingContainer).display,
'none');
- assert.equal(getComputedStyle(element.$.dashboards).display,
+ assert.notEqual(getComputedStyle(element.$.dashboards).display,
'none');
- element.repo = 'test';
- flush(() => {
- assert.equal(getComputedStyle(element.$.loadingContainer).display,
- 'none');
- assert.notEqual(getComputedStyle(element.$.dashboards).display,
- 'none');
- assert.equal(element._dashboards.length, 2);
- assert.equal(element._dashboards[0].section, 'custom');
- assert.equal(element._dashboards[1].section, 'default');
+ assert.equal(element._dashboards.length, 2);
+ assert.equal(element._dashboards[0].section, 'custom');
+ assert.equal(element._dashboards[1].section, 'default');
- const dashboards = element._dashboards[0].dashboards;
- assert.equal(dashboards.length, 2);
- assert.equal(dashboards[0].id, 'custom:custom1');
- assert.equal(dashboards[1].id, 'custom:custom2');
+ const dashboards = element._dashboards[0].dashboards;
+ assert.equal(dashboards.length, 2);
+ assert.equal(dashboards[0].id, 'custom:custom1');
+ assert.equal(dashboards[1].id, 'custom:custom2');
- done();
- });
- });
- });
-
- suite('test url', () => {
- test('_getUrl', () => {
- sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard',
- () => '/r/dashboard/test');
-
- assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test');
-
- assert.equal(element._getUrl(undefined, undefined), '');
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getRepoDashboards', (repo, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element.repo = 'test';
+ done();
});
});
});
+
+ suite('test url', () => {
+ test('_getUrl', () => {
+ sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard',
+ () => '/r/dashboard/test');
+
+ assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test');
+
+ assert.equal(element._getUrl(undefined, undefined), '');
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getRepoDashboards', (repo, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ element.repo = 'test';
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
index ccfdfc6..82a6a4c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -14,279 +14,302 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
- const DETAIL_TYPES = {
- BRANCHES: 'branches',
- TAGS: 'tags',
- };
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-pointer-dialog/gr-create-pointer-dialog.js';
+import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
+import {flush} 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-repo-detail-list_html.js';
- const PGP_START = '-----BEGIN PGP SIGNATURE-----';
+const DETAIL_TYPES = {
+ BRANCHES: 'branches',
+ TAGS: 'tags',
+};
- /**
- * @appliesMixin Gerrit.ListViewMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRepoDetailList extends Polymer.mixinBehaviors( [
- Gerrit.ListViewBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-detail-list'; }
+const PGP_START = '-----BEGIN PGP SIGNATURE-----';
- static get properties() {
- return {
+/**
+ * @appliesMixin Gerrit.ListViewMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRepoDetailList extends mixinBehaviors( [
+ Gerrit.ListViewBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-detail-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
/**
- * URL params passed from the router.
+ * The kind of detail we are displaying, possibilities are determined by
+ * the const DETAIL_TYPES.
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- /**
- * The kind of detail we are displaying, possibilities are determined by
- * the const DETAIL_TYPES.
- */
- detailType: String,
+ detailType: String,
- _editing: {
- type: Boolean,
- value: false,
- },
- _isOwner: {
- type: Boolean,
- value: false,
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
- _repo: Object,
- _items: Array,
- /**
- * Because we request one more than the projectsPerPage, _shownProjects
- * maybe one less than _projects.
- */
- _shownItems: {
- type: Array,
- computed: 'computeShownItems(_items)',
- },
- _itemsPerPage: {
- type: Number,
- value: 25,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: String,
- _refName: String,
- _hasNewItemName: Boolean,
- _isEditing: Boolean,
- _revisedRef: String,
- };
- }
+ _editing: {
+ type: Boolean,
+ value: false,
+ },
+ _isOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
+ _repo: Object,
+ _items: Array,
+ /**
+ * Because we request one more than the projectsPerPage, _shownProjects
+ * maybe one less than _projects.
+ */
+ _shownItems: {
+ type: Array,
+ computed: 'computeShownItems(_items)',
+ },
+ _itemsPerPage: {
+ type: Number,
+ value: 25,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: String,
+ _refName: String,
+ _hasNewItemName: Boolean,
+ _isEditing: Boolean,
+ _revisedRef: String,
+ };
+ }
- _determineIfOwner(repo) {
- return this.$.restAPI.getRepoAccess(repo)
- .then(access =>
- this._isOwner = access && !!access[repo].is_owner);
- }
+ _determineIfOwner(repo) {
+ return this.$.restAPI.getRepoAccess(repo)
+ .then(access =>
+ this._isOwner = access && !!access[repo].is_owner);
+ }
- _paramsChanged(params) {
- if (!params || !params.repo) { return; }
+ _paramsChanged(params) {
+ if (!params || !params.repo) { return; }
- this._repo = params.repo;
+ this._repo = params.repo;
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this._determineIfOwner(this._repo);
- }
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this._determineIfOwner(this._repo);
+ }
+ });
+
+ this.detailType = params.detail;
+
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getItems(this._filter, this._repo,
+ this._itemsPerPage, this._offset, this.detailType);
+ }
+
+ _getItems(filter, repo, itemsPerPage, offset, detailType) {
+ this._loading = true;
+ this._items = [];
+ flush();
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return this.$.restAPI.getRepoBranches(
+ filter, repo, itemsPerPage, offset, errFn).then(items => {
+ if (!items) { return; }
+ this._items = items;
+ this._loading = false;
});
-
- this.detailType = params.detail;
-
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getItems(this._filter, this._repo,
- this._itemsPerPage, this._offset, this.detailType);
- }
-
- _getItems(filter, repo, itemsPerPage, offset, detailType) {
- this._loading = true;
- this._items = [];
- Polymer.dom.flush();
- const errFn = response => {
- this.fire('page-error', {response});
- };
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return this.$.restAPI.getRepoBranches(
- filter, repo, itemsPerPage, offset, errFn).then(items => {
- if (!items) { return; }
- this._items = items;
- this._loading = false;
- });
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return this.$.restAPI.getRepoTags(
- filter, repo, itemsPerPage, offset, errFn).then(items => {
- if (!items) { return; }
- this._items = items;
- this._loading = false;
- });
- }
- }
-
- _getPath(repo) {
- return `/admin/repos/${this.encodeURL(repo, false)},` +
- `${this.detailType}`;
- }
-
- _computeWeblink(repo) {
- if (!repo.web_links) { return ''; }
- const webLinks = repo.web_links;
- return webLinks.length ? webLinks : null;
- }
-
- _computeMessage(message) {
- if (!message) { return; }
- // Strip PGP info.
- return message.split(PGP_START)[0];
- }
-
- _stripRefs(item, detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return item.replace('refs/heads/', '');
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return item.replace('refs/tags/', '');
- }
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _computeEditingClass(isEditing) {
- return isEditing ? 'editing' : '';
- }
-
- _computeCanEditClass(ref, detailType, isOwner) {
- return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ?
- 'canEdit' : '';
- }
-
- _handleEditRevision(e) {
- this._revisedRef = e.model.get('item.revision');
- this._isEditing = true;
- }
-
- _handleCancelRevision() {
- this._isEditing = false;
- }
-
- _handleSaveRevision(e) {
- this._setRepoHead(this._repo, this._revisedRef, e);
- }
-
- _setRepoHead(repo, ref, e) {
- return this.$.restAPI.setRepoHead(repo, ref).then(res => {
- if (res.status < 400) {
- this._isEditing = false;
- e.model.set('item.revision', ref);
- // This is needed to refresh _items property with fresh data,
- // specifically can_delete from the json response.
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return this.$.restAPI.getRepoTags(
+ filter, repo, itemsPerPage, offset, errFn).then(items => {
+ if (!items) { return; }
+ this._items = items;
+ this._loading = false;
});
}
-
- _computeItemName(detailType) {
- if (detailType === DETAIL_TYPES.BRANCHES) {
- return 'Branch';
- } else if (detailType === DETAIL_TYPES.TAGS) {
- return 'Tag';
- }
- }
-
- _handleDeleteItemConfirm() {
- this.$.overlay.close();
- if (this.detailType === DETAIL_TYPES.BRANCHES) {
- return this.$.restAPI.deleteRepoBranches(this._repo, this._refName)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
- });
- } else if (this.detailType === DETAIL_TYPES.TAGS) {
- return this.$.restAPI.deleteRepoTags(this._repo, this._refName)
- .then(itemDeleted => {
- if (itemDeleted.status === 204) {
- this._getItems(
- this._filter, this._repo, this._itemsPerPage,
- this._offset, this.detailType);
- }
- });
- }
- }
-
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
- }
-
- _handleDeleteItem(e) {
- const name = this._stripRefs(e.model.get('item.ref'), this.detailType);
- if (!name) { return; }
- this._refName = name;
- this.$.overlay.open();
- }
-
- _computeHideDeleteClass(owner, canDelete) {
- if (canDelete || owner) {
- return 'show';
- }
-
- return '';
- }
-
- _handleCreateItem() {
- this.$.createNewModal.handleCreateItem();
- this._handleCloseCreate();
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
- this.$.createOverlay.open();
- }
-
- _hideIfBranch(type) {
- if (type === DETAIL_TYPES.BRANCHES) {
- return 'hideItem';
- }
-
- return '';
- }
-
- _computeHideTagger(tagger) {
- return tagger ? '' : 'hide';
- }
}
- customElements.define(GrRepoDetailList.is, GrRepoDetailList);
-})();
+ _getPath(repo) {
+ return `/admin/repos/${this.encodeURL(repo, false)},` +
+ `${this.detailType}`;
+ }
+
+ _computeWeblink(repo) {
+ if (!repo.web_links) { return ''; }
+ const webLinks = repo.web_links;
+ return webLinks.length ? webLinks : null;
+ }
+
+ _computeMessage(message) {
+ if (!message) { return; }
+ // Strip PGP info.
+ return message.split(PGP_START)[0];
+ }
+
+ _stripRefs(item, detailType) {
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return item.replace('refs/heads/', '');
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return item.replace('refs/tags/', '');
+ }
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _computeEditingClass(isEditing) {
+ return isEditing ? 'editing' : '';
+ }
+
+ _computeCanEditClass(ref, detailType, isOwner) {
+ return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ?
+ 'canEdit' : '';
+ }
+
+ _handleEditRevision(e) {
+ this._revisedRef = e.model.get('item.revision');
+ this._isEditing = true;
+ }
+
+ _handleCancelRevision() {
+ this._isEditing = false;
+ }
+
+ _handleSaveRevision(e) {
+ this._setRepoHead(this._repo, this._revisedRef, e);
+ }
+
+ _setRepoHead(repo, ref, e) {
+ return this.$.restAPI.setRepoHead(repo, ref).then(res => {
+ if (res.status < 400) {
+ this._isEditing = false;
+ e.model.set('item.revision', ref);
+ // This is needed to refresh _items property with fresh data,
+ // specifically can_delete from the json response.
+ this._getItems(
+ this._filter, this._repo, this._itemsPerPage,
+ this._offset, this.detailType);
+ }
+ });
+ }
+
+ _computeItemName(detailType) {
+ if (detailType === DETAIL_TYPES.BRANCHES) {
+ return 'Branch';
+ } else if (detailType === DETAIL_TYPES.TAGS) {
+ return 'Tag';
+ }
+ }
+
+ _handleDeleteItemConfirm() {
+ this.$.overlay.close();
+ if (this.detailType === DETAIL_TYPES.BRANCHES) {
+ return this.$.restAPI.deleteRepoBranches(this._repo, this._refName)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this._getItems(
+ this._filter, this._repo, this._itemsPerPage,
+ this._offset, this.detailType);
+ }
+ });
+ } else if (this.detailType === DETAIL_TYPES.TAGS) {
+ return this.$.restAPI.deleteRepoTags(this._repo, this._refName)
+ .then(itemDeleted => {
+ if (itemDeleted.status === 204) {
+ this._getItems(
+ this._filter, this._repo, this._itemsPerPage,
+ this._offset, this.detailType);
+ }
+ });
+ }
+ }
+
+ _handleConfirmDialogCancel() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteItem(e) {
+ const name = this._stripRefs(e.model.get('item.ref'), this.detailType);
+ if (!name) { return; }
+ this._refName = name;
+ this.$.overlay.open();
+ }
+
+ _computeHideDeleteClass(owner, canDelete) {
+ if (canDelete || owner) {
+ return 'show';
+ }
+
+ return '';
+ }
+
+ _handleCreateItem() {
+ this.$.createNewModal.handleCreateItem();
+ this._handleCloseCreate();
+ }
+
+ _handleCloseCreate() {
+ this.$.createOverlay.close();
+ }
+
+ _handleCreateClicked() {
+ this.$.createOverlay.open();
+ }
+
+ _hideIfBranch(type) {
+ if (type === DETAIL_TYPES.BRANCHES) {
+ return 'hideItem';
+ }
+
+ return '';
+ }
+
+ _computeHideTagger(tagger) {
+ return tagger ? '' : 'hide';
+ }
+}
+
+customElements.define(GrRepoDetailList.is, GrRepoDetailList);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
index 467cef0..0d232f2 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
@@ -1,40 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-pointer-dialog/gr-create-pointer-dialog.html">
-<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
-
-<dom-module id="gr-repo-detail-list">
- <template>
+export const htmlTemplate = html`
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -88,100 +70,68 @@
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <gr-list-view
- create-new="[[_loggedIn]]"
- filter="[[_filter]]"
- items-per-page="[[_itemsPerPage]]"
- items="[[_items]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_getPath(_repo, detailType)]]">
+ <gr-list-view create-new="[[_loggedIn]]" filter="[[_filter]]" items-per-page="[[_itemsPerPage]]" items="[[_items]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_getPath(_repo, detailType)]]">
<table id="list" class="genericList gr-form-styles">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="name topHeader">Name</th>
<th class="revision topHeader">Revision</th>
- <th class$="message topHeader [[_hideIfBranch(detailType)]]">
+ <th class\$="message topHeader [[_hideIfBranch(detailType)]]">
Message</th>
- <th class$="tagger topHeader [[_hideIfBranch(detailType)]]">
+ <th class\$="tagger topHeader [[_hideIfBranch(detailType)]]">
Tagger</th>
<th class="repositoryBrowser topHeader">
Repository Browser</th>
<th class="delete topHeader"></th>
</tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownItems]]">
<tr class="table">
- <td class$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td>
- <td class$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]">
+ <td class\$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td>
+ <td class\$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]">
<span class="revisionNoEditing">
[[item.revision]]
</span>
- <span class$="revisionEdit [[_computeEditingClass(_isEditing)]]">
+ <span class\$="revisionEdit [[_computeEditingClass(_isEditing)]]">
<span class="revisionWithEditing">
[[item.revision]]
</span>
- <gr-button
- link
- on-click="_handleEditRevision"
- class="editBtn">
+ <gr-button link="" on-click="_handleEditRevision" class="editBtn">
edit
</gr-button>
- <iron-input
- bind-value="{{_revisedRef}}"
- class="editItem">
- <input
- is="iron-input"
- bind-value="{{_revisedRef}}">
+ <iron-input bind-value="{{_revisedRef}}" class="editItem">
+ <input is="iron-input" bind-value="{{_revisedRef}}">
</iron-input>
- <gr-button
- link
- on-click="_handleCancelRevision"
- class="cancelBtn editItem">
+ <gr-button link="" on-click="_handleCancelRevision" class="cancelBtn editItem">
Cancel
</gr-button>
- <gr-button
- link
- on-click="_handleSaveRevision"
- class="saveBtn editItem"
- disabled="[[!_revisedRef]]">
+ <gr-button link="" on-click="_handleSaveRevision" class="saveBtn editItem" disabled="[[!_revisedRef]]">
Save
</gr-button>
</span>
</td>
- <td class$="message [[_hideIfBranch(detailType)]]">
+ <td class\$="message [[_hideIfBranch(detailType)]]">
[[_computeMessage(item.message)]]
</td>
- <td class$="tagger [[_hideIfBranch(detailType)]]">
- <div class$="tagger [[_computeHideTagger(item.tagger)]]">
- <gr-account-link
- account="[[item.tagger]]">
+ <td class\$="tagger [[_hideIfBranch(detailType)]]">
+ <div class\$="tagger [[_computeHideTagger(item.tagger)]]">
+ <gr-account-link account="[[item.tagger]]">
</gr-account-link>
- (<gr-date-formatter
- has-tooltip
- date-str="[[item.tagger.date]]">
+ (<gr-date-formatter has-tooltip="" date-str="[[item.tagger.date]]">
</gr-date-formatter>)
</div>
</td>
<td class="repositoryBrowser">
- <template is="dom-repeat"
- items="[[_computeWeblink(item)]]" as="link">
- <a href$="[[link.url]]"
- class="webLink"
- rel="noopener"
- target="_blank">
+ <template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link">
+ <a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
([[link.name]])
</a>
</template>
</td>
<td class="delete">
- <gr-button
- link
- class$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]"
- on-click="_handleDeleteItem">
+ <gr-button link="" class\$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]" on-click="_handleDeleteItem">
Delete
</gr-button>
</td>
@@ -189,36 +139,19 @@
</template>
</tbody>
</table>
- <gr-overlay id="overlay" with-backdrop>
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteItemConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_refName]]"
- item-type="[[detailType]]"></gr-confirm-delete-item-dialog>
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteItemConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_refName]]" item-type="[[detailType]]"></gr-confirm-delete-item-dialog>
</gr-overlay>
</gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- id="createDialog"
- disabled="[[!_hasNewItemName]]"
- confirm-label="Create"
- on-confirm="_handleCreateItem"
- on-cancel="_handleCloseCreate">
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog id="createDialog" disabled="[[!_hasNewItemName]]" confirm-label="Create" on-confirm="_handleCreateItem" on-cancel="_handleCloseCreate">
<div class="header" slot="header">
Create [[_computeItemName(detailType)]]
</div>
<div class="main" slot="main">
- <gr-create-pointer-dialog
- id="createNewModal"
- detail-type="[[_computeItemName(detailType)]]"
- has-new-item-name="{{_hasNewItemName}}"
- item-detail="[[detailType]]"
- repo-name="[[_repo]]"></gr-create-pointer-dialog>
+ <gr-create-pointer-dialog id="createNewModal" detail-type="[[_computeItemName(detailType)]]" has-new-item-name="{{_hasNewItemName}}" item-detail="[[detailType]]" repo-name="[[_repo]]"></gr-create-pointer-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-detail-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
index d466f28..13510d8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-detail-list</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/page/page.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-repo-detail-list.html">
+<script src="/node_modules/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 type="module" src="./gr-repo-detail-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-detail-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,533 +41,536 @@
</template>
</test-fixture>
-<script>
- let counter;
- const branchGenerator = () => {
- return {
- ref: `refs/heads/test${++counter}`,
- revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
- web_links: [
- {
- name: 'diffusion',
- url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
- },
- ],
- };
- };
- const tagGenerator = () => {
- return {
- ref: `refs/tags/test${++counter}`,
- revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
- web_links: [
- {
- name: 'diffusion',
- url: `https://git.example.org/tag/test;refs/tags/test${counter}`,
- },
- ],
- message: 'Annotated tag',
- tagger: {
- name: 'Test User',
- email: 'test.user@gmail.com',
- date: '2017-09-19 14:54:00.000000000',
- tz: 540,
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-detail-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+let counter;
+const branchGenerator = () => {
+ return {
+ ref: `refs/heads/test${++counter}`,
+ revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
},
- };
+ ],
};
+};
+const tagGenerator = () => {
+ return {
+ ref: `refs/tags/test${++counter}`,
+ revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://git.example.org/tag/test;refs/tags/test${counter}`,
+ },
+ ],
+ message: 'Annotated tag',
+ tagger: {
+ name: 'Test User',
+ email: 'test.user@gmail.com',
+ date: '2017-09-19 14:54:00.000000000',
+ tz: 540,
+ },
+ };
+};
- suite('gr-repo-detail-list', async () => {
- await readyToTest();
- suite('Branches', () => {
- let element;
- let branches;
- let sandbox;
+suite('gr-repo-detail-list', () => {
+ suite('Branches', () => {
+ let element;
+ let branches;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.detailType = 'branches';
- counter = 0;
- sandbox.stub(page, 'show');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.detailType = 'branches';
+ counter = 0;
+ sandbox.stub(page, 'show');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list of repo branches', () => {
+ setup(done => {
+ branches = [{
+ ref: 'HEAD',
+ revision: 'master',
+ }].concat(_.times(25, branchGenerator));
+
+ stub('gr-rest-api-interface', {
+ getRepoBranches(num, project, offset) {
+ return Promise.resolve(branches);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'branches',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list of repo branches', () => {
- setup(done => {
- branches = [{
- ref: 'HEAD',
- revision: 'master',
- }].concat(_.times(25, branchGenerator));
-
- stub('gr-rest-api-interface', {
- getRepoBranches(num, project, offset) {
- return Promise.resolve(branches);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'branches',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
- });
-
- test('test for branch in the list', done => {
- flush(() => {
- assert.equal(element._items[2].ref, 'refs/heads/test2');
- done();
- });
- });
-
- test('test for web links in the branches list', done => {
- flush(() => {
- assert.equal(element._items[2].web_links[0].url,
- 'https://git.example.org/branch/test;refs/heads/test2');
- done();
- });
- });
-
- test('test for refs/heads/ being striped from ref', done => {
- flush(() => {
- assert.equal(element._stripRefs(element._items[2].ref,
- element.detailType), 'test2');
- done();
- });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
-
- test('Edit HEAD button not admin', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
- Promise.resolve({
- test: {is_owner: false},
- }));
- element._determineIfOwner('test').then(() => {
- assert.equal(element._isOwner, false);
- assert.equal(getComputedStyle(Polymer.dom(element.root)
- .querySelector('.revisionNoEditing')).display, 'inline');
- assert.equal(getComputedStyle(Polymer.dom(element.root)
- .querySelector('.revisionEdit')).display, 'none');
- done();
- });
- });
-
- test('Edit HEAD button admin', done => {
- const saveBtn = Polymer.dom(element.root).querySelector('.saveBtn');
- const cancelBtn = Polymer.dom(element.root).querySelector('.cancelBtn');
- const editBtn = Polymer.dom(element.root).querySelector('.editBtn');
- const revisionNoEditing = Polymer.dom(element.root)
- .querySelector('.revisionNoEditing');
- const revisionWithEditing = Polymer.dom(element.root)
- .querySelector('.revisionWithEditing');
-
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
- Promise.resolve({
- test: {is_owner: true},
- }));
- sandbox.stub(element, '_handleSaveRevision');
- element._determineIfOwner('test').then(() => {
- assert.equal(element._isOwner, true);
- // The revision container for non-editing enabled row is not visible.
- assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
-
- // The revision container for editing enabled row is visible.
- assert.notEqual(getComputedStyle(Polymer.dom(element.root)
- .querySelector('.revisionEdit')).display, 'none');
-
- // The revision and edit button are visible.
- assert.notEqual(getComputedStyle(revisionWithEditing).display,
- 'none');
- assert.notEqual(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- const hiddenElements = Polymer.dom(element.root)
- .querySelectorAll('.canEdit .editItem');
-
- for (const item of hiddenElements) {
- assert.equal(getComputedStyle(item).display, 'none');
- }
-
- MockInteractions.tap(editBtn);
- flushAsynchronousOperations();
- // The revision and edit button are not visible.
- assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
- assert.equal(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- for (const item of hiddenElements) {
- assert.notEqual(getComputedStyle(item).display, 'none');
- }
-
- // The revised ref was set correctly
- assert.equal(element._revisedRef, 'master');
-
- assert.isFalse(saveBtn.disabled);
-
- // Delete the ref.
- element._revisedRef = '';
- assert.isTrue(saveBtn.disabled);
-
- // Change the ref to something else
- element._revisedRef = 'newRef';
- element._repo = 'test';
- assert.isFalse(saveBtn.disabled);
-
- // Save button calls handleSave. since this is stubbed, the edit
- // section remains open.
- MockInteractions.tap(saveBtn);
- assert.isTrue(element._handleSaveRevision.called);
-
- // When cancel is tapped, the edit secion closes.
- MockInteractions.tap(cancelBtn);
- flushAsynchronousOperations();
-
- // The revision and edit button are visible.
- assert.notEqual(getComputedStyle(revisionWithEditing).display,
- 'none');
- assert.notEqual(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- for (const item of hiddenElements) {
- assert.equal(getComputedStyle(item).display, 'none');
- }
- done();
- });
- });
-
- test('_handleSaveRevision with invalid rev', done => {
- const event = {model: {set: sandbox.stub()}};
- element._isEditing = true;
- sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
- Promise.resolve({
- status: 400,
- })
- );
-
- element._setRepoHead('test', 'newRef', event).then(() => {
- assert.isTrue(element._isEditing);
- assert.isFalse(event.model.set.called);
- done();
- });
- });
-
- test('_handleSaveRevision with valid rev', done => {
- const event = {model: {set: sandbox.stub()}};
- element._isEditing = true;
- sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
- Promise.resolve({
- status: 200,
- })
- );
-
- element._setRepoHead('test', 'newRef', event).then(() => {
- assert.isFalse(element._isEditing);
- assert.isTrue(event.model.set.called);
- done();
- });
- });
-
- test('test _computeItemName', () => {
- assert.deepEqual(element._computeItemName('branches'), 'Branch');
- assert.deepEqual(element._computeItemName('tags'), 'Tag');
+ test('test for branch in the list', done => {
+ flush(() => {
+ assert.equal(element._items[2].ref, 'refs/heads/test2');
+ done();
});
});
- suite('list with less then 25 branches', () => {
- setup(done => {
- branches = _.times(25, branchGenerator);
-
- stub('gr-rest-api-interface', {
- getRepoBranches(num, repo, offset) {
- return Promise.resolve(branches);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'branches',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
+ test('test for web links in the branches list', done => {
+ flush(() => {
+ assert.equal(element._items[2].web_links[0].url,
+ 'https://git.example.org/branch/test;refs/heads/test2');
+ done();
});
});
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getRepoBranches',
- () => Promise.resolve(branches));
- const params = {
- detail: 'branches',
- repo: 'test',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params).then(() => {
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[0],
- 'test');
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[1],
- 'test');
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[2],
- 25);
- assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[3],
- 25);
- done();
- });
+ test('test for refs/heads/ being striped from ref', done => {
+ flush(() => {
+ assert.equal(element._stripRefs(element._items[2].ref,
+ element.detailType), 'test2');
+ done();
});
});
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getRepoBranches',
- (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
- errFn(response);
- });
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
+ test('Edit HEAD button not admin', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
+ Promise.resolve({
+ test: {is_owner: false},
+ }));
+ element._determineIfOwner('test').then(() => {
+ assert.equal(element._isOwner, false);
+ assert.equal(getComputedStyle(dom(element.root)
+ .querySelector('.revisionNoEditing')).display, 'inline');
+ assert.equal(getComputedStyle(dom(element.root)
+ .querySelector('.revisionEdit')).display, 'none');
+ done();
+ });
+ });
- const params = {
- detail: 'branches',
- repo: 'test',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params);
+ test('Edit HEAD button admin', done => {
+ const saveBtn = dom(element.root).querySelector('.saveBtn');
+ const cancelBtn = dom(element.root).querySelector('.cancelBtn');
+ const editBtn = dom(element.root).querySelector('.editBtn');
+ const revisionNoEditing = dom(element.root)
+ .querySelector('.revisionNoEditing');
+ const revisionWithEditing = dom(element.root)
+ .querySelector('.revisionWithEditing');
+
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ sandbox.stub(element.$.restAPI, 'getRepoAccess').returns(
+ Promise.resolve({
+ test: {is_owner: true},
+ }));
+ sandbox.stub(element, '_handleSaveRevision');
+ element._determineIfOwner('test').then(() => {
+ assert.equal(element._isOwner, true);
+ // The revision container for non-editing enabled row is not visible.
+ assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
+
+ // The revision container for editing enabled row is visible.
+ assert.notEqual(getComputedStyle(dom(element.root)
+ .querySelector('.revisionEdit')).display, 'none');
+
+ // The revision and edit button are visible.
+ assert.notEqual(getComputedStyle(revisionWithEditing).display,
+ 'none');
+ assert.notEqual(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ const hiddenElements = dom(element.root)
+ .querySelectorAll('.canEdit .editItem');
+
+ for (const item of hiddenElements) {
+ assert.equal(getComputedStyle(item).display, 'none');
+ }
+
+ MockInteractions.tap(editBtn);
+ flushAsynchronousOperations();
+ // The revision and edit button are not visible.
+ assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
+ assert.equal(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ for (const item of hiddenElements) {
+ assert.notEqual(getComputedStyle(item).display, 'none');
+ }
+
+ // The revised ref was set correctly
+ assert.equal(element._revisedRef, 'master');
+
+ assert.isFalse(saveBtn.disabled);
+
+ // Delete the ref.
+ element._revisedRef = '';
+ assert.isTrue(saveBtn.disabled);
+
+ // Change the ref to something else
+ element._revisedRef = 'newRef';
+ element._repo = 'test';
+ assert.isFalse(saveBtn.disabled);
+
+ // Save button calls handleSave. since this is stubbed, the edit
+ // section remains open.
+ MockInteractions.tap(saveBtn);
+ assert.isTrue(element._handleSaveRevision.called);
+
+ // When cancel is tapped, the edit secion closes.
+ MockInteractions.tap(cancelBtn);
+ flushAsynchronousOperations();
+
+ // The revision and edit button are visible.
+ assert.notEqual(getComputedStyle(revisionWithEditing).display,
+ 'none');
+ assert.notEqual(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ for (const item of hiddenElements) {
+ assert.equal(getComputedStyle(item).display, 'none');
+ }
+ done();
+ });
+ });
+
+ test('_handleSaveRevision with invalid rev', done => {
+ const event = {model: {set: sandbox.stub()}};
+ element._isEditing = true;
+ sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
+ Promise.resolve({
+ status: 400,
+ })
+ );
+
+ element._setRepoHead('test', 'newRef', event).then(() => {
+ assert.isTrue(element._isEditing);
+ assert.isFalse(event.model.set.called);
+ done();
+ });
+ });
+
+ test('_handleSaveRevision with valid rev', done => {
+ const event = {model: {set: sandbox.stub()}};
+ element._isEditing = true;
+ sandbox.stub(element.$.restAPI, 'setRepoHead').returns(
+ Promise.resolve({
+ status: 200,
+ })
+ );
+
+ element._setRepoHead('test', 'newRef', event).then(() => {
+ assert.isFalse(element._isEditing);
+ assert.isTrue(event.model.set.called);
+ done();
+ });
+ });
+
+ test('test _computeItemName', () => {
+ assert.deepEqual(element._computeItemName('branches'), 'Branch');
+ assert.deepEqual(element._computeItemName('tags'), 'Tag');
+ });
+ });
+
+ suite('list with less then 25 branches', () => {
+ setup(done => {
+ branches = _.times(25, branchGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepoBranches(num, repo, offset) {
+ return Promise.resolve(branches);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'branches',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getRepoBranches',
+ () => Promise.resolve(branches));
+ const params = {
+ detail: 'branches',
+ repo: 'test',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params).then(() => {
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[0],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[1],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[2],
+ 25);
+ assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[3],
+ 25);
+ done();
});
});
});
- suite('Tags', () => {
- let element;
- let tags;
- let sandbox;
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getRepoBranches',
+ (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
+ errFn(response);
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.detailType = 'tags';
- counter = 0;
- sandbox.stub(page, 'show');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeMessage', () => {
- let message = 'v2.15-rc1↵-----BEGIN PGP SIGNATURE-----↵Version: GnuPG v' +
- '1↵↵iQIcBAABAgAGBQJZ27O7AAoJEF/XxZqaEoiMy6kQAMoQCpGr3J6JITI4BVWsr7QM↵xy' +
- 'EcWH5YPUko5EPTbkABHmaVyFmKGkuIQdn6c+NIbqJOk+5XT4oUyRSo1T569HPJ↵3kyxEJi' +
- 'T1ryvp5BIHwdvHx58fjw1+YkiWLZuZq1FFkUYqnWTYCrkv7Fok98pdOmV↵CL1Hgugi5uK8' +
- '/kxf1M7+Nv6piaZ140pwSb1h6QdAjaZVfaBCnoxlG4LRUqHvEYay↵f4QYgFT67auHIGkZ4' +
- 'moUcsp2Du/1jSsCWL/CPwjPFGbbckVAjLCMT9yD3NKwpEZF↵pfsiZyHI9dL0M+QjVrM+RD' +
- 'HwIIJwra8R0IMkDlQ6MDrFlKNqNBbo588S6UPrm71L↵YuiwWlcrK9ZIybxT6LzbR65Rvez' +
- 'DSitQ+xeIfpZE19/X6BCnvlARLE8k/tC2JksI↵lEZi7Lf3FQdIcwwyt98tJkS9HX9v9jbC' +
- '5QXifnoj3Li8tHSLuQ1dJCxHQiis6ojI↵OWUFkm0IHBXVNHA2dqYBdM+pL12mlI3wp6Ica' +
- '4cdEVDwzu+j1xnVSFUa+d+Y2xJF↵7mytuyhHiKG4hm+zbhMv6WD8Q3FoDsJZeLY99l0hYQ' +
- 'SnnkMduFVroIs45pAs8gUA↵RvYla8mm9w/543IJAPzzFarPVLSsSyQ7tJl3UBzjKRNH/rX' +
- 'W+F22qyWD1zyHPUIR↵C00ItmwlAvveImYKpQAH↵=L+K9↵-----END PGP SIGNATURE---' +
- '--';
- assert.equal(element._computeMessage(message), 'v2.15-rc1↵');
- message = 'v2.15-rc1';
- assert.equal(element._computeMessage(message), 'v2.15-rc1');
- });
-
- suite('list of repo tags', () => {
- setup(done => {
- tags = _.times(26, tagGenerator);
-
- stub('gr-rest-api-interface', {
- getRepoTags(num, repo, offset) {
- return Promise.resolve(tags);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'tags',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
});
- test('test for tag in the list', done => {
- flush(() => {
- assert.equal(element._items[1].ref, 'refs/tags/test2');
- done();
- });
- });
-
- test('test for tag message in the list', done => {
- flush(() => {
- assert.equal(element._items[1].message, 'Annotated tag');
- done();
- });
- });
-
- test('test for tagger in the tag list', done => {
- const tagger = {
- name: 'Test User',
- email: 'test.user@gmail.com',
- date: '2017-09-19 14:54:00.000000000',
- tz: 540,
- };
- flush(() => {
- assert.deepEqual(element._items[1].tagger, tagger);
- done();
- });
- });
-
- test('test for web links in the tags list', done => {
- flush(() => {
- assert.equal(element._items[1].web_links[0].url,
- 'https://git.example.org/tag/test;refs/tags/test2');
- done();
- });
- });
-
- test('test for refs/tags/ being striped from ref', done => {
- flush(() => {
- assert.equal(element._stripRefs(element._items[1].ref,
- element.detailType), 'test2');
- done();
- });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
-
- test('_computeHideTagger', () => {
- const testObject1 = {
- tagger: 'test',
- };
- assert.equal(element._computeHideTagger(testObject1), '');
-
- assert.equal(element._computeHideTagger(undefined), 'hide');
- });
- });
-
- suite('list with less then 25 tags', () => {
- setup(done => {
- tags = _.times(25, tagGenerator);
-
- stub('gr-rest-api-interface', {
- getRepoTags(num, project, offset) {
- return Promise.resolve(tags);
- },
- });
-
- const params = {
- repo: 'test',
- detail: 'tags',
- };
-
- element._paramsChanged(params).then(() => { flush(done); });
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
- });
-
- suite('filter', () => {
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getRepoTags',
- () => Promise.resolve(tags));
- const params = {
- repo: 'test',
- detail: 'tags',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params).then(() => {
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[0],
- 'test');
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[1],
- 'test');
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[2],
- 25);
- assert.equal(element.$.restAPI.getRepoTags.lastCall.args[3],
- 25);
- done();
- });
- });
- });
-
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sandbox.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').fire('create-clicked');
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sandbox.stub(element.$.createOverlay, 'open');
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateItem called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateItem');
- element.$.createDialog.fire('confirm');
- assert.isTrue(element._handleCreateItem.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreate');
- element.$.createDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreate.called);
- });
- });
-
- suite('404', () => {
- test('fires page-error', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getRepoTags',
- (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
- errFn(response);
- });
-
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- const params = {
- repo: 'test',
- detail: 'tags',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params);
- });
- });
-
- test('test _computeHideDeleteClass', () => {
- assert.deepEqual(element._computeHideDeleteClass(true, false), 'show');
- assert.deepEqual(element._computeHideDeleteClass(false, true), 'show');
- assert.deepEqual(element._computeHideDeleteClass(false, false), '');
+ const params = {
+ detail: 'branches',
+ repo: 'test',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params);
});
});
});
+
+ suite('Tags', () => {
+ let element;
+ let tags;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.detailType = 'tags';
+ counter = 0;
+ sandbox.stub(page, 'show');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeMessage', () => {
+ let message = 'v2.15-rc1↵-----BEGIN PGP SIGNATURE-----↵Version: GnuPG v' +
+ '1↵↵iQIcBAABAgAGBQJZ27O7AAoJEF/XxZqaEoiMy6kQAMoQCpGr3J6JITI4BVWsr7QM↵xy' +
+ 'EcWH5YPUko5EPTbkABHmaVyFmKGkuIQdn6c+NIbqJOk+5XT4oUyRSo1T569HPJ↵3kyxEJi' +
+ 'T1ryvp5BIHwdvHx58fjw1+YkiWLZuZq1FFkUYqnWTYCrkv7Fok98pdOmV↵CL1Hgugi5uK8' +
+ '/kxf1M7+Nv6piaZ140pwSb1h6QdAjaZVfaBCnoxlG4LRUqHvEYay↵f4QYgFT67auHIGkZ4' +
+ 'moUcsp2Du/1jSsCWL/CPwjPFGbbckVAjLCMT9yD3NKwpEZF↵pfsiZyHI9dL0M+QjVrM+RD' +
+ 'HwIIJwra8R0IMkDlQ6MDrFlKNqNBbo588S6UPrm71L↵YuiwWlcrK9ZIybxT6LzbR65Rvez' +
+ 'DSitQ+xeIfpZE19/X6BCnvlARLE8k/tC2JksI↵lEZi7Lf3FQdIcwwyt98tJkS9HX9v9jbC' +
+ '5QXifnoj3Li8tHSLuQ1dJCxHQiis6ojI↵OWUFkm0IHBXVNHA2dqYBdM+pL12mlI3wp6Ica' +
+ '4cdEVDwzu+j1xnVSFUa+d+Y2xJF↵7mytuyhHiKG4hm+zbhMv6WD8Q3FoDsJZeLY99l0hYQ' +
+ 'SnnkMduFVroIs45pAs8gUA↵RvYla8mm9w/543IJAPzzFarPVLSsSyQ7tJl3UBzjKRNH/rX' +
+ 'W+F22qyWD1zyHPUIR↵C00ItmwlAvveImYKpQAH↵=L+K9↵-----END PGP SIGNATURE---' +
+ '--';
+ assert.equal(element._computeMessage(message), 'v2.15-rc1↵');
+ message = 'v2.15-rc1';
+ assert.equal(element._computeMessage(message), 'v2.15-rc1');
+ });
+
+ suite('list of repo tags', () => {
+ setup(done => {
+ tags = _.times(26, tagGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepoTags(num, repo, offset) {
+ return Promise.resolve(tags);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('test for tag in the list', done => {
+ flush(() => {
+ assert.equal(element._items[1].ref, 'refs/tags/test2');
+ done();
+ });
+ });
+
+ test('test for tag message in the list', done => {
+ flush(() => {
+ assert.equal(element._items[1].message, 'Annotated tag');
+ done();
+ });
+ });
+
+ test('test for tagger in the tag list', done => {
+ const tagger = {
+ name: 'Test User',
+ email: 'test.user@gmail.com',
+ date: '2017-09-19 14:54:00.000000000',
+ tz: 540,
+ };
+ flush(() => {
+ assert.deepEqual(element._items[1].tagger, tagger);
+ done();
+ });
+ });
+
+ test('test for web links in the tags list', done => {
+ flush(() => {
+ assert.equal(element._items[1].web_links[0].url,
+ 'https://git.example.org/tag/test;refs/tags/test2');
+ done();
+ });
+ });
+
+ test('test for refs/tags/ being striped from ref', done => {
+ flush(() => {
+ assert.equal(element._stripRefs(element._items[1].ref,
+ element.detailType), 'test2');
+ done();
+ });
+ });
+
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
+
+ test('_computeHideTagger', () => {
+ const testObject1 = {
+ tagger: 'test',
+ };
+ assert.equal(element._computeHideTagger(testObject1), '');
+
+ assert.equal(element._computeHideTagger(undefined), 'hide');
+ });
+ });
+
+ suite('list with less then 25 tags', () => {
+ setup(done => {
+ tags = _.times(25, tagGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepoTags(num, project, offset) {
+ return Promise.resolve(tags);
+ },
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ };
+
+ element._paramsChanged(params).then(() => { flush(done); });
+ });
+
+ test('_shownItems', () => {
+ assert.equal(element._shownItems.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getRepoTags',
+ () => Promise.resolve(tags));
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params).then(() => {
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[0],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[1],
+ 'test');
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[2],
+ 25);
+ assert.equal(element.$.restAPI.getRepoTags.lastCall.args[3],
+ 25);
+ done();
+ });
+ });
+ });
+
+ suite('create new', () => {
+ test('_handleCreateClicked called when create-click fired', () => {
+ sandbox.stub(element, '_handleCreateClicked');
+ element.shadowRoot
+ .querySelector('gr-list-view').fire('create-clicked');
+ assert.isTrue(element._handleCreateClicked.called);
+ });
+
+ test('_handleCreateClicked opens modal', () => {
+ const openStub = sandbox.stub(element.$.createOverlay, 'open');
+ element._handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateItem called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateItem');
+ element.$.createDialog.fire('confirm');
+ assert.isTrue(element._handleCreateItem.called);
+ });
+
+ test('_handleCloseCreate called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreate');
+ element.$.createDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreate.called);
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getRepoTags',
+ (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
+ errFn(response);
+ });
+
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
+ });
+
+ const params = {
+ repo: 'test',
+ detail: 'tags',
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(params);
+ });
+ });
+
+ test('test _computeHideDeleteClass', () => {
+ assert.deepEqual(element._computeHideDeleteClass(true, false), 'show');
+ assert.deepEqual(element._computeHideDeleteClass(false, true), 'show');
+ assert.deepEqual(element._computeHideDeleteClass(false, false), '');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
index c509717..24cc9a8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -14,160 +14,174 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-repo-dialog/gr-create-repo-dialog.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-repo-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrRepoList extends mixinBehaviors( [
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-list'; }
+
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/admin/repos',
+ },
+ _hasNewRepoName: Boolean,
+ _createNewCapability: {
+ type: Boolean,
+ value: false,
+ },
+ _repos: Array,
+
+ /**
+ * Because we request one more than the projectsPerPage, _shownProjects
+ * maybe one less than _projects.
+ * */
+ _shownRepos: {
+ type: Array,
+ computed: 'computeShownItems(_repos)',
+ },
+
+ _reposPerPage: {
+ type: Number,
+ value: 25,
+ },
+
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: {
+ type: String,
+ value: '',
+ },
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getCreateRepoCapability();
+ this.fire('title-change', {title: 'Repos'});
+ this._maybeOpenCreateOverlay(this.params);
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+ this._offset = this.getOffsetValue(params);
+
+ return this._getRepos(this._filter, this._reposPerPage,
+ this._offset);
+ }
/**
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
+ * Opens the create overlay if the route has a hash 'create'
+ *
+ * @param {!Object} params
*/
- class GrRepoList extends Polymer.mixinBehaviors( [
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-list'; }
-
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
-
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
- _path: {
- type: String,
- readOnly: true,
- value: '/admin/repos',
- },
- _hasNewRepoName: Boolean,
- _createNewCapability: {
- type: Boolean,
- value: false,
- },
- _repos: Array,
-
- /**
- * Because we request one more than the projectsPerPage, _shownProjects
- * maybe one less than _projects.
- * */
- _shownRepos: {
- type: Array,
- computed: 'computeShownItems(_repos)',
- },
-
- _reposPerPage: {
- type: Number,
- value: 25,
- },
-
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: {
- type: String,
- value: '',
- },
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getCreateRepoCapability();
- this.fire('title-change', {title: 'Repos'});
- this._maybeOpenCreateOverlay(this.params);
- }
-
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
- this._offset = this.getOffsetValue(params);
-
- return this._getRepos(this._filter, this._reposPerPage,
- this._offset);
- }
-
- /**
- * Opens the create overlay if the route has a hash 'create'
- *
- * @param {!Object} params
- */
- _maybeOpenCreateOverlay(params) {
- if (params && params.openCreateModal) {
- this.$.createOverlay.open();
- }
- }
-
- _computeRepoUrl(name) {
- return this.getUrl(this._path + '/', name);
- }
-
- _computeChangesLink(name) {
- return Gerrit.Nav.getUrlForProjectChanges(name);
- }
-
- _getCreateRepoCapability() {
- return this.$.restAPI.getAccount().then(account => {
- if (!account) { return; }
- return this.$.restAPI.getAccountCapabilities(['createProject'])
- .then(capabilities => {
- if (capabilities.createProject) {
- this._createNewCapability = true;
- }
- });
- });
- }
-
- _getRepos(filter, reposPerPage, offset) {
- this._repos = [];
- return this.$.restAPI.getRepos(filter, reposPerPage, offset)
- .then(repos => {
- // Late response.
- if (filter !== this._filter || !repos) { return; }
- this._repos = repos;
- this._loading = false;
- });
- }
-
- _refreshReposList() {
- this.$.restAPI.invalidateReposCache();
- return this._getRepos(this._filter, this._reposPerPage,
- this._offset);
- }
-
- _handleCreateRepo() {
- this.$.createNewModal.handleCreateRepo().then(() => {
- this._refreshReposList();
- });
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
-
- _readOnly(item) {
- return item.state === 'READ_ONLY' ? 'Y' : '';
- }
-
- _computeWeblink(repo) {
- if (!repo.web_links) { return ''; }
- const webLinks = repo.web_links;
- return webLinks.length ? webLinks : null;
- }
}
- customElements.define(GrRepoList.is, GrRepoList);
-})();
+ _computeRepoUrl(name) {
+ return this.getUrl(this._path + '/', name);
+ }
+
+ _computeChangesLink(name) {
+ return Gerrit.Nav.getUrlForProjectChanges(name);
+ }
+
+ _getCreateRepoCapability() {
+ return this.$.restAPI.getAccount().then(account => {
+ if (!account) { return; }
+ return this.$.restAPI.getAccountCapabilities(['createProject'])
+ .then(capabilities => {
+ if (capabilities.createProject) {
+ this._createNewCapability = true;
+ }
+ });
+ });
+ }
+
+ _getRepos(filter, reposPerPage, offset) {
+ this._repos = [];
+ return this.$.restAPI.getRepos(filter, reposPerPage, offset)
+ .then(repos => {
+ // Late response.
+ if (filter !== this._filter || !repos) { return; }
+ this._repos = repos;
+ this._loading = false;
+ });
+ }
+
+ _refreshReposList() {
+ this.$.restAPI.invalidateReposCache();
+ return this._getRepos(this._filter, this._reposPerPage,
+ this._offset);
+ }
+
+ _handleCreateRepo() {
+ this.$.createNewModal.handleCreateRepo().then(() => {
+ this._refreshReposList();
+ });
+ }
+
+ _handleCloseCreate() {
+ this.$.createOverlay.close();
+ }
+
+ _handleCreateClicked() {
+ this.$.createOverlay.open();
+ }
+
+ _readOnly(item) {
+ return item.state === 'READ_ONLY' ? 'Y' : '';
+ }
+
+ _computeWeblink(repo) {
+ if (!repo.web_links) { return ''; }
+ const webLinks = repo.web_links;
+ return webLinks.length ? webLinks : null;
+ }
+}
+
+customElements.define(GrRepoList.is, GrRepoList);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
index 08fd45c..d498869 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
@@ -1,32 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-repo-dialog/gr-create-repo-dialog.html">
-
-<dom-module id="gr-repo-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -47,44 +37,32 @@
white-space:nowrap;
}
</style>
- <gr-list-view
- create-new=[[_createNewCapability]]
- filter="[[_filter]]"
- items-per-page="[[_reposPerPage]]"
- items="[[_repos]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_path]]">
+ <gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items-per-page="[[_reposPerPage]]" items="[[_repos]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]">
<table id="list" class="genericList">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="name topHeader">Repository Name</th>
<th class="repositoryBrowser topHeader">Repository Browser</th>
<th class="changesLink topHeader">Changes</th>
<th class="topHeader readOnly">Read only</th>
<th class="description topHeader">Repository Description</th>
</tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownRepos]]">
<tr class="table">
<td class="name">
- <a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
+ <a href\$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
</td>
<td class="repositoryBrowser">
- <template is="dom-repeat"
- items="[[_computeWeblink(item)]]" as="link">
- <a href$="[[link.url]]"
- class="webLink"
- rel="noopener"
- target="_blank">
+ <template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link">
+ <a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
[[link.name]]
</a>
</template>
</td>
- <td class="changesLink"><a href$="[[_computeChangesLink(item.name)]]">view all</a></td>
+ <td class="changesLink"><a href\$="[[_computeChangesLink(item.name)]]">view all</a></td>
<td class="readOnly">[[_readOnly(item)]]</td>
<td class="description">[[item.description]]</td>
</tr>
@@ -92,26 +70,15 @@
</tbody>
</table>
</gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- id="createDialog"
- class="confirmDialog"
- disabled="[[!_hasNewRepoName]]"
- confirm-label="Create"
- on-confirm="_handleCreateRepo"
- on-cancel="_handleCloseCreate">
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewRepoName]]" confirm-label="Create" on-confirm="_handleCreateRepo" on-cancel="_handleCloseCreate">
<div class="header" slot="header">
Create Repository
</div>
<div class="main" slot="main">
- <gr-create-repo-dialog
- has-new-repo-name="{{_hasNewRepoName}}"
- params="[[params]]"
- id="createNewModal"></gr-create-repo-dialog>
+ <gr-create-repo-dialog has-new-repo-name="{{_hasNewRepoName}}" params="[[params]]" id="createNewModal"></gr-create-repo-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
index 4003b15..fbd1099 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-list</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/page/page.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-repo-list.html">
+<script src="/node_modules/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 type="module" src="./gr-repo-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,166 +41,168 @@
</template>
</test-fixture>
-<script>
- let counter;
- const repoGenerator = () => {
- return {
- id: `test${++counter}`,
- state: 'ACTIVE',
- web_links: [
- {
- name: 'diffusion',
- url: `https://phabricator.example.org/r/project/test${counter}`,
- },
- ],
- };
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-list.js';
+let counter;
+const repoGenerator = () => {
+ return {
+ id: `test${++counter}`,
+ state: 'ACTIVE',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://phabricator.example.org/r/project/test${counter}`,
+ },
+ ],
};
+};
- suite('gr-repo-list tests', async () => {
- await readyToTest();
- let element;
- let repos;
- let sandbox;
- let value;
+suite('gr-repo-list tests', () => {
+ let element;
+ let repos;
+ let sandbox;
+ let value;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(page, 'show');
+ element = fixture('basic');
+ counter = 0;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with repos', () => {
+ setup(done => {
+ repos = _.times(26, repoGenerator);
+ stub('gr-rest-api-interface', {
+ getRepos(num, offset) {
+ return Promise.resolve(repos);
+ },
+ });
+ element._paramsChanged(value).then(() => { flush(done); });
+ });
+
+ test('test for test repo in the list', done => {
+ flush(() => {
+ assert.equal(element._repos[1].id, 'test2');
+ done();
+ });
+ });
+
+ test('_shownRepos', () => {
+ assert.equal(element._shownRepos.length, 25);
+ });
+
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
+ });
+
+ suite('list with less then 25 repos', () => {
+ setup(done => {
+ repos = _.times(25, repoGenerator);
+
+ stub('gr-rest-api-interface', {
+ getRepos(num, offset) {
+ return Promise.resolve(repos);
+ },
+ });
+
+ element._paramsChanged(value).then(() => { flush(done); });
+ });
+
+ test('_shownRepos', () => {
+ assert.equal(element._shownRepos.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ let reposFiltered;
setup(() => {
- sandbox = sinon.sandbox.create();
- sandbox.stub(page, 'show');
- element = fixture('basic');
- counter = 0;
+ repos = _.times(25, repoGenerator);
+ reposFiltered = _.times(1, repoGenerator);
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with repos', () => {
- setup(done => {
- repos = _.times(26, repoGenerator);
- stub('gr-rest-api-interface', {
- getRepos(num, offset) {
- return Promise.resolve(repos);
- },
- });
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('test for test repo in the list', done => {
- flush(() => {
- assert.equal(element._repos[1].id, 'test2');
- done();
- });
- });
-
- test('_shownRepos', () => {
- assert.equal(element._shownRepos.length, 25);
- });
-
- test('_maybeOpenCreateOverlay', () => {
- const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
- element._maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- const params = {};
- element._maybeOpenCreateOverlay(params);
- assert.isFalse(overlayOpen.called);
- params.openCreateModal = true;
- element._maybeOpenCreateOverlay(params);
- assert.isTrue(overlayOpen.called);
+ test('_paramsChanged', done => {
+ sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos));
+ const value = {
+ filter: 'test',
+ offset: 25,
+ };
+ element._paramsChanged(value).then(() => {
+ assert.isTrue(element.$.restAPI.getRepos.lastCall
+ .calledWithExactly('test', 25, 25));
+ done();
});
});
- suite('list with less then 25 repos', () => {
- setup(done => {
- repos = _.times(25, repoGenerator);
+ test('latest repos requested are always set', done => {
+ const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
+ repoStub.withArgs('test').returns(Promise.resolve(repos));
+ repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
+ element._filter = 'test';
- stub('gr-rest-api-interface', {
- getRepos(num, offset) {
- return Promise.resolve(repos);
- },
- });
-
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('_shownRepos', () => {
- assert.equal(element._shownRepos.length, 25);
- });
- });
-
- suite('filter', () => {
- let reposFiltered;
- setup(() => {
- repos = _.times(25, repoGenerator);
- reposFiltered = _.times(1, repoGenerator);
- });
-
- test('_paramsChanged', done => {
- sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos));
- const value = {
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(value).then(() => {
- assert.isTrue(element.$.restAPI.getRepos.lastCall
- .calledWithExactly('test', 25, 25));
- done();
- });
- });
-
- test('latest repos requested are always set', done => {
- const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
- repoStub.withArgs('test').returns(Promise.resolve(repos));
- repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
- element._filter = 'test';
-
- // Repos are not set because the element._filter differs.
- element._getRepos('filter', 25, 0).then(() => {
- assert.deepEqual(element._repos, []);
- done();
- });
- });
- });
-
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._repos = _.times(25, repoGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
- });
-
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sandbox.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').fire('create-clicked');
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sandbox.stub(element.$.createOverlay, 'open');
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateRepo called when confirm fired', () => {
- sandbox.stub(element, '_handleCreateRepo');
- element.$.createDialog.fire('confirm');
- assert.isTrue(element._handleCreateRepo.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sandbox.stub(element, '_handleCloseCreate');
- element.$.createDialog.fire('cancel');
- assert.isTrue(element._handleCloseCreate.called);
+ // Repos are not set because the element._filter differs.
+ element._getRepos('filter', 25, 0).then(() => {
+ assert.deepEqual(element._repos, []);
+ done();
});
});
});
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._repos = _.times(25, repoGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+
+ suite('create new', () => {
+ test('_handleCreateClicked called when create-click fired', () => {
+ sandbox.stub(element, '_handleCreateClicked');
+ element.shadowRoot
+ .querySelector('gr-list-view').fire('create-clicked');
+ assert.isTrue(element._handleCreateClicked.called);
+ });
+
+ test('_handleCreateClicked opens modal', () => {
+ const openStub = sandbox.stub(element.$.createOverlay, 'open');
+ element._handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('_handleCreateRepo called when confirm fired', () => {
+ sandbox.stub(element, '_handleCreateRepo');
+ element.$.createDialog.fire('confirm');
+ assert.isTrue(element._handleCreateRepo.called);
+ });
+
+ test('_handleCloseCreate called when cancel fired', () => {
+ sandbox.stub(element, '_handleCloseCreate');
+ element.$.createDialog.fire('cancel');
+ assert.isTrue(element._handleCloseCreate.called);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
index 7368eb8..826bf93 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -14,128 +14,145 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-input/iron-input.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../gr-plugin-config-array-editor/gr-plugin-config-array-editor.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-repo-plugin-config_html.js';
+
+/**
+ * @appliesMixin Gerrit.RepoPluginConfigMixin
+ * @extends Polymer.Element
+ */
+class GrRepoPluginConfig extends mixinBehaviors( [
+ Gerrit.RepoPluginConfig,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-plugin-config'; }
/**
- * @appliesMixin Gerrit.RepoPluginConfigMixin
- * @extends Polymer.Element
+ * Fired when the plugin config changes.
+ *
+ * @event plugin-config-changed
*/
- class GrRepoPluginConfig extends Polymer.mixinBehaviors( [
- Gerrit.RepoPluginConfig,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-plugin-config'; }
- /**
- * Fired when the plugin config changes.
- *
- * @event plugin-config-changed
- */
- static get properties() {
- return {
- /** @type {?} */
- pluginData: Object,
- /** @type {Array} */
- _pluginConfigOptions: {
- type: Array,
- computed: '_computePluginConfigOptions(pluginData.*)',
- },
- };
- }
-
- _computePluginConfigOptions(dataRecord) {
- if (!dataRecord || !dataRecord.base || !dataRecord.base.config) {
- return [];
- }
- const {config} = dataRecord.base;
- return Object.keys(config)
- .map(_key => { return {_key, info: config[_key]}; });
- }
-
- _isArray(type) {
- return type === this.ENTRY_TYPES.ARRAY;
- }
-
- _isBoolean(type) {
- return type === this.ENTRY_TYPES.BOOLEAN;
- }
-
- _isList(type) {
- return type === this.ENTRY_TYPES.LIST;
- }
-
- _isString(type) {
- // Treat numbers like strings for simplicity.
- return type === this.ENTRY_TYPES.STRING ||
- type === this.ENTRY_TYPES.INT ||
- type === this.ENTRY_TYPES.LONG;
- }
-
- _computeDisabled(editable) {
- return editable === 'false';
- }
-
- /**
- * @param {string} value - fallback to 'false' if undefined
- */
- _computeChecked(value = 'false') {
- return JSON.parse(value);
- }
-
- _handleStringChange(e) {
- const el = Polymer.dom(e).localTarget;
- const _key = el.getAttribute('data-option-key');
- const configChangeInfo =
- this._buildConfigChangeInfo(el.value, _key);
- this._handleChange(configChangeInfo);
- }
-
- _handleListChange(e) {
- const el = Polymer.dom(e).localTarget;
- const _key = el.getAttribute('data-option-key');
- const configChangeInfo =
- this._buildConfigChangeInfo(el.value, _key);
- this._handleChange(configChangeInfo);
- }
-
- _handleBooleanChange(e) {
- const el = Polymer.dom(e).localTarget;
- const _key = el.getAttribute('data-option-key');
- const configChangeInfo =
- this._buildConfigChangeInfo(JSON.stringify(el.checked), _key);
- this._handleChange(configChangeInfo);
- }
-
- _buildConfigChangeInfo(value, _key) {
- const info = this.pluginData.config[_key];
- info.value = value;
- return {
- _key,
- info,
- notifyPath: `${_key}.value`,
- };
- }
-
- _handleArrayChange({detail}) {
- this._handleChange(detail);
- }
-
- _handleChange({_key, info, notifyPath}) {
- const {name, config} = this.pluginData;
-
- /** @type {Object} */
- const detail = {
- name,
- config: Object.assign(config, {[_key]: info}, {}),
- notifyPath: `${name}.${notifyPath}`,
- };
-
- this.dispatchEvent(new CustomEvent(
- this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ pluginData: Object,
+ /** @type {Array} */
+ _pluginConfigOptions: {
+ type: Array,
+ computed: '_computePluginConfigOptions(pluginData.*)',
+ },
+ };
}
- customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig);
-})();
+ _computePluginConfigOptions(dataRecord) {
+ if (!dataRecord || !dataRecord.base || !dataRecord.base.config) {
+ return [];
+ }
+ const {config} = dataRecord.base;
+ return Object.keys(config)
+ .map(_key => { return {_key, info: config[_key]}; });
+ }
+
+ _isArray(type) {
+ return type === this.ENTRY_TYPES.ARRAY;
+ }
+
+ _isBoolean(type) {
+ return type === this.ENTRY_TYPES.BOOLEAN;
+ }
+
+ _isList(type) {
+ return type === this.ENTRY_TYPES.LIST;
+ }
+
+ _isString(type) {
+ // Treat numbers like strings for simplicity.
+ return type === this.ENTRY_TYPES.STRING ||
+ type === this.ENTRY_TYPES.INT ||
+ type === this.ENTRY_TYPES.LONG;
+ }
+
+ _computeDisabled(editable) {
+ return editable === 'false';
+ }
+
+ /**
+ * @param {string} value - fallback to 'false' if undefined
+ */
+ _computeChecked(value = 'false') {
+ return JSON.parse(value);
+ }
+
+ _handleStringChange(e) {
+ const el = dom(e).localTarget;
+ const _key = el.getAttribute('data-option-key');
+ const configChangeInfo =
+ this._buildConfigChangeInfo(el.value, _key);
+ this._handleChange(configChangeInfo);
+ }
+
+ _handleListChange(e) {
+ const el = dom(e).localTarget;
+ const _key = el.getAttribute('data-option-key');
+ const configChangeInfo =
+ this._buildConfigChangeInfo(el.value, _key);
+ this._handleChange(configChangeInfo);
+ }
+
+ _handleBooleanChange(e) {
+ const el = dom(e).localTarget;
+ const _key = el.getAttribute('data-option-key');
+ const configChangeInfo =
+ this._buildConfigChangeInfo(JSON.stringify(el.checked), _key);
+ this._handleChange(configChangeInfo);
+ }
+
+ _buildConfigChangeInfo(value, _key) {
+ const info = this.pluginData.config[_key];
+ info.value = value;
+ return {
+ _key,
+ info,
+ notifyPath: `${_key}.value`,
+ };
+ }
+
+ _handleArrayChange({detail}) {
+ this._handleChange(detail);
+ }
+
+ _handleChange({_key, info, notifyPath}) {
+ const {name, config} = this.pluginData;
+
+ /** @type {Object} */
+ const detail = {
+ name,
+ config: Object.assign(config, {[_key]: info}, {}),
+ notifyPath: `${name}.${notifyPath}`,
+ };
+
+ this.dispatchEvent(new CustomEvent(
+ this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
+ }
+}
+
+customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
index ef5b755..fa4617d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
@@ -1,35 +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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-
-<link rel="import" href="../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../gr-plugin-config-array-editor/gr-plugin-config-array-editor.html">
-
-<dom-module id="gr-repo-plugin-config">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -53,55 +40,31 @@
<fieldset>
<h4>[[pluginData.name]]</h4>
<template is="dom-repeat" items="[[_pluginConfigOptions]]" as="option">
- <section class$="section [[option.info.type]]">
+ <section class\$="section [[option.info.type]]">
<span class="title">
- <gr-tooltip-content
- has-tooltip="[[option.info.description]]"
- show-icon="[[option.info.description]]"
- title="[[option.info.description]]">
+ <gr-tooltip-content has-tooltip="[[option.info.description]]" show-icon="[[option.info.description]]" title="[[option.info.description]]">
<span>[[option.info.display_name]]</span>
</gr-tooltip-content>
</span>
<span class="value">
<template is="dom-if" if="[[_isArray(option.info.type)]]">
- <gr-plugin-config-array-editor
- on-plugin-config-option-changed="_handleArrayChange"
- plugin-option="[[option]]"></gr-plugin-config-array-editor>
+ <gr-plugin-config-array-editor on-plugin-config-option-changed="_handleArrayChange" plugin-option="[[option]]"></gr-plugin-config-array-editor>
</template>
<template is="dom-if" if="[[_isBoolean(option.info.type)]]">
- <paper-toggle-button
- checked="[[_computeChecked(option.info.value)]]"
- on-change="_handleBooleanChange"
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button>
+ <paper-toggle-button checked="[[_computeChecked(option.info.value)]]" on-change="_handleBooleanChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button>
</template>
<template is="dom-if" if="[[_isList(option.info.type)]]">
- <gr-select
- bind-value$="[[option.info.value]]"
- on-change="_handleListChange">
- <select
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]">
- <template is="dom-repeat"
- items="[[option.info.permitted_values]]"
- as="value">
- <option value$="[[value]]">[[value]]</option>
+ <gr-select bind-value\$="[[option.info.value]]" on-change="_handleListChange">
+ <select data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
+ <template is="dom-repeat" items="[[option.info.permitted_values]]" as="value">
+ <option value\$="[[value]]">[[value]]</option>
</template>
</select>
</gr-select>
</template>
<template is="dom-if" if="[[_isString(option.info.type)]]">
- <iron-input
- bind-value="[[option.info.value]]"
- on-input="_handleStringChange"
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]">
- <input
- is="iron-input"
- value="[[option.info.value]]"
- on-input="_handleStringChange"
- data-option-key$="[[option._key]]"
- disabled$="[[_computeDisabled(option.info.editable)]]">
+ <iron-input bind-value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
+ <input is="iron-input" value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
</iron-input>
</template>
<template is="dom-if" if="[[option.info.inherited_value]]">
@@ -114,6 +77,4 @@
</template>
</fieldset>
</div>
- </template>
- <script src="gr-repo-plugin-config.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
index 8313edf..f70d3ea 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_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-repo-plugin-config</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-repo-plugin-config.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-repo-plugin-config.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-plugin-config.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,150 +40,152 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-plugin-config 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-repo-plugin-config.js';
+suite('gr-repo-plugin-config tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('_computePluginConfigOptions', () => {
+ assert.deepEqual(element._computePluginConfigOptions(), []);
+ assert.deepEqual(element._computePluginConfigOptions({}), []);
+ assert.deepEqual(element._computePluginConfigOptions({base: {}}), []);
+ assert.deepEqual(element._computePluginConfigOptions(
+ {base: {config: {}}}), []);
+ assert.deepEqual(element._computePluginConfigOptions(
+ {base: {config: {testKey: 'testInfo'}}}),
+ [{_key: 'testKey', info: 'testInfo'}]);
+ });
+
+ test('_computeDisabled', () => {
+ assert.isFalse(element._computeDisabled('true'));
+ assert.isTrue(element._computeDisabled('false'));
+ });
+
+ test('_handleChange', () => {
+ const eventStub = sandbox.stub(element, 'dispatchEvent');
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test'}},
+ };
+ element._handleChange({
+ _key: 'plugin',
+ info: {value: 'newTest'},
+ notifyPath: 'plugin.value',
+ });
+
+ assert.isTrue(eventStub.called);
+
+ const {detail} = eventStub.lastCall.args[0];
+ assert.equal(detail.name, 'testName');
+ assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
+ assert.equal(detail.notifyPath, 'testName.plugin.value');
+ });
+
+ suite('option types', () => {
+ let changeStub;
+ let buildStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ changeStub = sandbox.stub(element, '_handleChange');
+ buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
});
- teardown(() => sandbox.restore());
-
- test('_computePluginConfigOptions', () => {
- assert.deepEqual(element._computePluginConfigOptions(), []);
- assert.deepEqual(element._computePluginConfigOptions({}), []);
- assert.deepEqual(element._computePluginConfigOptions({base: {}}), []);
- assert.deepEqual(element._computePluginConfigOptions(
- {base: {config: {}}}), []);
- assert.deepEqual(element._computePluginConfigOptions(
- {base: {config: {testKey: 'testInfo'}}}),
- [{_key: 'testKey', info: 'testInfo'}]);
- });
-
- test('_computeDisabled', () => {
- assert.isFalse(element._computeDisabled('true'));
- assert.isTrue(element._computeDisabled('false'));
- });
-
- test('_handleChange', () => {
- const eventStub = sandbox.stub(element, 'dispatchEvent');
+ test('ARRAY type option', () => {
element.pluginData = {
name: 'testName',
- config: {plugin: {value: 'test'}},
+ config: {plugin: {value: 'test', type: 'ARRAY'}},
};
- element._handleChange({
- _key: 'plugin',
- info: {value: 'newTest'},
- notifyPath: 'plugin.value',
- });
+ flushAsynchronousOperations();
- assert.isTrue(eventStub.called);
-
- const {detail} = eventStub.lastCall.args[0];
- assert.equal(detail.name, 'testName');
- assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
- assert.equal(detail.notifyPath, 'testName.plugin.value');
+ const editor = element.shadowRoot
+ .querySelector('gr-plugin-config-array-editor');
+ assert.ok(editor);
+ element._handleArrayChange({detail: 'test'});
+ assert.isTrue(changeStub.called);
+ assert.equal(changeStub.lastCall.args[0], 'test');
});
- suite('option types', () => {
- let changeStub;
- let buildStub;
-
- setup(() => {
- changeStub = sandbox.stub(element, '_handleChange');
- buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
- });
-
- test('ARRAY type option', () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'ARRAY'}},
- };
- flushAsynchronousOperations();
-
- const editor = element.shadowRoot
- .querySelector('gr-plugin-config-array-editor');
- assert.ok(editor);
- element._handleArrayChange({detail: 'test'});
- assert.isTrue(changeStub.called);
- assert.equal(changeStub.lastCall.args[0], 'test');
- });
-
- test('BOOLEAN type option', () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'true', type: 'BOOLEAN'}},
- };
- flushAsynchronousOperations();
-
- const toggle = element.shadowRoot
- .querySelector('paper-toggle-button');
- assert.ok(toggle);
- toggle.click();
- flushAsynchronousOperations();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
-
- test('INT/LONG/STRING type option', () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'STRING'}},
- };
- flushAsynchronousOperations();
-
- const input = element.shadowRoot
- .querySelector('input');
- assert.ok(input);
- input.value = 'newTest';
- input.dispatchEvent(new Event('input'));
- flushAsynchronousOperations();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
-
- test('LIST type option', () => {
- const permitted_values = ['test', 'newTest'];
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
- };
- flushAsynchronousOperations();
-
- const select = element.shadowRoot
- .querySelector('select');
- assert.ok(select);
- select.value = 'newTest';
- select.dispatchEvent(new Event(
- 'change', {bubbles: true, composed: true}));
- flushAsynchronousOperations();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
- });
-
- test('_buildConfigChangeInfo', () => {
+ test('BOOLEAN type option', () => {
element.pluginData = {
name: 'testName',
- config: {plugin: {value: 'test'}},
+ config: {plugin: {value: 'true', type: 'BOOLEAN'}},
};
- const detail = element._buildConfigChangeInfo('newTest', 'plugin');
- assert.equal(detail._key, 'plugin');
- assert.deepEqual(detail.info, {value: 'newTest'});
- assert.equal(detail.notifyPath, 'plugin.value');
+ flushAsynchronousOperations();
+
+ const toggle = element.shadowRoot
+ .querySelector('paper-toggle-button');
+ assert.ok(toggle);
+ toggle.click();
+ flushAsynchronousOperations();
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
+
+ assert.isTrue(changeStub.called);
+ });
+
+ test('INT/LONG/STRING type option', () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test', type: 'STRING'}},
+ };
+ flushAsynchronousOperations();
+
+ const input = element.shadowRoot
+ .querySelector('input');
+ assert.ok(input);
+ input.value = 'newTest';
+ input.dispatchEvent(new Event('input'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+ assert.isTrue(changeStub.called);
+ });
+
+ test('LIST type option', () => {
+ const permitted_values = ['test', 'newTest'];
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
+ };
+ flushAsynchronousOperations();
+
+ const select = element.shadowRoot
+ .querySelector('select');
+ assert.ok(select);
+ select.value = 'newTest';
+ select.dispatchEvent(new Event(
+ 'change', {bubbles: true, composed: true}));
+ flushAsynchronousOperations();
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+ assert.isTrue(changeStub.called);
});
});
+
+ test('_buildConfigChangeInfo', () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {plugin: {value: 'test'}},
+ };
+ const detail = element._buildConfigChangeInfo('newTest', 'plugin');
+ assert.equal(detail._key, 'plugin');
+ assert.deepEqual(detail.info, {value: 'newTest'});
+ assert.equal(detail.notifyPath, 'plugin.value');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index f6328de..fd85f1a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -14,348 +14,366 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const STATES = {
- active: {value: 'ACTIVE', label: 'Active'},
- readOnly: {value: 'READ_ONLY', label: 'Read Only'},
- hidden: {value: 'HIDDEN', label: 'Hidden'},
- };
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-download-commands/gr-download-commands.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-subpage-styles.js';
+import '../../../styles/shared-styles.js';
+import '../gr-repo-plugin-config/gr-repo-plugin-config.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-repo_html.js';
- const SUBMIT_TYPES = {
- // Exclude INHERIT, which is handled specially.
- mergeIfNecessary: {
- value: 'MERGE_IF_NECESSARY',
- label: 'Merge if necessary',
- },
- fastForwardOnly: {
- value: 'FAST_FORWARD_ONLY',
- label: 'Fast forward only',
- },
- rebaseAlways: {
- value: 'REBASE_ALWAYS',
- label: 'Rebase Always',
- },
- rebaseIfNecessary: {
- value: 'REBASE_IF_NECESSARY',
- label: 'Rebase if necessary',
- },
- mergeAlways: {
- value: 'MERGE_ALWAYS',
- label: 'Merge always',
- },
- cherryPick: {
- value: 'CHERRY_PICK',
- label: 'Cherry pick',
- },
- };
+const STATES = {
+ active: {value: 'ACTIVE', label: 'Active'},
+ readOnly: {value: 'READ_ONLY', label: 'Read Only'},
+ hidden: {value: 'HIDDEN', label: 'Hidden'},
+};
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrRepo extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo'; }
+const SUBMIT_TYPES = {
+ // Exclude INHERIT, which is handled specially.
+ mergeIfNecessary: {
+ value: 'MERGE_IF_NECESSARY',
+ label: 'Merge if necessary',
+ },
+ fastForwardOnly: {
+ value: 'FAST_FORWARD_ONLY',
+ label: 'Fast forward only',
+ },
+ rebaseAlways: {
+ value: 'REBASE_ALWAYS',
+ label: 'Rebase Always',
+ },
+ rebaseIfNecessary: {
+ value: 'REBASE_IF_NECESSARY',
+ label: 'Rebase if necessary',
+ },
+ mergeAlways: {
+ value: 'MERGE_ALWAYS',
+ label: 'Merge always',
+ },
+ cherryPick: {
+ value: 'CHERRY_PICK',
+ label: 'Cherry pick',
+ },
+};
- static get properties() {
- return {
- params: Object,
- repo: String,
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRepo extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _configChanged: {
- type: Boolean,
- value: false,
+ static get is() { return 'gr-repo'; }
+
+ static get properties() {
+ return {
+ params: Object,
+ repo: String,
+
+ _configChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ observer: '_loggedInChanged',
+ },
+ /** @type {?} */
+ _repoConfig: Object,
+ /** @type {?} */
+ _pluginData: {
+ type: Array,
+ computed: '_computePluginData(_repoConfig.plugin_config.*)',
+ },
+ _readOnly: {
+ type: Boolean,
+ value: true,
+ },
+ _states: {
+ type: Array,
+ value() {
+ return Object.values(STATES);
},
- _loading: {
- type: Boolean,
- value: true,
+ },
+ _submitTypes: {
+ type: Array,
+ value() {
+ return Object.values(SUBMIT_TYPES);
},
- _loggedIn: {
- type: Boolean,
- value: false,
- observer: '_loggedInChanged',
- },
- /** @type {?} */
- _repoConfig: Object,
- /** @type {?} */
- _pluginData: {
- type: Array,
- computed: '_computePluginData(_repoConfig.plugin_config.*)',
- },
- _readOnly: {
- type: Boolean,
- value: true,
- },
- _states: {
- type: Array,
- value() {
- return Object.values(STATES);
- },
- },
- _submitTypes: {
- type: Array,
- value() {
- return Object.values(SUBMIT_TYPES);
- },
- },
- _schemes: {
- type: Array,
- value() { return []; },
- computed: '_computeSchemes(_schemesObj)',
- observer: '_schemesChanged',
- },
- _selectedCommand: {
- type: String,
- value: 'Clone',
- },
- _selectedScheme: String,
- _schemesObj: Object,
- };
- }
+ },
+ _schemes: {
+ type: Array,
+ value() { return []; },
+ computed: '_computeSchemes(_schemesObj)',
+ observer: '_schemesChanged',
+ },
+ _selectedCommand: {
+ type: String,
+ value: 'Clone',
+ },
+ _selectedScheme: String,
+ _schemesObj: Object,
+ };
+ }
- static get observers() {
- return [
- '_handleConfigChanged(_repoConfig.*)',
- ];
- }
+ static get observers() {
+ return [
+ '_handleConfigChanged(_repoConfig.*)',
+ ];
+ }
- /** @override */
- attached() {
- super.attached();
- this._loadRepo();
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadRepo();
- this.fire('title-change', {title: this.repo});
- }
+ this.fire('title-change', {title: this.repo});
+ }
- _computePluginData(configRecord) {
- if (!configRecord ||
- !configRecord.base) { return []; }
+ _computePluginData(configRecord) {
+ if (!configRecord ||
+ !configRecord.base) { return []; }
- const pluginConfig = configRecord.base;
- return Object.keys(pluginConfig)
- .map(name => { return {name, config: pluginConfig[name]}; });
- }
+ const pluginConfig = configRecord.base;
+ return Object.keys(pluginConfig)
+ .map(name => { return {name, config: pluginConfig[name]}; });
+ }
- _loadRepo() {
- if (!this.repo) { return Promise.resolve(); }
+ _loadRepo() {
+ if (!this.repo) { return Promise.resolve(); }
- const promises = [];
+ const promises = [];
- const errFn = response => {
- this.fire('page-error', {response});
- };
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
- promises.push(this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this.$.restAPI.getRepoAccess(this.repo).then(access => {
- if (!access) { return Promise.resolve(); }
+ promises.push(this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this.$.restAPI.getRepoAccess(this.repo).then(access => {
+ if (!access) { return Promise.resolve(); }
- // If the user is not an owner, is_owner is not a property.
- this._readOnly = !access[this.repo].is_owner;
- });
- }
- }));
-
- promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn)
- .then(config => {
- if (!config) { return Promise.resolve(); }
-
- if (config.default_submit_type) {
- // The gr-select is bound to submit_type, which needs to be the
- // *configured* submit type. When default_submit_type is
- // present, the server reports the *effective* submit type in
- // submit_type, so we need to overwrite it before storing the
- // config in this.
- config.submit_type =
- config.default_submit_type.configured_value;
- }
- if (!config.state) {
- config.state = STATES.active.value;
- }
- this._repoConfig = config;
- this._loading = false;
- }));
-
- promises.push(this.$.restAPI.getConfig().then(config => {
- if (!config) { return Promise.resolve(); }
-
- this._schemesObj = config.download.schemes;
- }));
-
- return Promise.all(promises);
- }
-
- _computeLoadingClass(loading) {
- return loading ? 'loading' : '';
- }
-
- _computeHideClass(arr) {
- return !arr || !arr.length ? 'hide' : '';
- }
-
- _loggedInChanged(_loggedIn) {
- if (!_loggedIn) { return; }
- this.$.restAPI.getPreferences().then(prefs => {
- if (prefs.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this._selectedScheme = prefs.download_scheme.toLowerCase();
- }
- });
- }
-
- _formatBooleanSelect(item) {
- if (!item) { return; }
- let inheritLabel = 'Inherit';
- if (!(item.inherited_value === undefined)) {
- inheritLabel = `Inherit (${item.inherited_value})`;
- }
- return [
- {
- label: inheritLabel,
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ];
- }
-
- _formatSubmitTypeSelect(projectConfig) {
- if (!projectConfig) { return; }
- const allValues = Object.values(SUBMIT_TYPES);
- const type = projectConfig.default_submit_type;
- if (!type) {
- // Server is too old to report default_submit_type, so assume INHERIT
- // is not a valid value.
- return allValues;
- }
-
- let inheritLabel = 'Inherit';
- if (type.inherited_value) {
- let inherited = type.inherited_value;
- for (const val of allValues) {
- if (val.value === type.inherited_value) {
- inherited = val.label;
- break;
- }
- }
- inheritLabel = `Inherit (${inherited})`;
- }
- return [
- {
- label: inheritLabel,
- value: 'INHERIT',
- },
- ...allValues,
- ];
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _formatRepoConfigForSave(repoConfig) {
- const configInputObj = {};
- for (const key in repoConfig) {
- if (repoConfig.hasOwnProperty(key)) {
- if (key === 'default_submit_type') {
- // default_submit_type is not in the input type, and the
- // configured value was already copied to submit_type by
- // _loadProject. Omit this property when saving.
- continue;
- }
- if (key === 'plugin_config') {
- configInputObj.plugin_config_values = repoConfig[key];
- } else if (typeof repoConfig[key] === 'object') {
- configInputObj[key] = repoConfig[key].configured_value;
- } else {
- configInputObj[key] = repoConfig[key];
- }
- }
- }
- return configInputObj;
- }
-
- _handleSaveRepoConfig() {
- return this.$.restAPI.saveRepoConfig(this.repo,
- this._formatRepoConfigForSave(this._repoConfig)).then(() => {
- this._configChanged = false;
- });
- }
-
- _handleConfigChanged() {
- if (this._isLoading()) { return; }
- this._configChanged = true;
- }
-
- _computeButtonDisabled(readOnly, configChanged) {
- return readOnly || !configChanged;
- }
-
- _computeHeaderClass(configChanged) {
- return configChanged ? 'edited' : '';
- }
-
- _computeSchemes(schemesObj) {
- return Object.keys(schemesObj);
- }
-
- _schemesChanged(schemes) {
- if (schemes.length === 0) { return; }
- if (!schemes.includes(this._selectedScheme)) {
- this._selectedScheme = schemes.sort()[0];
- }
- }
-
- _computeCommands(repo, schemesObj, _selectedScheme) {
- if (!schemesObj || !repo || !_selectedScheme) {
- return [];
- }
- const commands = [];
- let commandObj;
- if (schemesObj.hasOwnProperty(_selectedScheme)) {
- commandObj = schemesObj[_selectedScheme].clone_commands;
- }
- for (const title in commandObj) {
- if (!commandObj.hasOwnProperty(title)) { continue; }
- commands.push({
- title,
- command: commandObj[title]
- .replace(/\$\{project\}/gi, encodeURI(repo))
- .replace(/\$\{project-base-name\}/gi,
- encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
+ // If the user is not an owner, is_owner is not a property.
+ this._readOnly = !access[this.repo].is_owner;
});
}
- return commands;
+ }));
+
+ promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn)
+ .then(config => {
+ if (!config) { return Promise.resolve(); }
+
+ if (config.default_submit_type) {
+ // The gr-select is bound to submit_type, which needs to be the
+ // *configured* submit type. When default_submit_type is
+ // present, the server reports the *effective* submit type in
+ // submit_type, so we need to overwrite it before storing the
+ // config in this.
+ config.submit_type =
+ config.default_submit_type.configured_value;
+ }
+ if (!config.state) {
+ config.state = STATES.active.value;
+ }
+ this._repoConfig = config;
+ this._loading = false;
+ }));
+
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ if (!config) { return Promise.resolve(); }
+
+ this._schemesObj = config.download.schemes;
+ }));
+
+ return Promise.all(promises);
+ }
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ }
+
+ _computeHideClass(arr) {
+ return !arr || !arr.length ? 'hide' : '';
+ }
+
+ _loggedInChanged(_loggedIn) {
+ if (!_loggedIn) { return; }
+ this.$.restAPI.getPreferences().then(prefs => {
+ if (prefs.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this._selectedScheme = prefs.download_scheme.toLowerCase();
+ }
+ });
+ }
+
+ _formatBooleanSelect(item) {
+ if (!item) { return; }
+ let inheritLabel = 'Inherit';
+ if (!(item.inherited_value === undefined)) {
+ inheritLabel = `Inherit (${item.inherited_value})`;
+ }
+ return [
+ {
+ label: inheritLabel,
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ];
+ }
+
+ _formatSubmitTypeSelect(projectConfig) {
+ if (!projectConfig) { return; }
+ const allValues = Object.values(SUBMIT_TYPES);
+ const type = projectConfig.default_submit_type;
+ if (!type) {
+ // Server is too old to report default_submit_type, so assume INHERIT
+ // is not a valid value.
+ return allValues;
}
- _computeRepositoriesClass(config) {
- return config ? 'showConfig': '';
+ let inheritLabel = 'Inherit';
+ if (type.inherited_value) {
+ let inherited = type.inherited_value;
+ for (const val of allValues) {
+ if (val.value === type.inherited_value) {
+ inherited = val.label;
+ break;
+ }
+ }
+ inheritLabel = `Inherit (${inherited})`;
}
+ return [
+ {
+ label: inheritLabel,
+ value: 'INHERIT',
+ },
+ ...allValues,
+ ];
+ }
- _computeChangesUrl(name) {
- return Gerrit.Nav.getUrlForProjectChanges(name);
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _formatRepoConfigForSave(repoConfig) {
+ const configInputObj = {};
+ for (const key in repoConfig) {
+ if (repoConfig.hasOwnProperty(key)) {
+ if (key === 'default_submit_type') {
+ // default_submit_type is not in the input type, and the
+ // configured value was already copied to submit_type by
+ // _loadProject. Omit this property when saving.
+ continue;
+ }
+ if (key === 'plugin_config') {
+ configInputObj.plugin_config_values = repoConfig[key];
+ } else if (typeof repoConfig[key] === 'object') {
+ configInputObj[key] = repoConfig[key].configured_value;
+ } else {
+ configInputObj[key] = repoConfig[key];
+ }
+ }
}
+ return configInputObj;
+ }
- _handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
- this._repoConfig.plugin_config[name] = config;
- this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+ _handleSaveRepoConfig() {
+ return this.$.restAPI.saveRepoConfig(this.repo,
+ this._formatRepoConfigForSave(this._repoConfig)).then(() => {
+ this._configChanged = false;
+ });
+ }
+
+ _handleConfigChanged() {
+ if (this._isLoading()) { return; }
+ this._configChanged = true;
+ }
+
+ _computeButtonDisabled(readOnly, configChanged) {
+ return readOnly || !configChanged;
+ }
+
+ _computeHeaderClass(configChanged) {
+ return configChanged ? 'edited' : '';
+ }
+
+ _computeSchemes(schemesObj) {
+ return Object.keys(schemesObj);
+ }
+
+ _schemesChanged(schemes) {
+ if (schemes.length === 0) { return; }
+ if (!schemes.includes(this._selectedScheme)) {
+ this._selectedScheme = schemes.sort()[0];
}
}
- customElements.define(GrRepo.is, GrRepo);
-})();
+ _computeCommands(repo, schemesObj, _selectedScheme) {
+ if (!schemesObj || !repo || !_selectedScheme) {
+ return [];
+ }
+ const commands = [];
+ let commandObj;
+ if (schemesObj.hasOwnProperty(_selectedScheme)) {
+ commandObj = schemesObj[_selectedScheme].clone_commands;
+ }
+ for (const title in commandObj) {
+ if (!commandObj.hasOwnProperty(title)) { continue; }
+ commands.push({
+ title,
+ command: commandObj[title]
+ .replace(/\$\{project\}/gi, encodeURI(repo))
+ .replace(/\$\{project-base-name\}/gi,
+ encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
+ });
+ }
+ return commands;
+ }
+
+ _computeRepositoriesClass(config) {
+ return config ? 'showConfig': '';
+ }
+
+ _computeChangesUrl(name) {
+ return Gerrit.Nav.getUrlForProjectChanges(name);
+ }
+
+ _handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
+ this._repoConfig.plugin_config[name] = config;
+ this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+ }
+}
+
+customElements.define(GrRepo.is, GrRepo);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
index 5e37261..2c7540f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
@@ -1,37 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-subpage-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-repo-plugin-config/gr-repo-plugin-config.html">
-
-<dom-module id="gr-repo">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -65,50 +50,37 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<div class="info">
- <h1 id="Title" class$="name">
+ <h1 id="Title" class\$="name">
[[repo]]
- <hr/>
+ <hr>
</h1>
<div>
- <a href$="[[_computeChangesUrl(repo)]]">(view changes)</a>
+ <a href\$="[[_computeChangesUrl(repo)]]">(view changes)</a>
</div>
</div>
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
+ <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div>
+ <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
+ <div id="downloadContent" class\$="[[_computeHideClass(_schemes)]]">
<h2 id="download">Download</h2>
<fieldset>
- <gr-download-commands
- id="downloadCommands"
- commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]"
- schemes="[[_schemes]]"
- selected-scheme="{{_selectedScheme}}"></gr-download-commands>
+ <gr-download-commands id="downloadCommands" commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]" schemes="[[_schemes]]" selected-scheme="{{_selectedScheme}}"></gr-download-commands>
</fieldset>
</div>
- <h2 id="configurations"
- class$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2>
+ <h2 id="configurations" class\$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2>
<div id="form">
<fieldset>
<h3 id="Description">Description</h3>
<fieldset>
- <iron-autogrow-textarea
- id="descriptionInput"
- class="description"
- autocomplete="on"
- placeholder="<Insert repo description here>"
- bind-value="{{_repoConfig.description}}"
- disabled$="[[_readOnly]]"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="descriptionInput" class="description" autocomplete="on" placeholder="<Insert repo description here>" bind-value="{{_repoConfig.description}}" disabled\$="[[_readOnly]]"></iron-autogrow-textarea>
</fieldset>
<h3 id="Options">Repository Options</h3>
<fieldset id="options">
<section>
<span class="title">State</span>
<span class="value">
- <gr-select
- id="stateSelect"
- bind-value="{{_repoConfig.state}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat" items=[[_states]]>
+ <gr-select id="stateSelect" bind-value="{{_repoConfig.state}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_states]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -118,12 +90,9 @@
<section>
<span class="title">Submit type</span>
<span class="value">
- <gr-select
- id="submitTypeSelect"
- bind-value="{{_repoConfig.submit_type}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatSubmitTypeSelect(_repoConfig)]]">
+ <gr-select id="submitTypeSelect" bind-value="{{_repoConfig.submit_type}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatSubmitTypeSelect(_repoConfig)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -133,12 +102,9 @@
<section>
<span class="title">Allow content merges</span>
<span class="value">
- <gr-select
- id="contentMergeSelect"
- bind-value="{{_repoConfig.use_content_merge.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]">
+ <gr-select id="contentMergeSelect" bind-value="{{_repoConfig.use_content_merge.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -150,12 +116,9 @@
Create a new change for every commit not in the target branch
</span>
<span class="value">
- <gr-select
- id="newChangeSelect"
- bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]">
+ <gr-select id="newChangeSelect" bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -165,46 +128,33 @@
<section>
<span class="title">Require Change-Id in commit message</span>
<span class="value">
- <gr-select
- id="requireChangeIdSelect"
- bind-value="{{_repoConfig.require_change_id.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
+ <gr-select id="requireChangeIdSelect" bind-value="{{_repoConfig.require_change_id.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
</gr-select>
</span>
</section>
- <section
- id="enableSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
+ <section id="enableSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
<span class="title">Enable signed push</span>
<span class="value">
- <gr-select
- id="enableSignedPush"
- bind-value="{{_repoConfig.enable_signed_push.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]">
+ <gr-select id="enableSignedPush" bind-value="{{_repoConfig.enable_signed_push.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
</gr-select>
</span>
</section>
- <section
- id="requireSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]">
+ <section id="requireSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]">
<span class="title">Require signed push</span>
<span class="value">
- <gr-select
- id="requireSignedPush"
- bind-value="{{_repoConfig.require_signed_push.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]">
+ <gr-select id="requireSignedPush" bind-value="{{_repoConfig.require_signed_push.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -215,12 +165,9 @@
<span class="title">
Reject implicit merges when changes are pushed for review</span>
<span class="value">
- <gr-select
- id="rejectImplicitMergesSelect"
- bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]">
+ <gr-select id="rejectImplicitMergesSelect" bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -231,12 +178,9 @@
<span class="title">
Enable adding unregistered users as reviewers and CCs on changes</span>
<span class="value">
- <gr-select
- id="unRegisteredCcSelect"
- bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]">
+ <gr-select id="unRegisteredCcSelect" bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -247,12 +191,9 @@
<span class="title">
Set all new changes private by default</span>
<span class="value">
- <gr-select
- id="setAllnewChangesPrivateByDefaultSelect"
- bind-value="{{_repoConfig.private_by_default.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]">
+ <gr-select id="setAllnewChangesPrivateByDefaultSelect" bind-value="{{_repoConfig.private_by_default.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -263,12 +204,9 @@
<span class="title">
Set new changes to "work in progress" by default</span>
<span class="value">
- <gr-select
- id="setAllNewChangesWorkInProgressByDefaultSelect"
- bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
+ <gr-select id="setAllNewChangesWorkInProgressByDefaultSelect" bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -278,17 +216,8 @@
<section>
<span class="title">Maximum Git object size limit</span>
<span class="value">
- <iron-input
- id="maxGitObjSizeIronInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- type="text"
- disabled$="[[_readOnly]]">
- <input
- id="maxGitObjSizeInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- is="iron-input"
- type="text"
- disabled$="[[_readOnly]]">
+ <iron-input id="maxGitObjSizeIronInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" type="text" disabled\$="[[_readOnly]]">
+ <input id="maxGitObjSizeInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" is="iron-input" type="text" disabled\$="[[_readOnly]]">
</iron-input>
<template is="dom-if" if="[[_repoConfig.max_object_size_limit.value]]">
effective: [[_repoConfig.max_object_size_limit.value]] bytes
@@ -298,12 +227,9 @@
<section>
<span class="title">Match authored date with committer date upon submit</span>
<span class="value">
- <gr-select
- id="matchAuthoredDateWithCommitterDateSelect"
- bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]">
+ <gr-select id="matchAuthoredDateWithCommitterDateSelect" bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -313,12 +239,9 @@
<section>
<span class="title">Reject empty commit upon submit</span>
<span class="value">
- <gr-select
- id="rejectEmptyCommitSelect"
- bind-value="{{_repoConfig.reject_empty_commit.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]">
+ <gr-select id="rejectEmptyCommitSelect" bind-value="{{_repoConfig.reject_empty_commit.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -332,12 +255,9 @@
<span class="title">
Require a valid contributor agreement to upload</span>
<span class="value">
- <gr-select
- id="contributorAgreementSelect"
- bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]">
+ <gr-select id="contributorAgreementSelect" bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -347,12 +267,9 @@
<section>
<span class="title">Require Signed-off-by in commit message</span>
<span class="value">
- <gr-select
- id="useSignedOffBySelect"
- bind-value="{{_repoConfig.use_signed_off_by.configured_value}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]">
+ <gr-select id="useSignedOffBySelect" bind-value="{{_repoConfig.use_signed_off_by.configured_value}}">
+ <select disabled\$="[[_readOnly]]">
+ <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@@ -360,18 +277,13 @@
</span>
</section>
</fieldset>
- <div
- class$="pluginConfig [[_computeHideClass(_pluginData)]]"
- on-plugin-config-changed="_handlePluginConfigChanged">
+ <div class\$="pluginConfig [[_computeHideClass(_pluginData)]]" on-plugin-config-changed="_handlePluginConfigChanged">
<h3>Plugins</h3>
<template is="dom-repeat" items="[[_pluginData]]" as="data">
- <gr-repo-plugin-config
- plugin-data="[[data]]"></gr-repo-plugin-config>
+ <gr-repo-plugin-config plugin-data="[[data]]"></gr-repo-plugin-config>
</template>
</div>
- <gr-button
- on-click="_handleSaveRepoConfig"
- disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
+ <gr-button on-click="_handleSaveRepoConfig" disabled\$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
</fieldset>
<gr-endpoint-decorator name="repo-config">
<gr-endpoint-param name="repoName" value="[[repo]]"></gr-endpoint-param>
@@ -381,6 +293,4 @@
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index da0c271..b9d5d56 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_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-repo</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-repo.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-repo.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,367 +40,371 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let repoStub;
- const repoConf = {
- description: 'Access inherited by all other projects.',
- use_contributor_agreements: {
- value: false,
- configured_value: 'FALSE',
- },
- use_content_merge: {
- value: false,
- configured_value: 'FALSE',
- },
- use_signed_off_by: {
- value: false,
- configured_value: 'FALSE',
- },
- create_new_change_for_all_not_in_target: {
- value: false,
- configured_value: 'FALSE',
- },
- require_change_id: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- require_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_implicit_merges: {
- value: false,
- configured_value: 'FALSE',
- },
- private_by_default: {
- value: false,
- configured_value: 'FALSE',
- },
- match_author_to_committer_date: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_empty_commit: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_reviewer_by_email: {
- value: false,
- configured_value: 'FALSE',
- },
- max_object_size_limit: {},
- submit_type: 'MERGE_IF_NECESSARY',
- default_submit_type: {
- value: 'MERGE_IF_NECESSARY',
- configured_value: 'INHERIT',
- inherited_value: 'MERGE_IF_NECESSARY',
- },
- };
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+suite('gr-repo tests', () => {
+ let element;
+ let sandbox;
+ let repoStub;
+ const repoConf = {
+ description: 'Access inherited by all other projects.',
+ use_contributor_agreements: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ use_content_merge: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ use_signed_off_by: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ create_new_change_for_all_not_in_target: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ require_change_id: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ enable_signed_push: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ require_signed_push: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ reject_implicit_merges: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ private_by_default: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ match_author_to_committer_date: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ reject_empty_commit: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ enable_reviewer_by_email: {
+ value: false,
+ configured_value: 'FALSE',
+ },
+ max_object_size_limit: {},
+ submit_type: 'MERGE_IF_NECESSARY',
+ default_submit_type: {
+ value: 'MERGE_IF_NECESSARY',
+ configured_value: 'INHERIT',
+ inherited_value: 'MERGE_IF_NECESSARY',
+ },
+ };
- const REPO = 'test-repo';
- const SCHEMES = {http: {}, repo: {}, ssh: {}};
+ const REPO = 'test-repo';
+ const SCHEMES = {http: {}, repo: {}, ssh: {}};
- function getFormFields() {
- const selects = Array.from(
- Polymer.dom(element.root).querySelectorAll('select'));
- const textareas = Array.from(
- Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea'));
- const inputs = Array.from(
- Polymer.dom(element.root).querySelectorAll('input'));
- return inputs.concat(textareas).concat(selects);
- }
+ function getFormFields() {
+ const selects = Array.from(
+ dom(element.root).querySelectorAll('select'));
+ const textareas = Array.from(
+ dom(element.root).querySelectorAll('iron-autogrow-textarea'));
+ const inputs = Array.from(
+ dom(element.root).querySelectorAll('input'));
+ return inputs.concat(textareas).concat(selects);
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getConfig() {
- return Promise.resolve({download: {}});
- },
- });
- element = fixture('basic');
- repoStub = sandbox.stub(
- element.$.restAPI,
- 'getProjectConfig',
- () => Promise.resolve(repoConf));
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getConfig() {
+ return Promise.resolve({download: {}});
+ },
});
+ element = fixture('basic');
+ repoStub = sandbox.stub(
+ element.$.restAPI,
+ 'getProjectConfig',
+ () => Promise.resolve(repoConf));
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('_computePluginData', () => {
- assert.deepEqual(element._computePluginData(), []);
- assert.deepEqual(element._computePluginData({}), []);
- assert.deepEqual(element._computePluginData({base: {}}), []);
- assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
- [{name: 'plugin', config: 'data'}]);
- });
+ test('_computePluginData', () => {
+ assert.deepEqual(element._computePluginData(), []);
+ assert.deepEqual(element._computePluginData({}), []);
+ assert.deepEqual(element._computePluginData({base: {}}), []);
+ assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
+ [{name: 'plugin', config: 'data'}]);
+ });
- test('_handlePluginConfigChanged', () => {
- const notifyStub = sandbox.stub(element, 'notifyPath');
- element._repoConfig = {plugin_config: {}};
- element._handlePluginConfigChanged({detail: {
- name: 'test',
- config: 'data',
- notifyPath: 'path',
- }});
- flushAsynchronousOperations();
+ test('_handlePluginConfigChanged', () => {
+ const notifyStub = sandbox.stub(element, 'notifyPath');
+ element._repoConfig = {plugin_config: {}};
+ element._handlePluginConfigChanged({detail: {
+ name: 'test',
+ config: 'data',
+ notifyPath: 'path',
+ }});
+ flushAsynchronousOperations();
- assert.equal(element._repoConfig.plugin_config.test, 'data');
- assert.equal(notifyStub.lastCall.args[0],
- '_repoConfig.plugin_config.path');
- });
+ assert.equal(element._repoConfig.plugin_config.test, 'data');
+ assert.equal(notifyStub.lastCall.args[0],
+ '_repoConfig.plugin_config.path');
+ });
- test('loading displays before repo config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
+ test('loading displays before repo config is loaded', () => {
+ assert.isTrue(element.$.loading.classList.contains('loading'));
+ assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
+ assert.isTrue(element.$.loadedContent.classList.contains('loading'));
+ assert.isTrue(getComputedStyle(element.$.loadedContent)
+ .display === 'none');
+ });
- test('download commands visibility', () => {
- element._loading = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.downloadContent.classList.contains('hide'));
- assert.isTrue(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- element._schemesObj = SCHEMES;
- flushAsynchronousOperations();
- assert.isFalse(element.$.downloadContent.classList.contains('hide'));
- assert.isFalse(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- });
+ test('download commands visibility', () => {
+ element._loading = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.downloadContent.classList.contains('hide'));
+ assert.isTrue(getComputedStyle(element.$.downloadContent)
+ .display == 'none');
+ element._schemesObj = SCHEMES;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.downloadContent.classList.contains('hide'));
+ assert.isFalse(getComputedStyle(element.$.downloadContent)
+ .display == 'none');
+ });
- test('form defaults to read only', () => {
+ test('form defaults to read only', () => {
+ assert.isTrue(element._readOnly);
+ });
+
+ test('form defaults to read only when not logged in', done => {
+ element.repo = REPO;
+ element._loadRepo().then(() => {
assert.isTrue(element._readOnly);
+ done();
+ });
+ });
+
+ test('form defaults to read only when logged in and not admin', done => {
+ element.repo = REPO;
+ sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
+ sandbox.stub(
+ element.$.restAPI,
+ 'getRepoAccess',
+ () => Promise.resolve({'test-repo': {}}));
+ element._loadRepo().then(() => {
+ assert.isTrue(element._readOnly);
+ done();
+ });
+ });
+
+ test('all form elements are disabled when not admin', done => {
+ element.repo = REPO;
+ element._loadRepo().then(() => {
+ flushAsynchronousOperations();
+ const formFields = getFormFields();
+ for (const field of formFields) {
+ assert.isTrue(field.hasAttribute('disabled'));
+ }
+ done();
+ });
+ });
+
+ test('_formatBooleanSelect', () => {
+ let item = {inherited_value: true};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit (true)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ item = {inherited_value: false};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit (false)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ // For items without inherited values
+ item = {};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+ });
+
+ test('fires page-error', done => {
+ repoStub.restore();
+
+ element.repo = 'test';
+
+ const response = {status: 404};
+ sandbox.stub(
+ element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.deepEqual(e.detail.response, response);
+ done();
});
- test('form defaults to read only when not logged in', done => {
- element.repo = REPO;
- element._loadRepo().then(() => {
- assert.isTrue(element._readOnly);
- done();
- });
- });
+ element._loadRepo();
+ });
- test('form defaults to read only when logged in and not admin', done => {
+ suite('admin', () => {
+ setup(() => {
element.repo = REPO;
sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
sandbox.stub(
element.$.restAPI,
'getRepoAccess',
- () => Promise.resolve({'test-repo': {}}));
- element._loadRepo().then(() => {
- assert.isTrue(element._readOnly);
- done();
- });
+ () => Promise.resolve({'test-repo': {is_owner: true}}));
});
- test('all form elements are disabled when not admin', done => {
- element.repo = REPO;
+ test('all form elements are enabled', done => {
element._loadRepo().then(() => {
flushAsynchronousOperations();
const formFields = getFormFields();
for (const field of formFields) {
- assert.isTrue(field.hasAttribute('disabled'));
+ assert.isFalse(field.hasAttribute('disabled'));
}
+ assert.isFalse(element._loading);
done();
});
});
- test('_formatBooleanSelect', () => {
- let item = {inherited_value: true};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (true)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- item = {inherited_value: false};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (false)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- // For items without inherited values
- item = {};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
+ test('state gets set correctly', done => {
+ element._loadRepo().then(() => {
+ assert.equal(element._repoConfig.state, 'ACTIVE');
+ assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
+ done();
+ });
});
- test('fires page-error', done => {
- repoStub.restore();
-
- element.repo = 'test';
-
- const response = {status: 404};
- sandbox.stub(
- element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
- errFn(response);
+ test('inherited submit type value is calculated correctly', done => {
+ element
+ ._loadRepo().then(() => {
+ const sel = element.$.submitTypeSelect;
+ assert.equal(sel.bindValue, 'INHERIT');
+ assert.equal(
+ sel.nativeSelect.options[0].text,
+ 'Inherit (Merge if necessary)'
+ );
+ done();
});
- element.addEventListener('page-error', e => {
- assert.deepEqual(e.detail.response, response);
- done();
- });
-
- element._loadRepo();
});
- suite('admin', () => {
- setup(() => {
- element.repo = REPO;
- sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true));
- sandbox.stub(
- element.$.restAPI,
- 'getRepoAccess',
- () => Promise.resolve({'test-repo': {is_owner: true}}));
- });
+ test('fields update and save correctly', () => {
+ const configInputObj = {
+ description: 'new description',
+ use_contributor_agreements: 'TRUE',
+ use_content_merge: 'TRUE',
+ use_signed_off_by: 'TRUE',
+ create_new_change_for_all_not_in_target: 'TRUE',
+ require_change_id: 'TRUE',
+ enable_signed_push: 'TRUE',
+ require_signed_push: 'TRUE',
+ reject_implicit_merges: 'TRUE',
+ private_by_default: 'TRUE',
+ match_author_to_committer_date: 'TRUE',
+ reject_empty_commit: 'TRUE',
+ max_object_size_limit: 10,
+ submit_type: 'FAST_FORWARD_ONLY',
+ state: 'READ_ONLY',
+ enable_reviewer_by_email: 'TRUE',
+ };
- test('all form elements are enabled', done => {
- element._loadRepo().then(() => {
- flushAsynchronousOperations();
- const formFields = getFormFields();
- for (const field of formFields) {
- assert.isFalse(field.hasAttribute('disabled'));
- }
- assert.isFalse(element._loading);
- done();
- });
- });
+ const saveStub = sandbox.stub(element.$.restAPI, 'saveRepoConfig'
+ , () => Promise.resolve({}));
- test('state gets set correctly', done => {
- element._loadRepo().then(() => {
- assert.equal(element._repoConfig.state, 'ACTIVE');
- assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
- done();
- });
- });
+ const button = dom(element.root).querySelector('gr-button');
- test('inherited submit type value is calculated correctly', done => {
- element
- ._loadRepo().then(() => {
- const sel = element.$.submitTypeSelect;
- assert.equal(sel.bindValue, 'INHERIT');
- assert.equal(
- sel.nativeSelect.options[0].text,
- 'Inherit (Merge if necessary)'
- );
- done();
- });
- });
+ return element._loadRepo().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(element.$.Title.classList.contains('edited'));
+ element.$.descriptionInput.bindValue = configInputObj.description;
+ element.$.stateSelect.bindValue = configInputObj.state;
+ element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
+ element.$.contentMergeSelect.bindValue =
+ configInputObj.use_content_merge;
+ element.$.newChangeSelect.bindValue =
+ configInputObj.create_new_change_for_all_not_in_target;
+ element.$.requireChangeIdSelect.bindValue =
+ configInputObj.require_change_id;
+ element.$.enableSignedPush.bindValue =
+ configInputObj.enable_signed_push;
+ element.$.requireSignedPush.bindValue =
+ configInputObj.require_signed_push;
+ element.$.rejectImplicitMergesSelect.bindValue =
+ configInputObj.reject_implicit_merges;
+ element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
+ configInputObj.private_by_default;
+ element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
+ configInputObj.match_author_to_committer_date;
+ const inputElement = PolymerElement ?
+ element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
+ inputElement.bindValue = configInputObj.max_object_size_limit;
+ element.$.contributorAgreementSelect.bindValue =
+ configInputObj.use_contributor_agreements;
+ element.$.useSignedOffBySelect.bindValue =
+ configInputObj.use_signed_off_by;
+ element.$.rejectEmptyCommitSelect.bindValue =
+ configInputObj.reject_empty_commit;
+ element.$.unRegisteredCcSelect.bindValue =
+ configInputObj.enable_reviewer_by_email;
- test('fields update and save correctly', () => {
- const configInputObj = {
- description: 'new description',
- use_contributor_agreements: 'TRUE',
- use_content_merge: 'TRUE',
- use_signed_off_by: 'TRUE',
- create_new_change_for_all_not_in_target: 'TRUE',
- require_change_id: 'TRUE',
- enable_signed_push: 'TRUE',
- require_signed_push: 'TRUE',
- reject_implicit_merges: 'TRUE',
- private_by_default: 'TRUE',
- match_author_to_committer_date: 'TRUE',
- reject_empty_commit: 'TRUE',
- max_object_size_limit: 10,
- submit_type: 'FAST_FORWARD_ONLY',
- state: 'READ_ONLY',
- enable_reviewer_by_email: 'TRUE',
- };
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(element.$.configurations.classList.contains('edited'));
- const saveStub = sandbox.stub(element.$.restAPI, 'saveRepoConfig'
- , () => Promise.resolve({}));
+ const formattedObj =
+ element._formatRepoConfigForSave(element._repoConfig);
+ assert.deepEqual(formattedObj, configInputObj);
- const button = Polymer.dom(element.root).querySelector('gr-button');
-
- return element._loadRepo().then(() => {
+ return element._handleSaveRepoConfig().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
- element.$.descriptionInput.bindValue = configInputObj.description;
- element.$.stateSelect.bindValue = configInputObj.state;
- element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
- element.$.contentMergeSelect.bindValue =
- configInputObj.use_content_merge;
- element.$.newChangeSelect.bindValue =
- configInputObj.create_new_change_for_all_not_in_target;
- element.$.requireChangeIdSelect.bindValue =
- configInputObj.require_change_id;
- element.$.enableSignedPush.bindValue =
- configInputObj.enable_signed_push;
- element.$.requireSignedPush.bindValue =
- configInputObj.require_signed_push;
- element.$.rejectImplicitMergesSelect.bindValue =
- configInputObj.reject_implicit_merges;
- element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
- configInputObj.private_by_default;
- element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
- configInputObj.match_author_to_committer_date;
- const inputElement = Polymer.Element ?
- element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
- inputElement.bindValue = configInputObj.max_object_size_limit;
- element.$.contributorAgreementSelect.bindValue =
- configInputObj.use_contributor_agreements;
- element.$.useSignedOffBySelect.bindValue =
- configInputObj.use_signed_off_by;
- element.$.rejectEmptyCommitSelect.bindValue =
- configInputObj.reject_empty_commit;
- element.$.unRegisteredCcSelect.bindValue =
- configInputObj.enable_reviewer_by_email;
-
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.configurations.classList.contains('edited'));
-
- const formattedObj =
- element._formatRepoConfigForSave(element._repoConfig);
- assert.deepEqual(formattedObj, configInputObj);
-
- return element._handleSaveRepoConfig().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
- configInputObj));
- });
+ assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
+ configInputObj));
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index ac98d33..2421c46 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -14,270 +14,286 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-rule-editor_html.js';
+
+/**
+ * Fired when the rule has been modified or removed.
+ *
+ * @event access-modified
+ */
+
+/**
+ * Fired when a rule that was previously added was removed.
+ *
+ * @event added-rule-removed
+ */
+
+const PRIORITY_OPTIONS = [
+ 'BATCH',
+ 'INTERACTIVE',
+];
+
+const Action = {
+ ALLOW: 'ALLOW',
+ DENY: 'DENY',
+ BLOCK: 'BLOCK',
+};
+
+const DROPDOWN_OPTIONS = [Action.ALLOW, Action.DENY, Action.BLOCK];
+
+const ForcePushOptions = {
+ ALLOW: [
+ {name: 'Allow pushing (but not force pushing)', value: false},
+ {name: 'Allow pushing with or without force', value: true},
+ ],
+ BLOCK: [
+ {name: 'Block pushing with or without force', value: false},
+ {name: 'Block force pushing', value: true},
+ ],
+};
+
+const FORCE_EDIT_OPTIONS = [
+ {
+ name: 'No Force Edit',
+ value: false,
+ },
+ {
+ name: 'Force Edit',
+ value: true,
+ },
+];
+
+/**
+ * @appliesMixin Gerrit.AccessMixin
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRuleEditor extends mixinBehaviors( [
+ Gerrit.AccessBehavior,
+ Gerrit.BaseUrlBehavior,
/**
- * Fired when the rule has been modified or removed.
- *
- * @event access-modified
+ * Unused in this element, but called by other elements in tests
+ * e.g gr-permission_test.
*/
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * Fired when a rule that was previously added was removed.
- *
- * @event added-rule-removed
- */
+ static get is() { return 'gr-rule-editor'; }
- const PRIORITY_OPTIONS = [
- 'BATCH',
- 'INTERACTIVE',
- ];
+ static get properties() {
+ return {
+ hasRange: Boolean,
+ /** @type {?} */
+ label: Object,
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_handleEditingChanged',
+ },
+ groupId: String,
+ groupName: String,
+ permission: String,
+ /** @type {?} */
+ rule: {
+ type: Object,
+ notify: true,
+ },
+ section: String,
- const Action = {
- ALLOW: 'ALLOW',
- DENY: 'DENY',
- BLOCK: 'BLOCK',
- };
+ _deleted: {
+ type: Boolean,
+ value: false,
+ },
+ _originalRuleValues: Object,
+ };
+ }
- const DROPDOWN_OPTIONS = [Action.ALLOW, Action.DENY, Action.BLOCK];
+ static get observers() {
+ return [
+ '_handleValueChange(rule.value.*)',
+ ];
+ }
- const ForcePushOptions = {
- ALLOW: [
- {name: 'Allow pushing (but not force pushing)', value: false},
- {name: 'Allow pushing with or without force', value: true},
- ],
- BLOCK: [
- {name: 'Block pushing with or without force', value: false},
- {name: 'Block force pushing', value: true},
- ],
- };
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('access-saved',
+ () => this._handleAccessSaved());
+ }
- const FORCE_EDIT_OPTIONS = [
- {
- name: 'No Force Edit',
- value: false,
- },
- {
- name: 'Force Edit',
- value: true,
- },
- ];
+ /** @override */
+ ready() {
+ super.ready();
+ // Called on ready rather than the observer because when new rules are
+ // added, the observer is triggered prior to being ready.
+ if (!this.rule) { return; } // Check needed for test purposes.
+ this._setupValues(this.rule);
+ }
- /**
- * @appliesMixin Gerrit.AccessMixin
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRuleEditor extends Polymer.mixinBehaviors( [
- Gerrit.AccessBehavior,
- Gerrit.BaseUrlBehavior,
- /**
- * Unused in this element, but called by other elements in tests
- * e.g gr-permission_test.
- */
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-rule-editor'; }
-
- static get properties() {
- return {
- hasRange: Boolean,
- /** @type {?} */
- label: Object,
- editing: {
- type: Boolean,
- value: false,
- observer: '_handleEditingChanged',
- },
- groupId: String,
- groupName: String,
- permission: String,
- /** @type {?} */
- rule: {
- type: Object,
- notify: true,
- },
- section: String,
-
- _deleted: {
- type: Boolean,
- value: false,
- },
- _originalRuleValues: Object,
- };
- }
-
- static get observers() {
- return [
- '_handleValueChange(rule.value.*)',
- ];
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('access-saved',
- () => this._handleAccessSaved());
- }
-
- /** @override */
- ready() {
- super.ready();
- // Called on ready rather than the observer because when new rules are
- // added, the observer is triggered prior to being ready.
- if (!this.rule) { return; } // Check needed for test purposes.
- this._setupValues(this.rule);
- }
-
- /** @override */
- attached() {
- super.attached();
- if (!this.rule) { return; } // Check needed for test purposes.
- if (!this._originalRuleValues) {
- // Observer _handleValueChange is called after the ready()
- // method finishes. Original values must be set later to
- // avoid set .modified flag to true
- this._setOriginalRuleValues(this.rule.value);
- }
- }
-
- _setupValues(rule) {
- if (!rule.value) {
- this._setDefaultRuleValues();
- }
- }
-
- _computeForce(permission, action) {
- if (this.permissionValues.push.id === permission &&
- action !== Action.DENY) {
- return true;
- }
-
- return this.permissionValues.editTopicName.id === permission;
- }
-
- _computeForceClass(permission, action) {
- return this._computeForce(permission, action) ? 'force' : '';
- }
-
- _computeGroupPath(group) {
- return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
- }
-
- _handleAccessSaved() {
- // Set a new 'original' value to keep track of after the value has been
- // saved.
+ /** @override */
+ attached() {
+ super.attached();
+ if (!this.rule) { return; } // Check needed for test purposes.
+ if (!this._originalRuleValues) {
+ // Observer _handleValueChange is called after the ready()
+ // method finishes. Original values must be set later to
+ // avoid set .modified flag to true
this._setOriginalRuleValues(this.rule.value);
}
-
- _handleEditingChanged(editing, editingOld) {
- // Ignore when editing gets set initially.
- if (!editingOld) { return; }
- // Restore original values if no longer editing.
- if (!editing) {
- this._handleUndoChange();
- }
- }
-
- _computeSectionClass(editing, deleted) {
- const classList = [];
- if (editing) {
- classList.push('editing');
- }
- if (deleted) {
- classList.push('deleted');
- }
- return classList.join(' ');
- }
-
- _computeForceOptions(permission, action) {
- if (permission === this.permissionValues.push.id) {
- if (action === Action.ALLOW) {
- return ForcePushOptions.ALLOW;
- } else if (action === Action.BLOCK) {
- return ForcePushOptions.BLOCK;
- } else {
- return [];
- }
- } else if (permission === this.permissionValues.editTopicName.id) {
- return FORCE_EDIT_OPTIONS;
- }
- return [];
- }
-
- _getDefaultRuleValues(permission, label) {
- const ruleAction = Action.ALLOW;
- const value = {};
- if (permission === 'priority') {
- value.action = PRIORITY_OPTIONS[0];
- return value;
- } else if (label) {
- value.min = label.values[0].value;
- value.max = label.values[label.values.length - 1].value;
- } else if (this._computeForce(permission, ruleAction)) {
- value.force =
- this._computeForceOptions(permission, ruleAction)[0].value;
- }
- value.action = DROPDOWN_OPTIONS[0];
- return value;
- }
-
- _setDefaultRuleValues() {
- this.set('rule.value', this._getDefaultRuleValues(this.permission,
- this.label));
- }
-
- _computeOptions(permission) {
- if (permission === 'priority') {
- return PRIORITY_OPTIONS;
- }
- return DROPDOWN_OPTIONS;
- }
-
- _handleRemoveRule() {
- if (this.rule.value.added) {
- this.dispatchEvent(new CustomEvent(
- 'added-rule-removed', {bubbles: true, composed: true}));
- }
- this._deleted = true;
- this.rule.value.deleted = true;
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _handleUndoRemove() {
- this._deleted = false;
- delete this.rule.value.deleted;
- }
-
- _handleUndoChange() {
- // gr-permission will take care of removing rules that were added but
- // unsaved. We need to keep the added bit for the filter.
- if (this.rule.value.added) { return; }
- this.set('rule.value', Object.assign({}, this._originalRuleValues));
- this._deleted = false;
- delete this.rule.value.deleted;
- delete this.rule.value.modified;
- }
-
- _handleValueChange() {
- if (!this._originalRuleValues) { return; }
- this.rule.value.modified = true;
- // Allows overall access page to know a change has been made.
- this.dispatchEvent(
- new CustomEvent('access-modified', {bubbles: true, composed: true}));
- }
-
- _setOriginalRuleValues(value) {
- this._originalRuleValues = Object.assign({}, value);
- }
}
- customElements.define(GrRuleEditor.is, GrRuleEditor);
-})();
+ _setupValues(rule) {
+ if (!rule.value) {
+ this._setDefaultRuleValues();
+ }
+ }
+
+ _computeForce(permission, action) {
+ if (this.permissionValues.push.id === permission &&
+ action !== Action.DENY) {
+ return true;
+ }
+
+ return this.permissionValues.editTopicName.id === permission;
+ }
+
+ _computeForceClass(permission, action) {
+ return this._computeForce(permission, action) ? 'force' : '';
+ }
+
+ _computeGroupPath(group) {
+ return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
+ }
+
+ _handleAccessSaved() {
+ // Set a new 'original' value to keep track of after the value has been
+ // saved.
+ this._setOriginalRuleValues(this.rule.value);
+ }
+
+ _handleEditingChanged(editing, editingOld) {
+ // Ignore when editing gets set initially.
+ if (!editingOld) { return; }
+ // Restore original values if no longer editing.
+ if (!editing) {
+ this._handleUndoChange();
+ }
+ }
+
+ _computeSectionClass(editing, deleted) {
+ const classList = [];
+ if (editing) {
+ classList.push('editing');
+ }
+ if (deleted) {
+ classList.push('deleted');
+ }
+ return classList.join(' ');
+ }
+
+ _computeForceOptions(permission, action) {
+ if (permission === this.permissionValues.push.id) {
+ if (action === Action.ALLOW) {
+ return ForcePushOptions.ALLOW;
+ } else if (action === Action.BLOCK) {
+ return ForcePushOptions.BLOCK;
+ } else {
+ return [];
+ }
+ } else if (permission === this.permissionValues.editTopicName.id) {
+ return FORCE_EDIT_OPTIONS;
+ }
+ return [];
+ }
+
+ _getDefaultRuleValues(permission, label) {
+ const ruleAction = Action.ALLOW;
+ const value = {};
+ if (permission === 'priority') {
+ value.action = PRIORITY_OPTIONS[0];
+ return value;
+ } else if (label) {
+ value.min = label.values[0].value;
+ value.max = label.values[label.values.length - 1].value;
+ } else if (this._computeForce(permission, ruleAction)) {
+ value.force =
+ this._computeForceOptions(permission, ruleAction)[0].value;
+ }
+ value.action = DROPDOWN_OPTIONS[0];
+ return value;
+ }
+
+ _setDefaultRuleValues() {
+ this.set('rule.value', this._getDefaultRuleValues(this.permission,
+ this.label));
+ }
+
+ _computeOptions(permission) {
+ if (permission === 'priority') {
+ return PRIORITY_OPTIONS;
+ }
+ return DROPDOWN_OPTIONS;
+ }
+
+ _handleRemoveRule() {
+ if (this.rule.value.added) {
+ this.dispatchEvent(new CustomEvent(
+ 'added-rule-removed', {bubbles: true, composed: true}));
+ }
+ this._deleted = true;
+ this.rule.value.deleted = true;
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _handleUndoRemove() {
+ this._deleted = false;
+ delete this.rule.value.deleted;
+ }
+
+ _handleUndoChange() {
+ // gr-permission will take care of removing rules that were added but
+ // unsaved. We need to keep the added bit for the filter.
+ if (this.rule.value.added) { return; }
+ this.set('rule.value', Object.assign({}, this._originalRuleValues));
+ this._deleted = false;
+ delete this.rule.value.deleted;
+ delete this.rule.value.modified;
+ }
+
+ _handleValueChange() {
+ if (!this._originalRuleValues) { return; }
+ this.rule.value.modified = true;
+ // Allows overall access page to know a change has been made.
+ this.dispatchEvent(
+ new CustomEvent('access-modified', {bubbles: true, composed: true}));
+ }
+
+ _setOriginalRuleValues(value) {
+ this._originalRuleValues = Object.assign({}, value);
+ }
+}
+
+customElements.define(GrRuleEditor.is, GrRuleEditor);
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
index 9820e31..4ea13b1 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
@@ -1,35 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.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-access-behavior/gr-access-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-rule-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
border-bottom: 1px solid var(--border-color);
@@ -79,34 +66,25 @@
width: 14em;
}
</style>
- <div id="mainContainer"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
+ <div id="mainContainer" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
<div id="options">
- <gr-select id="action"
- bind-value="{{rule.value.action}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
+ <gr-select id="action" bind-value="{{rule.value.action}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
<template is="dom-repeat" items="[[_computeOptions(permission)]]">
<option value="[[item]]">[[item]]</option>
</template>
</select>
</gr-select>
<template is="dom-if" if="[[label]]">
- <gr-select
- id="labelMin"
- bind-value="{{rule.value.min}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
+ <gr-select id="labelMin" bind-value="{{rule.value.min}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
<template is="dom-repeat" items="[[label.values]]">
<option value="[[item.value]]">[[item.value]]</option>
</template>
</select>
</gr-select>
- <gr-select
- id="labelMax"
- bind-value="{{rule.value.max}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
+ <gr-select id="labelMax" bind-value="{{rule.value.max}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
<template is="dom-repeat" items="[[label.values]]">
<option value="[[item.value]]">[[item.value]]</option>
</template>
@@ -114,51 +92,25 @@
</gr-select>
</template>
<template is="dom-if" if="[[hasRange]]">
- <iron-autogrow-textarea
- id="minInput"
- class="min"
- autocomplete="on"
- placeholder="Min value"
- bind-value="{{rule.value.min}}"
- disabled$="[[!editing]]"></iron-autogrow-textarea>
- <iron-autogrow-textarea
- id="maxInput"
- class="max"
- autocomplete="on"
- placeholder="Max value"
- bind-value="{{rule.value.max}}"
- disabled$="[[!editing]]"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="minInput" class="min" autocomplete="on" placeholder="Min value" bind-value="{{rule.value.min}}" disabled\$="[[!editing]]"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="maxInput" class="max" autocomplete="on" placeholder="Max value" bind-value="{{rule.value.max}}" disabled\$="[[!editing]]"></iron-autogrow-textarea>
</template>
- <a class="groupPath" href$="[[_computeGroupPath(groupId)]]">
+ <a class="groupPath" href\$="[[_computeGroupPath(groupId)]]">
[[groupName]]
</a>
- <gr-select
- id="force"
- class$="[[_computeForceClass(permission, rule.value.action)]]"
- bind-value="{{rule.value.force}}"
- on-change="_handleValueChange">
- <select disabled$="[[!editing]]">
- <template
- is="dom-repeat"
- items="[[_computeForceOptions(permission, rule.value.action)]]">
+ <gr-select id="force" class\$="[[_computeForceClass(permission, rule.value.action)]]" bind-value="{{rule.value.force}}" on-change="_handleValueChange">
+ <select disabled\$="[[!editing]]">
+ <template is="dom-repeat" items="[[_computeForceOptions(permission, rule.value.action)]]">
<option value="[[item.value]]">[[item.name]]</option>
</template>
</select>
</gr-select>
</div>
- <gr-button
- link
- id="removeBtn"
- on-click="_handleRemoveRule">Remove</gr-button>
+ <gr-button link="" id="removeBtn" on-click="_handleRemoveRule">Remove</gr-button>
</div>
- <div
- id="deletedContainer"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
+ <div id="deletedContainer" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
[[groupName]] was deleted
- <gr-button link
- id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
+ <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-rule-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index 9f02ddc..42e5f32 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rule-editor</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/page/page.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-rule-editor.html">
+<script src="/node_modules/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 type="module" src="./gr-rule-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-rule-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,593 +41,596 @@
</template>
</test-fixture>
-<script>
- suite('gr-rule-editor 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-rule-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-rule-editor 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();
+ });
- suite('unit tests', () => {
- test('_computeForce, _computeForceClass, and _computeForceOptions',
- () => {
- const ForcePushOptions = {
- ALLOW: [
- {name: 'Allow pushing (but not force pushing)', value: false},
- {name: 'Allow pushing with or without force', value: true},
- ],
- BLOCK: [
- {name: 'Block pushing with or without force', value: false},
- {name: 'Block force pushing', value: true},
- ],
- };
+ suite('unit tests', () => {
+ test('_computeForce, _computeForceClass, and _computeForceOptions',
+ () => {
+ const ForcePushOptions = {
+ ALLOW: [
+ {name: 'Allow pushing (but not force pushing)', value: false},
+ {name: 'Allow pushing with or without force', value: true},
+ ],
+ BLOCK: [
+ {name: 'Block pushing with or without force', value: false},
+ {name: 'Block force pushing', value: true},
+ ],
+ };
- const FORCE_EDIT_OPTIONS = [
- {
- name: 'No Force Edit',
- value: false,
- },
- {
- name: 'Force Edit',
- value: true,
- },
- ];
- let permission = 'push';
- let action = 'ALLOW';
- assert.isTrue(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action),
- 'force');
- assert.deepEqual(element._computeForceOptions(permission, action),
- ForcePushOptions.ALLOW);
+ const FORCE_EDIT_OPTIONS = [
+ {
+ name: 'No Force Edit',
+ value: false,
+ },
+ {
+ name: 'Force Edit',
+ value: true,
+ },
+ ];
+ let permission = 'push';
+ let action = 'ALLOW';
+ assert.isTrue(element._computeForce(permission, action));
+ assert.equal(element._computeForceClass(permission, action),
+ 'force');
+ assert.deepEqual(element._computeForceOptions(permission, action),
+ ForcePushOptions.ALLOW);
- action = 'BLOCK';
- assert.isTrue(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action),
- 'force');
- assert.deepEqual(element._computeForceOptions(permission, action),
- ForcePushOptions.BLOCK);
+ action = 'BLOCK';
+ assert.isTrue(element._computeForce(permission, action));
+ assert.equal(element._computeForceClass(permission, action),
+ 'force');
+ assert.deepEqual(element._computeForceOptions(permission, action),
+ ForcePushOptions.BLOCK);
- action = 'DENY';
- assert.isFalse(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action), '');
- assert.equal(
- element._computeForceOptions(permission, action).length, 0);
-
- permission = 'editTopicName';
- assert.isTrue(element._computeForce(permission));
- assert.equal(element._computeForceClass(permission), 'force');
- assert.deepEqual(element._computeForceOptions(permission),
- FORCE_EDIT_OPTIONS);
- permission = 'submit';
- assert.isFalse(element._computeForce(permission));
- assert.equal(element._computeForceClass(permission), '');
- assert.deepEqual(element._computeForceOptions(permission), []);
- });
-
- test('_computeSectionClass', () => {
- let deleted = true;
- let editing = false;
- assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
-
- deleted = false;
- assert.equal(element._computeSectionClass(editing, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, deleted), 'editing');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, deleted),
- 'editing deleted');
- });
-
- test('_getDefaultRuleValues', () => {
- let permission = 'priority';
- let label;
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'BATCH'});
- permission = 'label-Code-Review';
- label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW', max: 2, min: -2});
- permission = 'push';
- label = undefined;
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW', force: false});
- permission = 'submit';
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW'});
- });
-
- test('_setDefaultRuleValues', () => {
- element.rule = {id: 123};
- const defaultValue = {action: 'ALLOW'};
- sandbox.stub(element, '_getDefaultRuleValues').returns(defaultValue);
- element._setDefaultRuleValues();
- assert.isTrue(element._getDefaultRuleValues.called);
- assert.equal(element.rule.value, defaultValue);
- });
-
- test('_computeOptions', () => {
- const PRIORITY_OPTIONS = [
- 'BATCH',
- 'INTERACTIVE',
- ];
- const DROPDOWN_OPTIONS = [
- 'ALLOW',
- 'DENY',
- 'BLOCK',
- ];
- let permission = 'priority';
- assert.deepEqual(element._computeOptions(permission), PRIORITY_OPTIONS);
- permission = 'submit';
- assert.deepEqual(element._computeOptions(permission), DROPDOWN_OPTIONS);
- });
-
- test('_handleValueChange', () => {
- const modifiedHandler = sandbox.stub();
- element.rule = {value: {}};
- element.addEventListener('access-modified', modifiedHandler);
- element._handleValueChange();
- assert.isNotOk(element.rule.value.modified);
- element._originalRuleValues = {};
- element._handleValueChange();
- assert.isTrue(element.rule.value.modified);
- assert.isTrue(modifiedHandler.called);
- });
-
- test('_handleAccessSaved', () => {
- const originalValue = {action: 'DENY'};
- const newValue = {action: 'ALLOW'};
- element._originalRuleValues = originalValue;
- element.rule = {value: newValue};
- element._handleAccessSaved();
- assert.deepEqual(element._originalRuleValues, newValue);
- });
-
- test('_setOriginalRuleValues', () => {
- const value = {
- action: 'ALLOW',
- force: false,
- };
- element._setOriginalRuleValues(value);
- assert.deepEqual(element._originalRuleValues, value);
- });
- });
-
- suite('already existing generic rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'submit';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: false,
- },
- };
- element.section = 'refs/*';
-
- // Typically called on ready since elements will have properies defined
- // by the parent element.
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin'));
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax'));
- assert.isFalse(element.$.force.classList.contains('force'));
- });
-
- test('modify and cancel restores original values', () => {
- element.editing = true;
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = 'DENY';
- assert.isTrue(element.rule.value.modified);
- element.editing = false;
- assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- assert.equal(element.$.action.bindValue, 'ALLOW');
- assert.isNotOk(element.rule.value.modified);
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = 'DENY';
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('all selects are disabled when not in edit mode', () => {
- const selects = Polymer.dom(element.root).querySelectorAll('select');
- for (const select of selects) {
- assert.isTrue(select.disabled);
- }
- element.editing = true;
- for (const select of selects) {
- assert.isFalse(select.disabled);
- }
- });
-
- test('remove rule and undo remove', () => {
- element.editing = true;
- element.rule = {id: 123, value: {action: 'ALLOW'}};
- assert.isFalse(
- element.$.deletedContainer.classList.contains('deleted'));
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(element.$.deletedContainer.classList.contains('deleted'));
- assert.isTrue(element._deleted);
- assert.isTrue(element.rule.value.deleted);
-
- MockInteractions.tap(element.$.undoRemoveBtn);
- assert.isFalse(element._deleted);
- assert.isNotOk(element.rule.value.deleted);
- });
-
- test('remove rule and cancel', () => {
- element.editing = true;
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.deletedContainer).display,
- 'none');
-
- element.rule = {id: 123, value: {action: 'ALLOW'}};
- MockInteractions.tap(element.$.removeBtn);
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.notEqual(getComputedStyle(element.$.deletedContainer).display,
- 'none');
- assert.isTrue(element._deleted);
- assert.isTrue(element.rule.value.deleted);
-
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.rule.value.deleted);
- assert.isNotOk(element.rule.value.modified);
-
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.deletedContainer).display,
- 'none');
- });
-
- test('_computeGroupPath', () => {
- const group = '123';
- assert.equal(element._computeGroupPath(group),
- `/admin/groups/123`);
- });
- });
-
- suite('new edit rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'editTopicName';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- element.rule.value.added = true;
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- const expectedRuleValue = {
- action: 'ALLOW',
- force: false,
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, expectedRuleValue.action);
- assert.equal(element.$.force.bindValue, expectedRuleValue.action);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.force.bindValue = true;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('remove value', () => {
- element.editing = true;
- const removeStub = sandbox.stub();
- element.addEventListener('added-rule-removed', removeStub);
- MockInteractions.tap(element.$.removeBtn);
- flushAsynchronousOperations();
- assert.isTrue(removeStub.called);
- });
- });
-
- suite('already existing rule with labels', () => {
- setup(done => {
- element.label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- element.group = 'Group Name';
- element.permission = 'label-Code-Review';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: false,
- max: 2,
- min: -2,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMin').bindValue,
- element.rule.value.min);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMax').bindValue,
- element.rule.value.max);
- assert.isFalse(element.$.force.classList.contains('force'));
- });
-
- test('modify value', () => {
- const removeStub = sandbox.stub();
- element.addEventListener('added-rule-removed', removeStub);
- assert.isNotOk(element.rule.value.modified);
- Polymer.dom(element.root).querySelector('#labelMin').bindValue = 1;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
- assert.isFalse(removeStub.called);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
-
- suite('new rule with labels', () => {
- setup(done => {
- sandbox.spy(element, '_setDefaultRuleValues');
- element.label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- element.group = 'Group Name';
- element.permission = 'label-Code-Review';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- element.rule.value.added = true;
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- assert.isTrue(element._setDefaultRuleValues.called);
-
- const expectedRuleValue = {
- max: element.label.values[element.label.values.length - 1].value,
- min: element.label.values[0].value,
- action: 'ALLOW',
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
+ action = 'DENY';
+ assert.isFalse(element._computeForce(permission, action));
+ assert.equal(element._computeForceClass(permission, action), '');
assert.equal(
- element.$.action.bindValue,
- expectedRuleValue.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMin').bindValue,
- expectedRuleValue.min);
- assert.equal(
- Polymer.dom(element.root).querySelector('#labelMax').bindValue,
- expectedRuleValue.max);
+ element._computeForceOptions(permission, action).length, 0);
+
+ permission = 'editTopicName';
+ assert.isTrue(element._computeForce(permission));
+ assert.equal(element._computeForceClass(permission), 'force');
+ assert.deepEqual(element._computeForceOptions(permission),
+ FORCE_EDIT_OPTIONS);
+ permission = 'submit';
+ assert.isFalse(element._computeForce(permission));
+ assert.equal(element._computeForceClass(permission), '');
+ assert.deepEqual(element._computeForceOptions(permission), []);
});
- });
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- Polymer.dom(element.root).querySelector('#labelMin').bindValue = 1;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
+ test('_computeSectionClass', () => {
+ let deleted = true;
+ let editing = false;
+ assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ deleted = false;
+ assert.equal(element._computeSectionClass(editing, deleted), '');
+
+ editing = true;
+ assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+
+ deleted = true;
+ assert.equal(element._computeSectionClass(editing, deleted),
+ 'editing deleted');
});
- suite('already existing push rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'push';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: true,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.isTrue(element.$.force.classList.contains('force'));
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#force').bindValue,
- element.rule.value.force);
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin'));
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax'));
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = false;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_getDefaultRuleValues', () => {
+ let permission = 'priority';
+ let label;
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'BATCH'});
+ permission = 'label-Code-Review';
+ label = {values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ]};
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'ALLOW', max: 2, min: -2});
+ permission = 'push';
+ label = undefined;
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'ALLOW', force: false});
+ permission = 'submit';
+ assert.deepEqual(element._getDefaultRuleValues(permission, label),
+ {action: 'ALLOW'});
});
- suite('new push rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'push';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- element.rule.value.added = true;
- flush(() => {
- element.attached();
- done();
- });
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- const expectedRuleValue = {
- action: 'ALLOW',
- force: false,
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, expectedRuleValue.action);
- assert.equal(element.$.force.bindValue, expectedRuleValue.action);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.force.bindValue = true;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_setDefaultRuleValues', () => {
+ element.rule = {id: 123};
+ const defaultValue = {action: 'ALLOW'};
+ sandbox.stub(element, '_getDefaultRuleValues').returns(defaultValue);
+ element._setDefaultRuleValues();
+ assert.isTrue(element._getDefaultRuleValues.called);
+ assert.equal(element.rule.value, defaultValue);
});
- suite('already existing edit rule', () => {
- setup(done => {
- element.group = 'Group Name';
- element.permission = 'editTopicName';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: true,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- flush(() => {
- element.attached();
- done();
- });
- });
+ test('_computeOptions', () => {
+ const PRIORITY_OPTIONS = [
+ 'BATCH',
+ 'INTERACTIVE',
+ ];
+ const DROPDOWN_OPTIONS = [
+ 'ALLOW',
+ 'DENY',
+ 'BLOCK',
+ ];
+ let permission = 'priority';
+ assert.deepEqual(element._computeOptions(permission), PRIORITY_OPTIONS);
+ permission = 'submit';
+ assert.deepEqual(element._computeOptions(permission), DROPDOWN_OPTIONS);
+ });
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_handleValueChange', () => {
+ const modifiedHandler = sandbox.stub();
+ element.rule = {value: {}};
+ element.addEventListener('access-modified', modifiedHandler);
+ element._handleValueChange();
+ assert.isNotOk(element.rule.value.modified);
+ element._originalRuleValues = {};
+ element._handleValueChange();
+ assert.isTrue(element.rule.value.modified);
+ assert.isTrue(modifiedHandler.called);
+ });
- test('values are set correctly', () => {
- assert.isTrue(element.$.force.classList.contains('force'));
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- Polymer.dom(element.root).querySelector('#force').bindValue,
- element.rule.value.force);
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin'));
- assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax'));
- });
+ test('_handleAccessSaved', () => {
+ const originalValue = {action: 'DENY'};
+ const newValue = {action: 'ALLOW'};
+ element._originalRuleValues = originalValue;
+ element.rule = {value: newValue};
+ element._handleAccessSaved();
+ assert.deepEqual(element._originalRuleValues, newValue);
+ });
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = false;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
+ test('_setOriginalRuleValues', () => {
+ const value = {
+ action: 'ALLOW',
+ force: false,
+ };
+ element._setOriginalRuleValues(value);
+ assert.deepEqual(element._originalRuleValues, value);
});
});
+
+ suite('already existing generic rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'submit';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: false,
+ },
+ };
+ element.section = 'refs/*';
+
+ // Typically called on ready since elements will have properies defined
+ // by the parent element.
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.isNotOk(dom(element.root).querySelector('#labelMin'));
+ assert.isNotOk(dom(element.root).querySelector('#labelMax'));
+ assert.isFalse(element.$.force.classList.contains('force'));
+ });
+
+ test('modify and cancel restores original values', () => {
+ element.editing = true;
+ assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = 'DENY';
+ assert.isTrue(element.rule.value.modified);
+ element.editing = false;
+ assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ assert.equal(element.$.action.bindValue, 'ALLOW');
+ assert.isNotOk(element.rule.value.modified);
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = 'DENY';
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('all selects are disabled when not in edit mode', () => {
+ const selects = dom(element.root).querySelectorAll('select');
+ for (const select of selects) {
+ assert.isTrue(select.disabled);
+ }
+ element.editing = true;
+ for (const select of selects) {
+ assert.isFalse(select.disabled);
+ }
+ });
+
+ test('remove rule and undo remove', () => {
+ element.editing = true;
+ element.rule = {id: 123, value: {action: 'ALLOW'}};
+ assert.isFalse(
+ element.$.deletedContainer.classList.contains('deleted'));
+ MockInteractions.tap(element.$.removeBtn);
+ assert.isTrue(element.$.deletedContainer.classList.contains('deleted'));
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.rule.value.deleted);
+
+ MockInteractions.tap(element.$.undoRemoveBtn);
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.rule.value.deleted);
+ });
+
+ test('remove rule and cancel', () => {
+ element.editing = true;
+ assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.deletedContainer).display,
+ 'none');
+
+ element.rule = {id: 123, value: {action: 'ALLOW'}};
+ MockInteractions.tap(element.$.removeBtn);
+ assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.notEqual(getComputedStyle(element.$.deletedContainer).display,
+ 'none');
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.rule.value.deleted);
+
+ element.editing = false;
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.rule.value.deleted);
+ assert.isNotOk(element.rule.value.modified);
+
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.deletedContainer).display,
+ 'none');
+ });
+
+ test('_computeGroupPath', () => {
+ const group = '123';
+ assert.equal(element._computeGroupPath(group),
+ `/admin/groups/123`);
+ });
+ });
+
+ suite('new edit rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'editTopicName';
+ element.rule = {
+ id: '123',
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ element.rule.value.added = true;
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule.value.modified);
+ const expectedRuleValue = {
+ action: 'ALLOW',
+ force: false,
+ added: true,
+ };
+ assert.deepEqual(element.rule.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, expectedRuleValue.action);
+ assert.equal(element.$.force.bindValue, expectedRuleValue.action);
+ });
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.force.bindValue = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('remove value', () => {
+ element.editing = true;
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-rule-removed', removeStub);
+ MockInteractions.tap(element.$.removeBtn);
+ flushAsynchronousOperations();
+ assert.isTrue(removeStub.called);
+ });
+ });
+
+ suite('already existing rule with labels', () => {
+ setup(done => {
+ element.label = {values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ]};
+ element.group = 'Group Name';
+ element.permission = 'label-Code-Review';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: false,
+ max: 2,
+ min: -2,
+ },
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.equal(
+ dom(element.root).querySelector('#labelMin').bindValue,
+ element.rule.value.min);
+ assert.equal(
+ dom(element.root).querySelector('#labelMax').bindValue,
+ element.rule.value.max);
+ assert.isFalse(element.$.force.classList.contains('force'));
+ });
+
+ test('modify value', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-rule-removed', removeStub);
+ assert.isNotOk(element.rule.value.modified);
+ dom(element.root).querySelector('#labelMin').bindValue = 1;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+ assert.isFalse(removeStub.called);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('new rule with labels', () => {
+ setup(done => {
+ sandbox.spy(element, '_setDefaultRuleValues');
+ element.label = {values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ]};
+ element.group = 'Group Name';
+ element.permission = 'label-Code-Review';
+ element.rule = {
+ id: '123',
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ element.rule.value.added = true;
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule.value.modified);
+ assert.isTrue(element._setDefaultRuleValues.called);
+
+ const expectedRuleValue = {
+ max: element.label.values[element.label.values.length - 1].value,
+ min: element.label.values[0].value,
+ action: 'ALLOW',
+ added: true,
+ };
+ assert.deepEqual(element.rule.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(
+ element.$.action.bindValue,
+ expectedRuleValue.action);
+ assert.equal(
+ dom(element.root).querySelector('#labelMin').bindValue,
+ expectedRuleValue.min);
+ assert.equal(
+ dom(element.root).querySelector('#labelMax').bindValue,
+ expectedRuleValue.max);
+ });
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ dom(element.root).querySelector('#labelMin').bindValue = 1;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('already existing push rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'push';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: true,
+ },
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.isTrue(element.$.force.classList.contains('force'));
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.equal(
+ dom(element.root).querySelector('#force').bindValue,
+ element.rule.value.force);
+ assert.isNotOk(dom(element.root).querySelector('#labelMin'));
+ assert.isNotOk(dom(element.root).querySelector('#labelMax'));
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('new push rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'push';
+ element.rule = {
+ id: '123',
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ element.rule.value.added = true;
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule.value.modified);
+ const expectedRuleValue = {
+ action: 'ALLOW',
+ force: false,
+ added: true,
+ };
+ assert.deepEqual(element.rule.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(element.$.action.bindValue, expectedRuleValue.action);
+ assert.equal(element.$.force.bindValue, expectedRuleValue.action);
+ });
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.force.bindValue = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('already existing edit rule', () => {
+ setup(done => {
+ element.group = 'Group Name';
+ element.permission = 'editTopicName';
+ element.rule = {
+ id: '123',
+ value: {
+ action: 'ALLOW',
+ force: true,
+ },
+ };
+ element.section = 'refs/*';
+ element._setupValues(element.rule);
+ flushAsynchronousOperations();
+ flush(() => {
+ element.attached();
+ done();
+ });
+ });
+
+ test('_ruleValues and _originalRuleValues are set correctly', () => {
+ assert.deepEqual(element._originalRuleValues, element.rule.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.isTrue(element.$.force.classList.contains('force'));
+ assert.equal(element.$.action.bindValue, element.rule.value.action);
+ assert.equal(
+ dom(element.root).querySelector('#force').bindValue,
+ element.rule.value.force);
+ assert.isNotOk(dom(element.root).querySelector('#labelMin'));
+ assert.isNotOk(dom(element.root).querySelector('#labelMax'));
+ });
+
+ test('modify value', () => {
+ assert.isNotOk(element.rule.value.modified);
+ element.$.action.bindValue = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.rule.value.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element._originalRuleValues, element.rule.value);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 013b44b..6e2f11c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.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,210 +14,232 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const CHANGE_SIZE = {
- XS: 10,
- SMALL: 50,
- MEDIUM: 250,
- LARGE: 1000,
- };
+import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../styles/gr-change-list-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-change-star/gr-change-star.js';
+import '../../shared/gr-change-status/gr-change-status.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-limited-text/gr-limited-text.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.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-change-list-item_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.ChangeTableMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrChangeListItem extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.ChangeTableBehavior,
- Gerrit.PathListBehavior,
- Gerrit.RESTClientBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-list-item'; }
+const CHANGE_SIZE = {
+ XS: 10,
+ SMALL: 50,
+ MEDIUM: 250,
+ LARGE: 1000,
+};
- static get properties() {
- return {
- visibleChangeTableColumns: Array,
- labelNames: {
- type: Array,
- },
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrChangeListItem extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.ChangeTableBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.RESTClientBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @type {?} */
- change: Object,
- changeURL: {
- type: String,
- computed: '_computeChangeURL(change)',
- },
- statuses: {
- type: Array,
- computed: 'changeStatuses(change)',
- },
- showStar: {
- type: Boolean,
- value: false,
- },
- showNumber: Boolean,
- _changeSize: {
- type: String,
- computed: '_computeChangeSize(change)',
- },
- _dynamicCellEndpoints: {
- type: Array,
- },
- };
+ static get is() { return 'gr-change-list-item'; }
+
+ static get properties() {
+ return {
+ visibleChangeTableColumns: Array,
+ labelNames: {
+ type: Array,
+ },
+
+ /** @type {?} */
+ change: Object,
+ changeURL: {
+ type: String,
+ computed: '_computeChangeURL(change)',
+ },
+ statuses: {
+ type: Array,
+ computed: 'changeStatuses(change)',
+ },
+ showStar: {
+ type: Boolean,
+ value: false,
+ },
+ showNumber: Boolean,
+ _changeSize: {
+ type: String,
+ computed: '_computeChangeSize(change)',
+ },
+ _dynamicCellEndpoints: {
+ type: Array,
+ },
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ Gerrit.awaitPluginsLoaded().then(() => {
+ this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-list-item-cell');
+ });
+ }
+
+ _computeChangeURL(change) {
+ return Gerrit.Nav.getUrlForChange(change);
+ }
+
+ _computeLabelTitle(change, labelName) {
+ const label = change.labels[labelName];
+ if (!label) { return 'Label not applicable'; }
+ const significantLabel = label.rejected || label.approved ||
+ label.disliked || label.recommended;
+ if (significantLabel && significantLabel.name) {
+ return labelName + '\nby ' + significantLabel.name;
}
+ return labelName;
+ }
- /** @override */
- attached() {
- super.attached();
- Gerrit.awaitPluginsLoaded().then(() => {
- this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-list-item-cell');
- });
- }
-
- _computeChangeURL(change) {
- return Gerrit.Nav.getUrlForChange(change);
- }
-
- _computeLabelTitle(change, labelName) {
- const label = change.labels[labelName];
- if (!label) { return 'Label not applicable'; }
- const significantLabel = label.rejected || label.approved ||
- label.disliked || label.recommended;
- if (significantLabel && significantLabel.name) {
- return labelName + '\nby ' + significantLabel.name;
- }
- return labelName;
- }
-
- _computeLabelClass(change, labelName) {
- const label = change.labels[labelName];
- // Mimic a Set.
- const classes = {
- cell: true,
- label: true,
- };
- if (label) {
- if (label.approved) {
- classes['u-green'] = true;
- }
- if (label.value == 1) {
- classes['u-monospace'] = true;
- classes['u-green'] = true;
- } else if (label.value == -1) {
- classes['u-monospace'] = true;
- classes['u-red'] = true;
- }
- if (label.rejected) {
- classes['u-red'] = true;
- }
- } else {
- classes['u-gray-background'] = true;
- }
- return Object.keys(classes).sort()
- .join(' ');
- }
-
- _computeLabelValue(change, labelName) {
- const label = change.labels[labelName];
- if (!label) { return ''; }
+ _computeLabelClass(change, labelName) {
+ const label = change.labels[labelName];
+ // Mimic a Set.
+ const classes = {
+ cell: true,
+ label: true,
+ };
+ if (label) {
if (label.approved) {
- return '✓';
+ classes['u-green'] = true;
+ }
+ if (label.value == 1) {
+ classes['u-monospace'] = true;
+ classes['u-green'] = true;
+ } else if (label.value == -1) {
+ classes['u-monospace'] = true;
+ classes['u-red'] = true;
}
if (label.rejected) {
- return '✕';
+ classes['u-red'] = true;
}
- if (label.value > 0) {
- return '+' + label.value;
- }
- if (label.value < 0) {
- return label.value;
- }
- return '';
+ } else {
+ classes['u-gray-background'] = true;
}
+ return Object.keys(classes).sort()
+ .join(' ');
+ }
- _computeRepoUrl(change) {
- return Gerrit.Nav.getUrlForProjectChanges(change.project, true,
- change.internalHost);
+ _computeLabelValue(change, labelName) {
+ const label = change.labels[labelName];
+ if (!label) { return ''; }
+ if (label.approved) {
+ return '✓';
}
-
- _computeRepoBranchURL(change) {
- return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null,
- change.internalHost);
+ if (label.rejected) {
+ return '✕';
}
-
- _computeTopicURL(change) {
- if (!change.topic) { return ''; }
- return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost);
+ if (label.value > 0) {
+ return '+' + label.value;
}
-
- /**
- * Computes the display string for the project column. If there is a host
- * specified in the change detail, the string will be prefixed with it.
- *
- * @param {!Object} change
- * @param {string=} truncate whether or not the project name should be
- * truncated. If this value is truthy, the name will be truncated.
- * @return {string}
- */
- _computeRepoDisplay(change, truncate) {
- if (!change || !change.project) { return ''; }
- let str = '';
- if (change.internalHost) { str += change.internalHost + '/'; }
- str += truncate ? this.truncatePath(change.project, 2) : change.project;
- return str;
+ if (label.value < 0) {
+ return label.value;
}
+ return '';
+ }
- _computeSizeTooltip(change) {
- if (change.insertions + change.deletions === 0 ||
- isNaN(change.insertions + change.deletions)) {
- return 'Size unknown';
- } else {
- return `+${change.insertions}, -${change.deletions}`;
- }
- }
+ _computeRepoUrl(change) {
+ return Gerrit.Nav.getUrlForProjectChanges(change.project, true,
+ change.internalHost);
+ }
- /**
- * TShirt sizing is based on the following paper:
- * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf
- */
- _computeChangeSize(change) {
- const delta = change.insertions + change.deletions;
- if (isNaN(delta) || delta === 0) {
- return null; // Unknown
- }
- if (delta < CHANGE_SIZE.XS) {
- return 'XS';
- } else if (delta < CHANGE_SIZE.SMALL) {
- return 'S';
- } else if (delta < CHANGE_SIZE.MEDIUM) {
- return 'M';
- } else if (delta < CHANGE_SIZE.LARGE) {
- return 'L';
- } else {
- return 'XL';
- }
- }
+ _computeRepoBranchURL(change) {
+ return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null,
+ change.internalHost);
+ }
- toggleReviewed() {
- const newVal = !this.change.reviewed;
- this.set('change.reviewed', newVal);
- this.dispatchEvent(new CustomEvent('toggle-reviewed', {
- bubbles: true,
- composed: true,
- detail: {change: this.change, reviewed: newVal},
- }));
+ _computeTopicURL(change) {
+ if (!change.topic) { return ''; }
+ return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost);
+ }
+
+ /**
+ * Computes the display string for the project column. If there is a host
+ * specified in the change detail, the string will be prefixed with it.
+ *
+ * @param {!Object} change
+ * @param {string=} truncate whether or not the project name should be
+ * truncated. If this value is truthy, the name will be truncated.
+ * @return {string}
+ */
+ _computeRepoDisplay(change, truncate) {
+ if (!change || !change.project) { return ''; }
+ let str = '';
+ if (change.internalHost) { str += change.internalHost + '/'; }
+ str += truncate ? this.truncatePath(change.project, 2) : change.project;
+ return str;
+ }
+
+ _computeSizeTooltip(change) {
+ if (change.insertions + change.deletions === 0 ||
+ isNaN(change.insertions + change.deletions)) {
+ return 'Size unknown';
+ } else {
+ return `+${change.insertions}, -${change.deletions}`;
}
}
- customElements.define(GrChangeListItem.is, GrChangeListItem);
-})();
+ /**
+ * TShirt sizing is based on the following paper:
+ * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf
+ */
+ _computeChangeSize(change) {
+ const delta = change.insertions + change.deletions;
+ if (isNaN(delta) || delta === 0) {
+ return null; // Unknown
+ }
+ if (delta < CHANGE_SIZE.XS) {
+ return 'XS';
+ } else if (delta < CHANGE_SIZE.SMALL) {
+ return 'S';
+ } else if (delta < CHANGE_SIZE.MEDIUM) {
+ return 'M';
+ } else if (delta < CHANGE_SIZE.LARGE) {
+ return 'L';
+ } else {
+ return 'XL';
+ }
+ }
+
+ toggleReviewed() {
+ const newVal = !this.change.reviewed;
+ this.set('change.reviewed', newVal);
+ this.dispatchEvent(new CustomEvent('toggle-reviewed', {
+ bubbles: true,
+ composed: true,
+ detail: {change: this.change, reviewed: newVal},
+ }));
+ }
+}
+
+customElements.define(GrChangeListItem.is, GrChangeListItem);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
index 9022cf2..f76189c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
@@ -1,39 +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/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/gr-change-list-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
-<link rel="import" href="../../shared/gr-change-status/gr-change-status.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-
-<dom-module id="gr-change-list-item">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: table-row;
@@ -128,17 +111,16 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<td class="cell leftPadding"></td>
- <td class="cell star" hidden$="[[!showStar]]" hidden>
+ <td class="cell star" hidden\$="[[!showStar]]" hidden="">
<gr-change-star change="{{change}}"></gr-change-star>
</td>
- <td class="cell number" hidden$="[[!showNumber]]" hidden>
- <a href$="[[changeURL]]">[[change._number]]</a>
+ <td class="cell number" hidden\$="[[!showNumber]]" hidden="">
+ <a href\$="[[changeURL]]">[[change._number]]</a>
</td>
- <td class="cell subject"
- hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]">
+ <td class="cell subject" hidden\$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]">
<div class="container">
<div class="content">
- <a title$="[[change.subject]]" href$="[[changeURL]]">
+ <a title\$="[[change.subject]]" href\$="[[changeURL]]">
[[change.subject]]
</a>
</div>
@@ -148,67 +130,50 @@
<span> </span>
</div>
</td>
- <td class="cell status"
- hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]">
+ <td class="cell status" hidden\$="[[isColumnHidden('Status', visibleChangeTableColumns)]]">
<template is="dom-repeat" items="[[statuses]]" as="status">
<div class="comma">,</div>
- <gr-change-status flat status="[[status]]"></gr-change-status>
+ <gr-change-status flat="" status="[[status]]"></gr-change-status>
</template>
<template is="dom-if" if="[[!statuses.length]]">
<span class="placeholder">--</span>
</template>
</td>
- <td class="cell owner"
- hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
- <gr-account-link
- account="[[change.owner]]"></gr-account-link>
+ <td class="cell owner" hidden\$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
+ <gr-account-link account="[[change.owner]]"></gr-account-link>
</td>
- <td class="cell assignee"
- hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
+ <td class="cell assignee" hidden\$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
<template is="dom-if" if="[[change.assignee]]">
- <gr-account-link
- id="assigneeAccountLink"
- account="[[change.assignee]]"></gr-account-link>
+ <gr-account-link id="assigneeAccountLink" account="[[change.assignee]]"></gr-account-link>
</template>
<template is="dom-if" if="[[!change.assignee]]">
<span class="placeholder">--</span>
</template>
</td>
- <td class="cell repo"
- hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]">
- <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
+ <td class="cell repo" hidden\$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]">
+ <a class="fullRepo" href\$="[[_computeRepoUrl(change)]]">
[[_computeRepoDisplay(change)]]
</a>
- <a
- class="truncatedRepo"
- href$="[[_computeRepoUrl(change)]]"
- title$="[[_computeRepoDisplay(change)]]">
+ <a class="truncatedRepo" href\$="[[_computeRepoUrl(change)]]" title\$="[[_computeRepoDisplay(change)]]">
[[_computeRepoDisplay(change, 'true')]]
</a>
</td>
- <td class="cell branch"
- hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]">
- <a href$="[[_computeRepoBranchURL(change)]]">
+ <td class="cell branch" hidden\$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]">
+ <a href\$="[[_computeRepoBranchURL(change)]]">
[[change.branch]]
</a>
<template is="dom-if" if="[[change.topic]]">
- (<a href$="[[_computeTopicURL(change)]]"><!--
+ (<a href\$="[[_computeTopicURL(change)]]"><!--
--><gr-limited-text limit="50" text="[[change.topic]]">
</gr-limited-text><!--
--></a>)
</template>
</td>
- <td class="cell updated"
- hidden$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]">
- <gr-date-formatter
- has-tooltip
- date-str="[[change.updated]]"></gr-date-formatter>
+ <td class="cell updated" hidden\$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]">
+ <gr-date-formatter has-tooltip="" date-str="[[change.updated]]"></gr-date-formatter>
</td>
- <td class="cell size"
- hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]">
- <gr-tooltip-content
- has-tooltip
- title="[[_computeSizeTooltip(change)]]">
+ <td class="cell size" hidden\$="[[isColumnHidden('Size', visibleChangeTableColumns)]]">
+ <gr-tooltip-content has-tooltip="" title="[[_computeSizeTooltip(change)]]">
<template is="dom-if" if="[[_changeSize]]">
<span>[[_changeSize]]</span>
</template>
@@ -218,20 +183,16 @@
</gr-tooltip-content>
</td>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
- <td title$="[[_computeLabelTitle(change, labelName)]]"
- class$="[[_computeLabelClass(change, labelName)]]">
+ <td title\$="[[_computeLabelTitle(change, labelName)]]" class\$="[[_computeLabelClass(change, labelName)]]">
[[_computeLabelValue(change, labelName)]]
</td>
</template>
- <template is="dom-repeat" items="[[_dynamicCellEndpoints]]"
- as="pluginEndpointName">
+ <template is="dom-repeat" items="[[_dynamicCellEndpoints]]" as="pluginEndpointName">
<td class="cell endpoint">
- <gr-endpoint-decorator name$="[[pluginEndpointName]]">
+ <gr-endpoint-decorator name\$="[[pluginEndpointName]]">
<gr-endpoint-param name="change" value="[[change]]">
</gr-endpoint-param>
</gr-endpoint-decorator>
</td>
</template>
- </template>
- <script src="gr-change-list-item.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 076c478..9bfec19 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-item</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-change-list-item.html">
+<script type="module" src="./gr-change-list-item.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-change-list-item.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,244 +43,247 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-list-item tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-change-list-item.js';
+suite('gr-change-list-item tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
});
+ element = fixture('basic');
+ });
- teardown(() => { sandbox.restore(); });
+ teardown(() => { sandbox.restore(); });
- test('computed fields', () => {
- assert.equal(element._computeLabelClass({labels: {}}),
- 'cell label u-gray-background');
- assert.equal(element._computeLabelClass(
- {labels: {}}, 'Verified'), 'cell label u-gray-background');
- assert.equal(element._computeLabelClass(
- {labels: {Verified: {approved: true, value: 1}}}, 'Verified'),
- 'cell label u-green u-monospace');
- assert.equal(element._computeLabelClass(
- {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'),
- 'cell label u-monospace u-red');
- assert.equal(element._computeLabelClass(
- {labels: {'Code-Review': {value: 1}}}, 'Code-Review'),
- 'cell label u-green u-monospace');
- assert.equal(element._computeLabelClass(
- {labels: {'Code-Review': {value: -1}}}, 'Code-Review'),
- 'cell label u-monospace u-red');
- assert.equal(element._computeLabelClass(
- {labels: {'Code-Review': {value: -1}}}, 'Verified'),
- 'cell label u-gray-background');
+ test('computed fields', () => {
+ assert.equal(element._computeLabelClass({labels: {}}),
+ 'cell label u-gray-background');
+ assert.equal(element._computeLabelClass(
+ {labels: {}}, 'Verified'), 'cell label u-gray-background');
+ assert.equal(element._computeLabelClass(
+ {labels: {Verified: {approved: true, value: 1}}}, 'Verified'),
+ 'cell label u-green u-monospace');
+ assert.equal(element._computeLabelClass(
+ {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'),
+ 'cell label u-monospace u-red');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: 1}}}, 'Code-Review'),
+ 'cell label u-green u-monospace');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: -1}}}, 'Code-Review'),
+ 'cell label u-monospace u-red');
+ assert.equal(element._computeLabelClass(
+ {labels: {'Code-Review': {value: -1}}}, 'Verified'),
+ 'cell label u-gray-background');
- assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'),
- 'Label not applicable');
- assert.equal(element._computeLabelTitle(
- {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'),
- 'Verified\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'),
- 'Label not applicable');
- assert.equal(element._computeLabelTitle(
- {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'),
- 'Verified\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}},
- 'Code-Review'), 'Code-Review\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}},
- 'Code-Review'), 'Code-Review\nby Diffy');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {recommended: {name: 'Diffy'},
- rejected: {name: 'Admin'}}}}, 'Code-Review'),
- 'Code-Review\nby Admin');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {approved: {name: 'Diffy'},
- rejected: {name: 'Admin'}}}}, 'Code-Review'),
- 'Code-Review\nby Admin');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {recommended: {name: 'Diffy'},
- disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
- 'Code-Review\nby Admin');
- assert.equal(element._computeLabelTitle(
- {labels: {'Code-Review': {approved: {name: 'Diffy'},
- disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
- 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'),
+ 'Label not applicable');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'),
+ 'Verified\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'),
+ 'Label not applicable');
+ assert.equal(element._computeLabelTitle(
+ {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'),
+ 'Verified\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}},
+ 'Code-Review'), 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}},
+ 'Code-Review'), 'Code-Review\nby Diffy');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'},
+ rejected: {name: 'Admin'}}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {approved: {name: 'Diffy'},
+ rejected: {name: 'Admin'}}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {recommended: {name: 'Diffy'},
+ disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
+ 'Code-Review\nby Admin');
+ assert.equal(element._computeLabelTitle(
+ {labels: {'Code-Review': {approved: {name: 'Diffy'},
+ disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'),
+ 'Code-Review\nby Diffy');
- assert.equal(element._computeLabelValue({labels: {}}), '');
- assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), '');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {value: 1}}}, 'Verified'), '+1');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {value: -1}}}, 'Verified'), '-1');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {approved: true}}}, 'Verified'), '✓');
- assert.equal(element._computeLabelValue(
- {labels: {Verified: {rejected: true}}}, 'Verified'), '✕');
- });
+ assert.equal(element._computeLabelValue({labels: {}}), '');
+ assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), '');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {value: 1}}}, 'Verified'), '+1');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {value: -1}}}, 'Verified'), '-1');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {approved: true}}}, 'Verified'), '✓');
+ assert.equal(element._computeLabelValue(
+ {labels: {Verified: {rejected: true}}}, 'Verified'), '✕');
+ });
- test('no hidden columns', () => {
- element.visibleChangeTableColumns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ];
+ test('no hidden columns', () => {
+ element.visibleChangeTableColumns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
- flushAsynchronousOperations();
+ flushAsynchronousOperations();
- for (const column of element.columnNames) {
- const elementClass = '.' + column.toLowerCase();
- assert.isOk(element.shadowRoot
- .querySelector(elementClass),
- `Expect ${elementClass} element to be found`);
+ for (const column of element.columnNames) {
+ const elementClass = '.' + column.toLowerCase();
+ assert.isOk(element.shadowRoot
+ .querySelector(elementClass),
+ `Expect ${elementClass} element to be found`);
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ });
+
+ test('repo column hidden', () => {
+ element.visibleChangeTableColumns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ];
+
+ flushAsynchronousOperations();
+
+ for (const column of element.columnNames) {
+ const elementClass = '.' + column.toLowerCase();
+ if (column === 'Repo') {
+ assert.isTrue(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ } else {
assert.isFalse(element.shadowRoot
.querySelector(elementClass).hidden);
}
- });
-
- test('repo column hidden', () => {
- element.visibleChangeTableColumns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Branch',
- 'Updated',
- 'Size',
- ];
-
- flushAsynchronousOperations();
-
- for (const column of element.columnNames) {
- const elementClass = '.' + column.toLowerCase();
- if (column === 'Repo') {
- assert.isTrue(element.shadowRoot
- .querySelector(elementClass).hidden);
- } else {
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- }
- });
-
- test('random column does not exist', () => {
- element.visibleChangeTableColumns = [
- 'Bad',
- ];
-
- flushAsynchronousOperations();
- const elementClass = '.bad';
- assert.isNotOk(element.shadowRoot
- .querySelector(elementClass));
- });
-
- test('assignee only displayed if there is one', () => {
- element.change = {};
- flushAsynchronousOperations();
- assert.isNotOk(element.shadowRoot
- .querySelector('.assignee gr-account-link'));
- assert.equal(element.shadowRoot
- .querySelector('.assignee').textContent.trim(), '--');
- element.change = {
- assignee: {
- name: 'test',
- status: 'test',
- },
- };
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('.assignee gr-account-link'));
- });
-
- test('TShirt sizing tooltip', () => {
- assert.equal(element._computeSizeTooltip({
- insertions: 'foo',
- deletions: 'bar',
- }), 'Size unknown');
- assert.equal(element._computeSizeTooltip({
- insertions: 0,
- deletions: 0,
- }), 'Size unknown');
- assert.equal(element._computeSizeTooltip({
- insertions: 1,
- deletions: 2,
- }), '+1, -2');
- });
-
- test('TShirt sizing', () => {
- assert.equal(element._computeChangeSize({
- insertions: 'foo',
- deletions: 'bar',
- }), null);
- assert.equal(element._computeChangeSize({
- insertions: 1,
- deletions: 1,
- }), 'XS');
- assert.equal(element._computeChangeSize({
- insertions: 9,
- deletions: 1,
- }), 'S');
- assert.equal(element._computeChangeSize({
- insertions: 10,
- deletions: 200,
- }), 'M');
- assert.equal(element._computeChangeSize({
- insertions: 99,
- deletions: 900,
- }), 'L');
- assert.equal(element._computeChangeSize({
- insertions: 99,
- deletions: 999,
- }), 'XL');
- });
-
- test('change params passed to gr-navigation', () => {
- sandbox.stub(Gerrit.Nav);
- const change = {
- internalHost: 'test-host',
- project: 'test-repo',
- topic: 'test-topic',
- branch: 'test-branch',
- };
- element.change = change;
- flushAsynchronousOperations();
-
- assert.deepEqual(Gerrit.Nav.getUrlForChange.lastCall.args, [change]);
- assert.deepEqual(Gerrit.Nav.getUrlForProjectChanges.lastCall.args,
- [change.project, true, change.internalHost]);
- assert.deepEqual(Gerrit.Nav.getUrlForBranch.lastCall.args,
- [change.branch, change.project, null, change.internalHost]);
- assert.deepEqual(Gerrit.Nav.getUrlForTopic.lastCall.args,
- [change.topic, change.internalHost]);
- });
-
- test('_computeRepoDisplay', () => {
- const change = {
- project: 'a/test/repo',
- internalHost: 'host',
- };
- assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
- assert.equal(element._computeRepoDisplay(change, true),
- 'host/…/test/repo');
- delete change.internalHost;
- assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
- assert.equal(element._computeRepoDisplay(change, true),
- '…/test/repo');
- });
+ }
});
+
+ test('random column does not exist', () => {
+ element.visibleChangeTableColumns = [
+ 'Bad',
+ ];
+
+ flushAsynchronousOperations();
+ const elementClass = '.bad';
+ assert.isNotOk(element.shadowRoot
+ .querySelector(elementClass));
+ });
+
+ test('assignee only displayed if there is one', () => {
+ element.change = {};
+ flushAsynchronousOperations();
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.assignee gr-account-link'));
+ assert.equal(element.shadowRoot
+ .querySelector('.assignee').textContent.trim(), '--');
+ element.change = {
+ assignee: {
+ name: 'test',
+ status: 'test',
+ },
+ };
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('.assignee gr-account-link'));
+ });
+
+ test('TShirt sizing tooltip', () => {
+ assert.equal(element._computeSizeTooltip({
+ insertions: 'foo',
+ deletions: 'bar',
+ }), 'Size unknown');
+ assert.equal(element._computeSizeTooltip({
+ insertions: 0,
+ deletions: 0,
+ }), 'Size unknown');
+ assert.equal(element._computeSizeTooltip({
+ insertions: 1,
+ deletions: 2,
+ }), '+1, -2');
+ });
+
+ test('TShirt sizing', () => {
+ assert.equal(element._computeChangeSize({
+ insertions: 'foo',
+ deletions: 'bar',
+ }), null);
+ assert.equal(element._computeChangeSize({
+ insertions: 1,
+ deletions: 1,
+ }), 'XS');
+ assert.equal(element._computeChangeSize({
+ insertions: 9,
+ deletions: 1,
+ }), 'S');
+ assert.equal(element._computeChangeSize({
+ insertions: 10,
+ deletions: 200,
+ }), 'M');
+ assert.equal(element._computeChangeSize({
+ insertions: 99,
+ deletions: 900,
+ }), 'L');
+ assert.equal(element._computeChangeSize({
+ insertions: 99,
+ deletions: 999,
+ }), 'XL');
+ });
+
+ test('change params passed to gr-navigation', () => {
+ sandbox.stub(Gerrit.Nav);
+ const change = {
+ internalHost: 'test-host',
+ project: 'test-repo',
+ topic: 'test-topic',
+ branch: 'test-branch',
+ };
+ element.change = change;
+ flushAsynchronousOperations();
+
+ assert.deepEqual(Gerrit.Nav.getUrlForChange.lastCall.args, [change]);
+ assert.deepEqual(Gerrit.Nav.getUrlForProjectChanges.lastCall.args,
+ [change.project, true, change.internalHost]);
+ assert.deepEqual(Gerrit.Nav.getUrlForBranch.lastCall.args,
+ [change.branch, change.project, null, change.internalHost]);
+ assert.deepEqual(Gerrit.Nav.getUrlForTopic.lastCall.args,
+ [change.topic, change.internalHost]);
+ });
+
+ test('_computeRepoDisplay', () => {
+ const change = {
+ project: 'a/test/repo',
+ internalHost: 'host',
+ };
+ assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
+ assert.equal(element._computeRepoDisplay(change, true),
+ 'host/…/test/repo');
+ delete change.internalHost;
+ assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
+ assert.equal(element._computeRepoDisplay(change, true),
+ '…/test/repo');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index b82593d..383aafa 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.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,282 +14,298 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const LookupQueryPatterns = {
- CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i,
- CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
- };
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-change-list/gr-change-list.js';
+import '../gr-repo-header/gr-repo-header.js';
+import '../gr-user-header/gr-user-header.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-change-list-view_html.js';
- const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
+const LookupQueryPatterns = {
+ CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i,
+ CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g,
+};
- const REPO_QUERY_PATTERN =
- /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/;
+const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
- const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
+const REPO_QUERY_PATTERN =
+ /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/;
+const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrChangeListView extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-list-view'; }
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
*/
- class GrChangeListView extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-list-view'; }
+
+ static get properties() {
+ return {
/**
- * Fired when the title of the page should change.
- *
- * @event title-change
+ * URL params passed from the router.
*/
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
- static get properties() {
- return {
/**
- * URL params passed from the router.
+ * True when user is logged in.
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
+ _loggedIn: {
+ type: Boolean,
+ computed: '_computeLoggedIn(account)',
+ },
- /**
- * True when user is logged in.
- */
- _loggedIn: {
- type: Boolean,
- computed: '_computeLoggedIn(account)',
- },
+ account: {
+ type: Object,
+ value: null,
+ },
- account: {
- type: Object,
- value: null,
- },
+ /**
+ * State persisted across restamps of the element.
+ *
+ * Need sub-property declaration since it is used in template before
+ * assignment.
+ *
+ * @type {{ selectedChangeIndex: (number|undefined) }}
+ *
+ */
+ viewState: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ },
- /**
- * State persisted across restamps of the element.
- *
- * Need sub-property declaration since it is used in template before
- * assignment.
- *
- * @type {{ selectedChangeIndex: (number|undefined) }}
- *
- */
- viewState: {
- type: Object,
- notify: true,
- value() { return {}; },
- },
+ preferences: Object,
- preferences: Object,
+ _changesPerPage: Number,
- _changesPerPage: Number,
+ /**
+ * Currently active query.
+ */
+ _query: {
+ type: String,
+ value: '',
+ },
- /**
- * Currently active query.
- */
- _query: {
- type: String,
- value: '',
- },
+ /**
+ * Offset of currently visible query results.
+ */
+ _offset: Number,
- /**
- * Offset of currently visible query results.
- */
- _offset: Number,
+ /**
+ * Change objects loaded from the server.
+ */
+ _changes: {
+ type: Array,
+ observer: '_changesChanged',
+ },
- /**
- * Change objects loaded from the server.
- */
- _changes: {
- type: Array,
- observer: '_changesChanged',
- },
+ /**
+ * For showing a "loading..." string during ajax requests.
+ */
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
- /**
- * For showing a "loading..." string during ajax requests.
- */
- _loading: {
- type: Boolean,
- value: true,
- },
+ /** @type {?string} */
+ _userId: {
+ type: String,
+ value: null,
+ },
- /** @type {?string} */
- _userId: {
- type: String,
- value: null,
- },
+ /** @type {?string} */
+ _repo: {
+ type: String,
+ value: null,
+ },
+ };
+ }
- /** @type {?string} */
- _repo: {
- type: String,
- value: null,
- },
- };
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('next-page',
+ () => this._handleNextPage());
+ this.addEventListener('previous-page',
+ () => this._handlePreviousPage());
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadPreferences();
+ }
+
+ _paramsChanged(value) {
+ if (value.view !== Gerrit.Nav.View.SEARCH) { return; }
+
+ this._loading = true;
+ this._query = value.query;
+ this._offset = value.offset || 0;
+ if (this.viewState.query != this._query ||
+ this.viewState.offset != this._offset) {
+ this.set('viewState.selectedChangeIndex', 0);
+ this.set('viewState.query', this._query);
+ this.set('viewState.offset', this._offset);
}
- /** @override */
- created() {
- super.created();
- this.addEventListener('next-page',
- () => this._handleNextPage());
- this.addEventListener('previous-page',
- () => this._handlePreviousPage());
- }
+ // NOTE: This method may be called before attachment. Fire title-change
+ // in an async so that attachment to the DOM can take place first.
+ this.async(() => this.fire('title-change', {title: this._query}));
- /** @override */
- attached() {
- super.attached();
- this._loadPreferences();
- }
-
- _paramsChanged(value) {
- if (value.view !== Gerrit.Nav.View.SEARCH) { return; }
-
- this._loading = true;
- this._query = value.query;
- this._offset = value.offset || 0;
- if (this.viewState.query != this._query ||
- this.viewState.offset != this._offset) {
- this.set('viewState.selectedChangeIndex', 0);
- this.set('viewState.query', this._query);
- this.set('viewState.offset', this._offset);
- }
-
- // NOTE: This method may be called before attachment. Fire title-change
- // in an async so that attachment to the DOM can take place first.
- this.async(() => this.fire('title-change', {title: this._query}));
-
- this._getPreferences()
- .then(prefs => {
- this._changesPerPage = prefs.changes_per_page;
- return this._getChanges();
- })
- .then(changes => {
- changes = changes || [];
- if (this._query && changes.length === 1) {
- for (const query in LookupQueryPatterns) {
- if (LookupQueryPatterns.hasOwnProperty(query) &&
- this._query.match(LookupQueryPatterns[query])) {
- Gerrit.Nav.navigateToChange(changes[0]);
- return;
- }
+ this._getPreferences()
+ .then(prefs => {
+ this._changesPerPage = prefs.changes_per_page;
+ return this._getChanges();
+ })
+ .then(changes => {
+ changes = changes || [];
+ if (this._query && changes.length === 1) {
+ for (const query in LookupQueryPatterns) {
+ if (LookupQueryPatterns.hasOwnProperty(query) &&
+ this._query.match(LookupQueryPatterns[query])) {
+ Gerrit.Nav.navigateToChange(changes[0]);
+ return;
}
}
- this._changes = changes;
- this._loading = false;
- });
- }
+ }
+ this._changes = changes;
+ this._loading = false;
+ });
+ }
- _loadPreferences() {
- return this.$.restAPI.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- this._getPreferences().then(preferences => {
- this.preferences = preferences;
- });
- } else {
- this.preferences = {};
- }
- });
- }
-
- _getChanges() {
- return this.$.restAPI.getChanges(this._changesPerPage, this._query,
- this._offset);
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- _limitFor(query, defaultLimit) {
- const match = query.match(LIMIT_OPERATOR_PATTERN);
- if (!match) {
- return defaultLimit;
+ _loadPreferences() {
+ return this.$.restAPI.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ this._getPreferences().then(preferences => {
+ this.preferences = preferences;
+ });
+ } else {
+ this.preferences = {};
}
- return parseInt(match[1], 10);
- }
+ });
+ }
- _computeNavLink(query, offset, direction, changesPerPage) {
- // Offset could be a string when passed from the router.
- offset = +(offset || 0);
- const limit = this._limitFor(query, changesPerPage);
- const newOffset = Math.max(0, offset + (limit * direction));
- return Gerrit.Nav.getUrlForSearchQuery(query, newOffset);
- }
+ _getChanges() {
+ return this.$.restAPI.getChanges(this._changesPerPage, this._query,
+ this._offset);
+ }
- _computePrevArrowClass(offset) {
- return offset === 0 ? 'hide' : '';
- }
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
- _computeNextArrowClass(changes) {
- const more = changes.length && changes[changes.length - 1]._more_changes;
- return more ? '' : 'hide';
+ _limitFor(query, defaultLimit) {
+ const match = query.match(LIMIT_OPERATOR_PATTERN);
+ if (!match) {
+ return defaultLimit;
}
+ return parseInt(match[1], 10);
+ }
- _computeNavClass(loading) {
- return loading || !this._changes || !this._changes.length ? 'hide' : '';
+ _computeNavLink(query, offset, direction, changesPerPage) {
+ // Offset could be a string when passed from the router.
+ offset = +(offset || 0);
+ const limit = this._limitFor(query, changesPerPage);
+ const newOffset = Math.max(0, offset + (limit * direction));
+ return Gerrit.Nav.getUrlForSearchQuery(query, newOffset);
+ }
+
+ _computePrevArrowClass(offset) {
+ return offset === 0 ? 'hide' : '';
+ }
+
+ _computeNextArrowClass(changes) {
+ const more = changes.length && changes[changes.length - 1]._more_changes;
+ return more ? '' : 'hide';
+ }
+
+ _computeNavClass(loading) {
+ return loading || !this._changes || !this._changes.length ? 'hide' : '';
+ }
+
+ _handleNextPage() {
+ if (this.$.nextArrow.hidden) { return; }
+ page.show(this._computeNavLink(
+ this._query, this._offset, 1, this._changesPerPage));
+ }
+
+ _handlePreviousPage() {
+ if (this.$.prevArrow.hidden) { return; }
+ page.show(this._computeNavLink(
+ this._query, this._offset, -1, this._changesPerPage));
+ }
+
+ _changesChanged(changes) {
+ this._userId = null;
+ this._repo = null;
+ if (!changes || !changes.length) {
+ return;
}
-
- _handleNextPage() {
- if (this.$.nextArrow.hidden) { return; }
- page.show(this._computeNavLink(
- this._query, this._offset, 1, this._changesPerPage));
- }
-
- _handlePreviousPage() {
- if (this.$.prevArrow.hidden) { return; }
- page.show(this._computeNavLink(
- this._query, this._offset, -1, this._changesPerPage));
- }
-
- _changesChanged(changes) {
- this._userId = null;
- this._repo = null;
- if (!changes || !changes.length) {
+ if (USER_QUERY_PATTERN.test(this._query)) {
+ const owner = changes[0].owner;
+ const userId = owner._account_id ? owner._account_id : owner.email;
+ if (userId) {
+ this._userId = userId;
return;
}
- if (USER_QUERY_PATTERN.test(this._query)) {
- const owner = changes[0].owner;
- const userId = owner._account_id ? owner._account_id : owner.email;
- if (userId) {
- this._userId = userId;
- return;
- }
- }
- if (REPO_QUERY_PATTERN.test(this._query)) {
- this._repo = changes[0].project;
- }
}
-
- _computeHeaderClass(id) {
- return id ? '' : 'hide';
- }
-
- _computePage(offset, changesPerPage) {
- return offset / changesPerPage + 1;
- }
-
- _computeLoggedIn(account) {
- return !!(account && Object.keys(account).length > 0);
- }
-
- _handleToggleStar(e) {
- this.$.restAPI.saveChangeStarred(e.detail.change._number,
- e.detail.starred);
- }
-
- _handleToggleReviewed(e) {
- this.$.restAPI.saveChangeReviewed(e.detail.change._number,
- e.detail.reviewed);
+ if (REPO_QUERY_PATTERN.test(this._query)) {
+ this._repo = changes[0].project;
}
}
- customElements.define(GrChangeListView.is, GrChangeListView);
-})();
+ _computeHeaderClass(id) {
+ return id ? '' : 'hide';
+ }
+
+ _computePage(offset, changesPerPage) {
+ return offset / changesPerPage + 1;
+ }
+
+ _computeLoggedIn(account) {
+ return !!(account && Object.keys(account).length > 0);
+ }
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ }
+
+ _handleToggleReviewed(e) {
+ this.$.restAPI.saveChangeReviewed(e.detail.change._number,
+ e.detail.reviewed);
+ }
+}
+
+customElements.define(GrChangeListView.is, GrChangeListView);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
index 6c9d975..bed3985 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
@@ -1,34 +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/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-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-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-change-list/gr-change-list.html">
-<link rel="import" href="../gr-repo-header/gr-repo-header.html">
-<link rel="import" href="../gr-user-header/gr-user-header.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-list-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -70,39 +58,20 @@
}
}
</style>
- <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
- <div hidden$="[[_loading]]" hidden>
- <gr-repo-header
- repo="[[_repo]]"
- class$="[[_computeHeaderClass(_repo)]]"></gr-repo-header>
- <gr-user-header
- user-id="[[_userId]]"
- show-dashboard-link
- logged-in="[[_loggedIn]]"
- class$="[[_computeHeaderClass(_userId)]]"></gr-user-header>
- <gr-change-list
- account="[[account]]"
- changes="{{_changes}}"
- preferences="[[preferences]]"
- selected-index="{{viewState.selectedChangeIndex}}"
- show-star="[[_loggedIn]]"
- on-toggle-star="_handleToggleStar"
- on-toggle-reviewed="_handleToggleReviewed"></gr-change-list>
- <nav class$="[[_computeNavClass(_loading)]]">
+ <div class="loading" hidden\$="[[!_loading]]" hidden="">Loading...</div>
+ <div hidden\$="[[_loading]]" hidden="">
+ <gr-repo-header repo="[[_repo]]" class\$="[[_computeHeaderClass(_repo)]]"></gr-repo-header>
+ <gr-user-header user-id="[[_userId]]" show-dashboard-link="" logged-in="[[_loggedIn]]" class\$="[[_computeHeaderClass(_userId)]]"></gr-user-header>
+ <gr-change-list account="[[account]]" changes="{{_changes}}" preferences="[[preferences]]" selected-index="{{viewState.selectedChangeIndex}}" show-star="[[_loggedIn]]" on-toggle-star="_handleToggleStar" on-toggle-reviewed="_handleToggleReviewed"></gr-change-list>
+ <nav class\$="[[_computeNavClass(_loading)]]">
Page [[_computePage(_offset, _changesPerPage)]]
- <a id="prevArrow"
- href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
- class$="[[_computePrevArrowClass(_offset)]]">
+ <a id="prevArrow" href\$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]" class\$="[[_computePrevArrowClass(_offset)]]">
<iron-icon icon="gr-icons:chevron-left"></iron-icon>
</a>
- <a id="nextArrow"
- href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
- class$="[[_computeNextArrowClass(_changes)]]">
+ <a id="nextArrow" href\$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]" class\$="[[_computeNextArrowClass(_changes)]]">
<iron-icon icon="gr-icons:chevron-right"></iron-icon>
</a>
</nav>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-change-list-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index 1d81ab7..468cd41 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-view</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/page/page.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-change-list-view.html">
+<script src="/node_modules/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 type="module" src="./gr-change-list-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-list-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,26 +41,170 @@
</template>
</test-fixture>
-<script>
- const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
- const COMMIT_HASH = '12345678';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-list-view.js';
+const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
+const COMMIT_HASH = '12345678';
- suite('gr-change-list-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+suite('gr-change-list-view tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getChanges(num, query) {
+ return Promise.resolve([]);
+ },
+ getAccountDetails() { return Promise.resolve({}); },
+ getAccountStatus() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(done => {
+ flush(() => {
+ sandbox.restore();
+ done();
+ });
+ });
+
+ test('_computePage', () => {
+ assert.equal(element._computePage(0, 25), 1);
+ assert.equal(element._computePage(50, 25), 3);
+ });
+
+ test('_limitFor', () => {
+ const defaultLimit = 25;
+ const _limitFor = q => element._limitFor(q, defaultLimit);
+ assert.equal(_limitFor(''), defaultLimit);
+ assert.equal(_limitFor('limit:10'), 10);
+ assert.equal(_limitFor('xlimit:10'), defaultLimit);
+ assert.equal(_limitFor('x(limit:10'), 10);
+ });
+
+ test('_computeNavLink', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForSearchQuery')
+ .returns('');
+ const query = 'status:open';
+ let offset = 0;
+ let direction = 1;
+ const changesPerPage = 5;
+
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 5);
+
+ direction = -1;
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 0);
+
+ offset = 5;
+ direction = 1;
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 10);
+ });
+
+ test('_computePrevArrowClass', () => {
+ let offset = 0;
+ assert.equal(element._computePrevArrowClass(offset), 'hide');
+ offset = 5;
+ assert.equal(element._computePrevArrowClass(offset), '');
+ });
+
+ test('_computeNextArrowClass', () => {
+ let changes = _.times(25, _.constant({_more_changes: true}));
+ assert.equal(element._computeNextArrowClass(changes), '');
+ changes = _.times(25, _.constant({}));
+ assert.equal(element._computeNextArrowClass(changes), 'hide');
+ });
+
+ test('_computeNavClass', () => {
+ let loading = true;
+ assert.equal(element._computeNavClass(loading), 'hide');
+ loading = false;
+ assert.equal(element._computeNavClass(loading), 'hide');
+ element._changes = [];
+ assert.equal(element._computeNavClass(loading), 'hide');
+ element._changes = _.times(5, _.constant({}));
+ assert.equal(element._computeNavClass(loading), '');
+ });
+
+ test('_handleNextPage', () => {
+ const showStub = sandbox.stub(page, 'show');
+ element.$.nextArrow.hidden = true;
+ element._handleNextPage();
+ assert.isFalse(showStub.called);
+ element.$.nextArrow.hidden = false;
+ element._handleNextPage();
+ assert.isTrue(showStub.called);
+ });
+
+ test('_handlePreviousPage', () => {
+ const showStub = sandbox.stub(page, 'show');
+ element.$.prevArrow.hidden = true;
+ element._handlePreviousPage();
+ assert.isFalse(showStub.called);
+ element.$.prevArrow.hidden = false;
+ element._handlePreviousPage();
+ assert.isTrue(showStub.called);
+ });
+
+ test('_userId query', done => {
+ assert.isNull(element._userId);
+ element._query = 'owner: foo@bar';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ flush(() => {
+ assert.equal(element._userId, 'foo@bar');
+
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._userId);
+
+ done();
+ });
+ });
+
+ test('_userId query without email', done => {
+ assert.isNull(element._userId);
+ element._query = 'owner: foo@bar';
+ element._changes = [{owner: {}}];
+ flush(() => {
+ assert.isNull(element._userId);
+ done();
+ });
+ });
+
+ test('_repo query', done => {
+ assert.isNull(element._repo);
+ element._query = 'project: test-repo';
+ element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
+ flush(() => {
+ assert.equal(element._repo, 'test-repo');
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._repo);
+ done();
+ });
+ });
+
+ test('_repo query with open status', done => {
+ assert.isNull(element._repo);
+ element._query = 'project:test-repo status:open';
+ element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
+ flush(() => {
+ assert.equal(element._repo, 'test-repo');
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._repo);
+ done();
+ });
+ });
+
+ suite('query based navigation', () => {
setup(() => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getChanges(num, query) {
- return Promise.resolve([]);
- },
- getAccountDetails() { return Promise.resolve({}); },
- getAccountStatus() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
});
teardown(done => {
@@ -65,205 +214,63 @@
});
});
- test('_computePage', () => {
- assert.equal(element._computePage(0, 25), 1);
- assert.equal(element._computePage(50, 25), 3);
- });
-
- test('_limitFor', () => {
- const defaultLimit = 25;
- const _limitFor = q => element._limitFor(q, defaultLimit);
- assert.equal(_limitFor(''), defaultLimit);
- assert.equal(_limitFor('limit:10'), 10);
- assert.equal(_limitFor('xlimit:10'), defaultLimit);
- assert.equal(_limitFor('x(limit:10'), 10);
- });
-
- test('_computeNavLink', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForSearchQuery')
- .returns('');
- const query = 'status:open';
- let offset = 0;
- let direction = 1;
- const changesPerPage = 5;
-
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 5);
-
- direction = -1;
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 0);
-
- offset = 5;
- direction = 1;
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 10);
- });
-
- test('_computePrevArrowClass', () => {
- let offset = 0;
- assert.equal(element._computePrevArrowClass(offset), 'hide');
- offset = 5;
- assert.equal(element._computePrevArrowClass(offset), '');
- });
-
- test('_computeNextArrowClass', () => {
- let changes = _.times(25, _.constant({_more_changes: true}));
- assert.equal(element._computeNextArrowClass(changes), '');
- changes = _.times(25, _.constant({}));
- assert.equal(element._computeNextArrowClass(changes), 'hide');
- });
-
- test('_computeNavClass', () => {
- let loading = true;
- assert.equal(element._computeNavClass(loading), 'hide');
- loading = false;
- assert.equal(element._computeNavClass(loading), 'hide');
- element._changes = [];
- assert.equal(element._computeNavClass(loading), 'hide');
- element._changes = _.times(5, _.constant({}));
- assert.equal(element._computeNavClass(loading), '');
- });
-
- test('_handleNextPage', () => {
- const showStub = sandbox.stub(page, 'show');
- element.$.nextArrow.hidden = true;
- element._handleNextPage();
- assert.isFalse(showStub.called);
- element.$.nextArrow.hidden = false;
- element._handleNextPage();
- assert.isTrue(showStub.called);
- });
-
- test('_handlePreviousPage', () => {
- const showStub = sandbox.stub(page, 'show');
- element.$.prevArrow.hidden = true;
- element._handlePreviousPage();
- assert.isFalse(showStub.called);
- element.$.prevArrow.hidden = false;
- element._handlePreviousPage();
- assert.isTrue(showStub.called);
- });
-
- test('_userId query', done => {
- assert.isNull(element._userId);
- element._query = 'owner: foo@bar';
- element._changes = [{owner: {email: 'foo@bar'}}];
- flush(() => {
- assert.equal(element._userId, 'foo@bar');
-
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._userId);
-
+ test('Searching for a change ID redirects to change', done => {
+ const change = {_number: 1};
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([change]));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+ assert.equal(url, change);
done();
});
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
});
- test('_userId query without email', done => {
- assert.isNull(element._userId);
- element._query = 'owner: foo@bar';
- element._changes = [{owner: {}}];
- flush(() => {
- assert.isNull(element._userId);
+ test('Searching for a change num redirects to change', done => {
+ const change = {_number: 1};
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([change]));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+ assert.equal(url, change);
done();
});
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: '1'};
});
- test('_repo query', done => {
- assert.isNull(element._repo);
- element._query = 'project: test-repo';
- element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
- flush(() => {
- assert.equal(element._repo, 'test-repo');
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._repo);
+ test('Commit hash redirects to change', done => {
+ const change = {_number: 1};
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([change]));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
+ assert.equal(url, change);
done();
});
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: COMMIT_HASH};
});
- test('_repo query with open status', done => {
- assert.isNull(element._repo);
- element._query = 'project:test-repo status:open';
- element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
- flush(() => {
- assert.equal(element._repo, 'test-repo');
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._repo);
- done();
- });
+ test('Searching for an invalid change ID searches', () => {
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([]));
+ const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
+ flushAsynchronousOperations();
+
+ assert.isFalse(stub.called);
});
- suite('query based navigation', () => {
- setup(() => {
- });
+ test('Change ID with multiple search results searches', () => {
+ sandbox.stub(element, '_getChanges')
+ .returns(Promise.resolve([{}, {}]));
+ const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- teardown(done => {
- flush(() => {
- sandbox.restore();
- done();
- });
- });
+ element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
+ flushAsynchronousOperations();
- test('Searching for a change ID redirects to change', done => {
- const change = {_number: 1};
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
- assert.equal(url, change);
- done();
- });
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
- });
-
- test('Searching for a change num redirects to change', done => {
- const change = {_number: 1};
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
- assert.equal(url, change);
- done();
- });
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: '1'};
- });
-
- test('Commit hash redirects to change', done => {
- const change = {_number: 1};
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- sandbox.stub(Gerrit.Nav, 'navigateToChange', url => {
- assert.equal(url, change);
- done();
- });
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: COMMIT_HASH};
- });
-
- test('Searching for an invalid change ID searches', () => {
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([]));
- const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
- flushAsynchronousOperations();
-
- assert.isFalse(stub.called);
- });
-
- test('Change ID with multiple search results searches', () => {
- sandbox.stub(element, '_getChanges')
- .returns(Promise.resolve([{}, {}]));
- const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID};
- flushAsynchronousOperations();
-
- assert.isFalse(stub.called);
- });
+ assert.isFalse(stub.called);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 30df8fd..33c2ea2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.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,403 +14,423 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const NUMBER_FIXED_COLUMNS = 3;
- const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
- const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
- const MAX_SHORTCUT_CHARS = 5;
+import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-change-list-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-change-list-item/gr-change-list-item.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.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-change-list_html.js';
+
+const NUMBER_FIXED_COLUMNS = 3;
+const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
+const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
+const MAX_SHORTCUT_CHARS = 5;
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrChangeList extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.ChangeTableBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.RESTClientBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-list'; }
+ /**
+ * Fired when next page key shortcut was pressed.
+ *
+ * @event next-page
+ */
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.ChangeTableMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
+ * Fired when previous page key shortcut was pressed.
+ *
+ * @event previous-page
*/
- class GrChangeList extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.ChangeTableBehavior,
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.RESTClientBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-list'; }
- /**
- * Fired when next page key shortcut was pressed.
- *
- * @event next-page
- */
+ static get properties() {
+ return {
/**
- * Fired when previous page key shortcut was pressed.
- *
- * @event previous-page
+ * The logged-in user's account, or an empty object if no user is logged
+ * in.
*/
-
- static get properties() {
- return {
+ account: {
+ type: Object,
+ value: null,
+ },
/**
- * The logged-in user's account, or an empty object if no user is logged
- * in.
+ * An array of ChangeInfo objects to render.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
*/
- account: {
- type: Object,
- value: null,
- },
- /**
- * An array of ChangeInfo objects to render.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
- */
- changes: {
- type: Array,
- observer: '_changesChanged',
- },
- /**
- * ChangeInfo objects grouped into arrays. The sections and changes
- * properties should not be used together.
- *
- * @type {!Array<{
- * name: string,
- * query: string,
- * results: !Array<!Object>
- * }>}
- */
- sections: {
- type: Array,
- value() { return []; },
- },
- labelNames: {
- type: Array,
- computed: '_computeLabelNames(sections)',
- },
- _dynamicHeaderEndpoints: {
- type: Array,
- },
- selectedIndex: {
- type: Number,
- notify: true,
- },
- showNumber: Boolean, // No default value to prevent flickering.
- showStar: {
- type: Boolean,
- value: false,
- },
- showReviewedState: {
- type: Boolean,
- value: false,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- changeTableColumns: Array,
- visibleChangeTableColumns: Array,
- preferences: Object,
- };
- }
+ changes: {
+ type: Array,
+ observer: '_changesChanged',
+ },
+ /**
+ * ChangeInfo objects grouped into arrays. The sections and changes
+ * properties should not be used together.
+ *
+ * @type {!Array<{
+ * name: string,
+ * query: string,
+ * results: !Array<!Object>
+ * }>}
+ */
+ sections: {
+ type: Array,
+ value() { return []; },
+ },
+ labelNames: {
+ type: Array,
+ computed: '_computeLabelNames(sections)',
+ },
+ _dynamicHeaderEndpoints: {
+ type: Array,
+ },
+ selectedIndex: {
+ type: Number,
+ notify: true,
+ },
+ showNumber: Boolean, // No default value to prevent flickering.
+ showStar: {
+ type: Boolean,
+ value: false,
+ },
+ showReviewedState: {
+ type: Boolean,
+ value: false,
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ changeTableColumns: Array,
+ visibleChangeTableColumns: Array,
+ preferences: Object,
+ };
+ }
- static get observers() {
- return [
- '_sectionsChanged(sections.*)',
- '_computePreferences(account, preferences)',
- ];
- }
+ static get observers() {
+ return [
+ '_sectionsChanged(sections.*)',
+ '_computePreferences(account, preferences)',
+ ];
+ }
- keyboardShortcuts() {
- return {
- [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
- [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
- [this.Shortcut.NEXT_PAGE]: '_nextPage',
- [this.Shortcut.PREV_PAGE]: '_prevPage',
- [this.Shortcut.OPEN_CHANGE]: '_openChange',
- [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
- [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
- [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
- };
- }
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
+ [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
+ [this.Shortcut.NEXT_PAGE]: '_nextPage',
+ [this.Shortcut.PREV_PAGE]: '_prevPage',
+ [this.Shortcut.OPEN_CHANGE]: '_openChange',
+ [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
+ [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
+ [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
+ };
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('keydown',
- e => this._scopedKeydownHandler(e));
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('keydown',
+ e => this._scopedKeydownHandler(e));
+ }
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('tabindex', 0);
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('tabindex', 0);
+ }
- /** @override */
- attached() {
- super.attached();
- Gerrit.awaitPluginsLoaded().then(() => {
- this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-list-header');
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ Gerrit.awaitPluginsLoaded().then(() => {
+ this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-list-header');
+ });
+ }
- /**
- * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
- * events must be scoped to a component level (e.g. `enter`) in order to not
- * override native browser functionality.
- *
- * Context: Issue 7294
- */
- _scopedKeydownHandler(e) {
- if (e.keyCode === 13) {
- // Enter.
- this._openChange(e);
- }
- }
-
- _lowerCase(column) {
- return column.toLowerCase();
- }
-
- _computePreferences(account, preferences) {
- // Polymer 2: check for undefined
- if ([account, preferences].some(arg => arg === undefined)) {
- return;
- }
-
- this.changeTableColumns = this.columnNames;
-
- if (account) {
- this.showNumber = !!(preferences &&
- preferences.legacycid_in_change_table);
- if (preferences.change_table &&
- preferences.change_table.length > 0) {
- this.visibleChangeTableColumns =
- this.getVisibleColumns(preferences.change_table);
- } else {
- this.visibleChangeTableColumns = this.columnNames;
- }
- } else {
- // Not logged in.
- this.showNumber = false;
- this.visibleChangeTableColumns = this.columnNames;
- }
- }
-
- _computeColspan(changeTableColumns, labelNames) {
- if (!changeTableColumns || !labelNames) return;
- return changeTableColumns.length + labelNames.length +
- NUMBER_FIXED_COLUMNS;
- }
-
- _computeLabelNames(sections) {
- if (!sections) { return []; }
- let labels = [];
- const nonExistingLabel = function(item) {
- return !labels.includes(item);
- };
- for (const section of sections) {
- if (!section.results) { continue; }
- for (const change of section.results) {
- if (!change.labels) { continue; }
- const currentLabels = Object.keys(change.labels);
- labels = labels.concat(currentLabels.filter(nonExistingLabel));
- }
- }
- return labels.sort();
- }
-
- _computeLabelShortcut(labelName) {
- if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
- labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
- }
- return labelName.split('-')
- .reduce((a, i) => {
- if (!i) { return a; }
- return a + i[0].toUpperCase();
- }, '')
- .slice(0, MAX_SHORTCUT_CHARS);
- }
-
- _changesChanged(changes) {
- this.sections = changes ? [{results: changes}] : [];
- }
-
- _processQuery(query) {
- let tokens = query.split(' ');
- const invalidTokens = ['limit:', 'age:', '-age:'];
- tokens = tokens.filter(token => !invalidTokens
- .some(invalidToken => token.startsWith(invalidToken)));
- return tokens.join(' ');
- }
-
- _sectionHref(query) {
- return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query));
- }
-
- /**
- * Maps an index local to a particular section to the absolute index
- * across all the changes on the page.
- *
- * @param {number} sectionIndex index of section
- * @param {number} localIndex index of row within section
- * @return {number} absolute index of row in the aggregate dashboard
- */
- _computeItemAbsoluteIndex(sectionIndex, localIndex) {
- let idx = 0;
- for (let i = 0; i < sectionIndex; i++) {
- idx += this.sections[i].results.length;
- }
- return idx + localIndex;
- }
-
- _computeItemSelected(sectionIndex, index, selectedIndex) {
- const idx = this._computeItemAbsoluteIndex(sectionIndex, index);
- return idx == selectedIndex;
- }
-
- _computeItemNeedsReview(account, change, showReviewedState) {
- return showReviewedState && !change.reviewed &&
- !change.work_in_progress &&
- this.changeIsOpen(change) &&
- (!account || account._account_id != change.owner._account_id);
- }
-
- _computeItemHighlight(account, change) {
- // Do not show the assignee highlight if the change is not open.
- if (!change ||!change.assignee ||
- !account ||
- CLOSED_STATUS.indexOf(change.status) !== -1) {
- return false;
- }
- return account._account_id === change.assignee._account_id;
- }
-
- _nextChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.next();
- }
-
- _prevChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.previous();
- }
-
- _openChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex));
- }
-
- _nextPage(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
- return;
- }
-
- e.preventDefault();
- this.fire('next-page');
- }
-
- _prevPage(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
- return;
- }
-
- e.preventDefault();
- this.fire('previous-page');
- }
-
- _toggleChangeReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._toggleReviewedForIndex(this.selectedIndex);
- }
-
- _toggleReviewedForIndex(index) {
- const changeEls = this._getListItems();
- if (index >= changeEls.length || !changeEls[index]) {
- return;
- }
-
- const changeEl = changeEls[index];
- changeEl.toggleReviewed();
- }
-
- _refreshChangeList(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._reloadWindow();
- }
-
- _reloadWindow() {
- window.location.reload();
- }
-
- _toggleChangeStar(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._toggleStarForIndex(this.selectedIndex);
- }
-
- _toggleStarForIndex(index) {
- const changeEls = this._getListItems();
- if (index >= changeEls.length || !changeEls[index]) {
- return;
- }
-
- const changeEl = changeEls[index];
- changeEl.shadowRoot
- .querySelector('gr-change-star').toggleStar();
- }
-
- _changeForIndex(index) {
- const changeEls = this._getListItems();
- if (index < changeEls.length && changeEls[index]) {
- return changeEls[index].change;
- }
- return null;
- }
-
- _getListItems() {
- return Array.from(
- Polymer.dom(this.root).querySelectorAll('gr-change-list-item'));
- }
-
- _sectionsChanged() {
- // Flush DOM operations so that the list item elements will be loaded.
- Polymer.RenderStatus.afterNextRender(this, () => {
- this.$.cursor.stops = this._getListItems();
- this.$.cursor.moveToStart();
- });
- }
-
- _isOutgoing(section) {
- return !!section.isOutgoing;
- }
-
- _isEmpty(section) {
- return !section.results.length;
+ /**
+ * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
+ * events must be scoped to a component level (e.g. `enter`) in order to not
+ * override native browser functionality.
+ *
+ * Context: Issue 7294
+ */
+ _scopedKeydownHandler(e) {
+ if (e.keyCode === 13) {
+ // Enter.
+ this._openChange(e);
}
}
- customElements.define(GrChangeList.is, GrChangeList);
-})();
+ _lowerCase(column) {
+ return column.toLowerCase();
+ }
+
+ _computePreferences(account, preferences) {
+ // Polymer 2: check for undefined
+ if ([account, preferences].some(arg => arg === undefined)) {
+ return;
+ }
+
+ this.changeTableColumns = this.columnNames;
+
+ if (account) {
+ this.showNumber = !!(preferences &&
+ preferences.legacycid_in_change_table);
+ if (preferences.change_table &&
+ preferences.change_table.length > 0) {
+ this.visibleChangeTableColumns =
+ this.getVisibleColumns(preferences.change_table);
+ } else {
+ this.visibleChangeTableColumns = this.columnNames;
+ }
+ } else {
+ // Not logged in.
+ this.showNumber = false;
+ this.visibleChangeTableColumns = this.columnNames;
+ }
+ }
+
+ _computeColspan(changeTableColumns, labelNames) {
+ if (!changeTableColumns || !labelNames) return;
+ return changeTableColumns.length + labelNames.length +
+ NUMBER_FIXED_COLUMNS;
+ }
+
+ _computeLabelNames(sections) {
+ if (!sections) { return []; }
+ let labels = [];
+ const nonExistingLabel = function(item) {
+ return !labels.includes(item);
+ };
+ for (const section of sections) {
+ if (!section.results) { continue; }
+ for (const change of section.results) {
+ if (!change.labels) { continue; }
+ const currentLabels = Object.keys(change.labels);
+ labels = labels.concat(currentLabels.filter(nonExistingLabel));
+ }
+ }
+ return labels.sort();
+ }
+
+ _computeLabelShortcut(labelName) {
+ if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
+ labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
+ }
+ return labelName.split('-')
+ .reduce((a, i) => {
+ if (!i) { return a; }
+ return a + i[0].toUpperCase();
+ }, '')
+ .slice(0, MAX_SHORTCUT_CHARS);
+ }
+
+ _changesChanged(changes) {
+ this.sections = changes ? [{results: changes}] : [];
+ }
+
+ _processQuery(query) {
+ let tokens = query.split(' ');
+ const invalidTokens = ['limit:', 'age:', '-age:'];
+ tokens = tokens.filter(token => !invalidTokens
+ .some(invalidToken => token.startsWith(invalidToken)));
+ return tokens.join(' ');
+ }
+
+ _sectionHref(query) {
+ return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query));
+ }
+
+ /**
+ * Maps an index local to a particular section to the absolute index
+ * across all the changes on the page.
+ *
+ * @param {number} sectionIndex index of section
+ * @param {number} localIndex index of row within section
+ * @return {number} absolute index of row in the aggregate dashboard
+ */
+ _computeItemAbsoluteIndex(sectionIndex, localIndex) {
+ let idx = 0;
+ for (let i = 0; i < sectionIndex; i++) {
+ idx += this.sections[i].results.length;
+ }
+ return idx + localIndex;
+ }
+
+ _computeItemSelected(sectionIndex, index, selectedIndex) {
+ const idx = this._computeItemAbsoluteIndex(sectionIndex, index);
+ return idx == selectedIndex;
+ }
+
+ _computeItemNeedsReview(account, change, showReviewedState) {
+ return showReviewedState && !change.reviewed &&
+ !change.work_in_progress &&
+ this.changeIsOpen(change) &&
+ (!account || account._account_id != change.owner._account_id);
+ }
+
+ _computeItemHighlight(account, change) {
+ // Do not show the assignee highlight if the change is not open.
+ if (!change ||!change.assignee ||
+ !account ||
+ CLOSED_STATUS.indexOf(change.status) !== -1) {
+ return false;
+ }
+ return account._account_id === change.assignee._account_id;
+ }
+
+ _nextChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.next();
+ }
+
+ _prevChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.previous();
+ }
+
+ _openChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex));
+ }
+
+ _nextPage(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
+ return;
+ }
+
+ e.preventDefault();
+ this.fire('next-page');
+ }
+
+ _prevPage(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
+ return;
+ }
+
+ e.preventDefault();
+ this.fire('previous-page');
+ }
+
+ _toggleChangeReviewed(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._toggleReviewedForIndex(this.selectedIndex);
+ }
+
+ _toggleReviewedForIndex(index) {
+ const changeEls = this._getListItems();
+ if (index >= changeEls.length || !changeEls[index]) {
+ return;
+ }
+
+ const changeEl = changeEls[index];
+ changeEl.toggleReviewed();
+ }
+
+ _refreshChangeList(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this._reloadWindow();
+ }
+
+ _reloadWindow() {
+ window.location.reload();
+ }
+
+ _toggleChangeStar(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._toggleStarForIndex(this.selectedIndex);
+ }
+
+ _toggleStarForIndex(index) {
+ const changeEls = this._getListItems();
+ if (index >= changeEls.length || !changeEls[index]) {
+ return;
+ }
+
+ const changeEl = changeEls[index];
+ changeEl.shadowRoot
+ .querySelector('gr-change-star').toggleStar();
+ }
+
+ _changeForIndex(index) {
+ const changeEls = this._getListItems();
+ if (index < changeEls.length && changeEls[index]) {
+ return changeEls[index].change;
+ }
+ return null;
+ }
+
+ _getListItems() {
+ return Array.from(
+ dom(this.root).querySelectorAll('gr-change-list-item'));
+ }
+
+ _sectionsChanged() {
+ // Flush DOM operations so that the list item elements will be loaded.
+ afterNextRender(this, () => {
+ this.$.cursor.stops = this._getListItems();
+ this.$.cursor.moveToStart();
+ });
+ }
+
+ _isOutgoing(section) {
+ return !!section.isOutgoing;
+ }
+
+ _isEmpty(section) {
+ return !section.results.length;
+ }
+}
+
+customElements.define(GrChangeList.is, GrChangeList);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
index 61b9960..ac37827 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
@@ -1,36 +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/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
-<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="../../../behaviors/rest-client-behavior/rest-client-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="../../../styles/gr-change-list-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-
-<dom-module id="gr-change-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -57,16 +43,14 @@
}
</style>
<table id="changeList">
- <template is="dom-repeat" items="[[sections]]" as="changeSection"
- index-as="sectionIndex">
+ <template is="dom-repeat" items="[[sections]]" as="changeSection" index-as="sectionIndex">
<template is="dom-if" if="[[changeSection.name]]">
<tbody>
<tr class="groupHeader">
<td class="leftPadding"></td>
- <td class="star" hidden$="[[!showStar]]" hidden></td>
- <td class="cell"
- colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
- <a href$="[[_sectionHref(changeSection.query)]]" class="section-title">
+ <td class="star" hidden\$="[[!showStar]]" hidden=""></td>
+ <td class="cell" colspan\$="[[_computeColspan(changeTableColumns, labelNames)]]">
+ <a href\$="[[_sectionHref(changeSection.query)]]" class="section-title">
<span class="section-name">[[changeSection.name]]</span>
<span class="section-count-label">[[changeSection.countLabel]]</span>
</a>
@@ -78,9 +62,8 @@
<template is="dom-if" if="[[_isEmpty(changeSection)]]">
<tr class="noChanges">
<td class="leftPadding"></td>
- <td class="star" hidden$="[[!showStar]]" hidden></td>
- <td class="cell"
- colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
+ <td class="star" hidden\$="[[!showStar]]" hidden=""></td>
+ <td class="cell" colspan\$="[[_computeColspan(changeTableColumns, labelNames)]]">
<template is="dom-if" if="[[_isOutgoing(changeSection)]]">
<slot name="empty-outgoing"></slot>
</template>
@@ -93,48 +76,31 @@
<template is="dom-if" if="[[!_isEmpty(changeSection)]]">
<tr class="groupTitle">
<td class="leftPadding"></td>
- <td class="star" hidden$="[[!showStar]]" hidden></td>
- <td class="number" hidden$="[[!showNumber]]" hidden>#</td>
+ <td class="star" hidden\$="[[!showStar]]" hidden=""></td>
+ <td class="number" hidden\$="[[!showNumber]]" hidden="">#</td>
<template is="dom-repeat" items="[[changeTableColumns]]" as="item">
- <td class$="[[_lowerCase(item)]]"
- hidden$="[[isColumnHidden(item, visibleChangeTableColumns)]]">
+ <td class\$="[[_lowerCase(item)]]" hidden\$="[[isColumnHidden(item, visibleChangeTableColumns)]]">
[[item]]
</td>
</template>
<template is="dom-repeat" items="[[labelNames]]" as="labelName">
- <td class="label" title$="[[labelName]]">
+ <td class="label" title\$="[[labelName]]">
[[_computeLabelShortcut(labelName)]]
</td>
</template>
- <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]"
- as="pluginHeader">
+ <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" as="pluginHeader">
<td class="endpoint">
- <gr-endpoint-decorator name$="[[pluginHeader]]">
+ <gr-endpoint-decorator name\$="[[pluginHeader]]">
</gr-endpoint-decorator>
</td>
</template>
</tr>
</template>
<template is="dom-repeat" items="[[changeSection.results]]" as="change">
- <gr-change-list-item
- selected$="[[_computeItemSelected(sectionIndex, index, selectedIndex)]]"
- highlight$="[[_computeItemHighlight(account, change)]]"
- needs-review$="[[_computeItemNeedsReview(account, change, showReviewedState)]]"
- change="[[change]]"
- visible-change-table-columns="[[visibleChangeTableColumns]]"
- show-number="[[showNumber]]"
- show-star="[[showStar]]"
- tabindex="0"
- label-names="[[labelNames]]"></gr-change-list-item>
+ <gr-change-list-item selected\$="[[_computeItemSelected(sectionIndex, index, selectedIndex)]]" highlight\$="[[_computeItemHighlight(account, change)]]" needs-review\$="[[_computeItemNeedsReview(account, change, showReviewedState)]]" change="[[change]]" visible-change-table-columns="[[visibleChangeTableColumns]]" show-number="[[showNumber]]" show-star="[[showStar]]" tabindex="0" label-names="[[labelNames]]"></gr-change-list-item>
</template>
</tbody>
</template>
</table>
- <gr-cursor-manager
- id="cursor"
- index="{{selectedIndex}}"
- scroll-behavior="keep-visible"
- focus-on-move></gr-cursor-manager>
- </template>
- <script src="gr-change-list.js"></script>
-</dom-module>
+ <gr-cursor-manager id="cursor" index="{{selectedIndex}}" scroll-behavior="keep-visible" focus-on-move=""></gr-cursor-manager>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 23a5046..6329666 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -19,17 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list</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-change-list.html">
+<script type="module" src="./gr-change-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -43,20 +48,420 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-list basic tests', async () => {
- await readyToTest();
- // Define keybindings before attaching other fixtures.
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
- kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
- kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
- kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
- kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
- kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
- kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
+suite('gr-change-list basic tests', () => {
+ // Define keybindings before attaching other fixtures.
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+ kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
+ kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
+ kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+ kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
+ kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ suite('test show change number not logged in', () => {
+ setup(() => {
+ element = fixture('basic');
+ element.account = null;
+ element.preferences = null;
+ });
+
+ test('show number disabled', () => {
+ assert.isFalse(element.showNumber);
+ });
+ });
+
+ suite('test show change number preference enabled', () => {
+ setup(() => {
+ element = fixture('basic');
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [],
+ };
+ element.account = {_account_id: 1001};
+ flushAsynchronousOperations();
+ });
+
+ test('show number enabled', () => {
+ assert.isTrue(element.showNumber);
+ });
+ });
+
+ suite('test show change number preference disabled', () => {
+ setup(() => {
+ element = fixture('basic');
+ // legacycid_in_change_table is not set when false.
+ element.preferences = {
+ time_format: 'HHMM_12',
+ change_table: [],
+ };
+ element.account = {_account_id: 1001};
+ flushAsynchronousOperations();
+ });
+
+ test('show number disabled', () => {
+ assert.isFalse(element.showNumber);
+ });
+ });
+
+ test('computed fields', () => {
+ assert.equal(element._computeLabelNames(
+ [{results: [{_number: 0, labels: {}}]}]).length, 0);
+ assert.equal(element._computeLabelNames([
+ {results: [
+ {_number: 0, labels: {Verified: {approved: {}}}},
+ {
+ _number: 1,
+ labels: {
+ 'Verified': {approved: {}},
+ 'Code-Review': {approved: {}},
+ },
+ },
+ {
+ _number: 2,
+ labels: {
+ 'Verified': {approved: {}},
+ 'Library-Compliance': {approved: {}},
+ },
+ },
+ ]},
+ ]).length, 3);
+
+ assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
+ assert.equal(element._computeLabelShortcut('Verified'), 'V');
+ assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC');
+ assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR');
+ assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR');
+ assert.equal(element._computeLabelShortcut(
+ 'Invalid-Prolog-Rules-Label-Name--Verified'), 'V');
+ assert.equal(element._computeLabelShortcut(
+ 'Some-Special-Label-7'), 'SSL7');
+ assert.equal(element._computeLabelShortcut('--Too----many----dashes---'),
+ 'TMD');
+ assert.equal(element._computeLabelShortcut(
+ 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL');
+ });
+
+ test('colspans', () => {
+ element.sections = [
+ {results: [{}]},
+ ];
+ flushAsynchronousOperations();
+ const tdItemCount = dom(element.root).querySelectorAll(
+ 'td').length;
+
+ const changeTableColumns = [];
+ const labelNames = [];
+ assert.equal(tdItemCount, element._computeColspan(
+ changeTableColumns, labelNames));
+ });
+
+ test('keyboard shortcuts', done => {
+ sandbox.stub(element, '_computeLabelNames');
+ element.sections = [
+ {results: new Array(1)},
+ {results: new Array(2)},
+ ];
+ element.selectedIndex = 0;
+ element.changes = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ flushAsynchronousOperations();
+ afterNextRender(element, () => {
+ const elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 3);
+
+ assert.isTrue(elementItems[0].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert.equal(element.selectedIndex, 1);
+ assert.isTrue(elementItems[1].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert.equal(element.selectedIndex, 2);
+ assert.isTrue(elementItems[2].hasAttribute('selected'));
+
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ assert.equal(element.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
+ 'Should navigate to /c/2/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
+ 'Should navigate to /c/1/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.selectedIndex, 0);
+
+ const reloadStub = sandbox.stub(element, '_reloadWindow');
+ MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ assert.isTrue(reloadStub.called);
+
+ done();
+ });
+ });
+
+ test('changes needing review', () => {
+ element.changes = [
+ {
+ _number: 0,
+ status: 'NEW',
+ reviewed: true,
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 1,
+ status: 'NEW',
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 2,
+ status: 'MERGED',
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 3,
+ status: 'ABANDONED',
+ owner: {_account_id: 0},
+ },
+ {
+ _number: 4,
+ status: 'NEW',
+ work_in_progress: true,
+ owner: {_account_id: 0},
+ },
+ ];
+ flushAsynchronousOperations();
+ let elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 5);
+ for (let i = 0; i < elementItems.length; i++) {
+ assert.isFalse(elementItems[i].hasAttribute('needs-review'));
+ }
+
+ element.showReviewedState = true;
+ elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 5);
+ assert.isFalse(elementItems[0].hasAttribute('needs-review'));
+ assert.isTrue(elementItems[1].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[2].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[4].hasAttribute('needs-review'));
+
+ element.account = {_account_id: 42};
+ elementItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 5);
+ assert.isFalse(elementItems[0].hasAttribute('needs-review'));
+ assert.isTrue(elementItems[1].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[2].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[4].hasAttribute('needs-review'));
+ });
+
+ test('no changes', () => {
+ element.changes = [];
+ flushAsynchronousOperations();
+ const listItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(listItems.length, 0);
+ const noChangesMsg =
+ dom(element.root).querySelector('.noChanges');
+ assert.ok(noChangesMsg);
+ });
+
+ test('empty sections', () => {
+ element.sections = [{results: []}, {results: []}];
+ flushAsynchronousOperations();
+ const listItems = dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(listItems.length, 0);
+ const noChangesMsg = dom(element.root).querySelectorAll(
+ '.noChanges');
+ assert.equal(noChangesMsg.length, 2);
+ });
+
+ suite('empty outgoing', () => {
+ test('not shown on empty non-outgoing sections', () => {
+ const section = {results: []};
+ assert.isTrue(element._isEmpty(section));
+ assert.isFalse(element._isOutgoing(section));
+ });
+
+ test('shown on empty outgoing sections', () => {
+ const section = {results: [], isOutgoing: true};
+ assert.isTrue(element._isEmpty(section));
+ assert.isTrue(element._isOutgoing(section));
+ });
+
+ test('not shown on non-empty outgoing sections', () => {
+ const section = {isOutgoing: true, results: [
+ {_number: 0, labels: {Verified: {approved: {}}}}]};
+ assert.isFalse(element._isEmpty(section));
+ assert.isTrue(element._isOutgoing(section));
+ });
+ });
+
+ test('_isOutgoing', () => {
+ assert.isTrue(element._isOutgoing({results: [], isOutgoing: true}));
+ assert.isFalse(element._isOutgoing({results: []}));
+ });
+
+ suite('empty column preference', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ element.sections = [
+ {results: [{}]},
+ ];
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('show number enabled', () => {
+ assert.isTrue(element.showNumber);
+ });
+
+ test('all columns visible', () => {
+ for (const column of element.columnNames) {
+ const elementClass = '.' + element._lowerCase(column);
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ });
+ });
+
+ suite('full column preference', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ element.sections = [
+ {results: [{}]},
+ ];
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('all columns visible', () => {
+ for (const column of element.changeTableColumns) {
+ const elementClass = '.' + element._lowerCase(column);
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ });
+ });
+
+ suite('partial column preference', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ element.sections = [
+ {results: [{}]},
+ ];
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('all columns except repo visible', () => {
+ for (const column of element.changeTableColumns) {
+ const elementClass = '.' + column.toLowerCase();
+ if (column === 'Repo') {
+ assert.isTrue(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ } else {
+ assert.isFalse(element.shadowRoot
+ .querySelector(elementClass).hidden);
+ }
+ }
+ });
+ });
+
+ suite('random column does not exist', () => {
+ let element;
+
+ /* This would only exist if somebody manually updated the config
+ file. */
+ setup(() => {
+ element = fixture('basic');
+ element.account = {_account_id: 1001};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: 'HHMM_12',
+ change_table: [
+ 'Bad',
+ ],
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('bad column does not exist', () => {
+ const elementClass = '.bad';
+ assert.isNotOk(element.shadowRoot
+ .querySelector(elementClass));
+ });
+ });
+
+ suite('dashboard queries', () => {
let element;
let sandbox;
@@ -67,576 +472,180 @@
teardown(() => { sandbox.restore(); });
- suite('test show change number not logged in', () => {
- setup(() => {
- element = fixture('basic');
- element.account = null;
- element.preferences = null;
- });
-
- test('show number disabled', () => {
- assert.isFalse(element.showNumber);
- });
+ test('query without age and limit unchanged', () => {
+ const query = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), query);
});
- suite('test show change number preference enabled', () => {
- setup(() => {
- element = fixture('basic');
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [],
- };
- element.account = {_account_id: 1001};
- flushAsynchronousOperations();
- });
-
- test('show number enabled', () => {
- assert.isTrue(element.showNumber);
- });
+ test('query with age and limit', () => {
+ const query = 'status:closed age:1week limit:10 owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
- suite('test show change number preference disabled', () => {
- setup(() => {
- element = fixture('basic');
- // legacycid_in_change_table is not set when false.
- element.preferences = {
- time_format: 'HHMM_12',
- change_table: [],
- };
- element.account = {_account_id: 1001};
- flushAsynchronousOperations();
- });
-
- test('show number disabled', () => {
- assert.isFalse(element.showNumber);
- });
+ test('query with age', () => {
+ const query = 'status:closed age:1week owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
- test('computed fields', () => {
- assert.equal(element._computeLabelNames(
- [{results: [{_number: 0, labels: {}}]}]).length, 0);
- assert.equal(element._computeLabelNames([
- {results: [
- {_number: 0, labels: {Verified: {approved: {}}}},
- {
- _number: 1,
- labels: {
- 'Verified': {approved: {}},
- 'Code-Review': {approved: {}},
- },
- },
- {
- _number: 2,
- labels: {
- 'Verified': {approved: {}},
- 'Library-Compliance': {approved: {}},
- },
- },
- ]},
- ]).length, 3);
-
- assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
- assert.equal(element._computeLabelShortcut('Verified'), 'V');
- assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC');
- assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR');
- assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR');
- assert.equal(element._computeLabelShortcut(
- 'Invalid-Prolog-Rules-Label-Name--Verified'), 'V');
- assert.equal(element._computeLabelShortcut(
- 'Some-Special-Label-7'), 'SSL7');
- assert.equal(element._computeLabelShortcut('--Too----many----dashes---'),
- 'TMD');
- assert.equal(element._computeLabelShortcut(
- 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL');
+ test('query with limit', () => {
+ const query = 'status:closed limit:10 owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
- test('colspans', () => {
- element.sections = [
- {results: [{}]},
- ];
- flushAsynchronousOperations();
- const tdItemCount = Polymer.dom(element.root).querySelectorAll(
- 'td').length;
-
- const changeTableColumns = [];
- const labelNames = [];
- assert.equal(tdItemCount, element._computeColspan(
- changeTableColumns, labelNames));
+ test('query with age as value and not key', () => {
+ const query = 'status:closed random:age';
+ const expectedQuery = 'status:closed random:age';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
});
+ test('query with limit as value and not key', () => {
+ const query = 'status:closed random:limit';
+ const expectedQuery = 'status:closed random:limit';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
+ });
+
+ test('query with -age key', () => {
+ const query = 'status:closed -age:1week';
+ const expectedQuery = 'status:closed';
+ assert.deepEqual(element._processQuery(query), expectedQuery);
+ });
+ });
+
+ suite('gr-change-list sections', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
test('keyboard shortcuts', done => {
- sandbox.stub(element, '_computeLabelNames');
- element.sections = [
- {results: new Array(1)},
- {results: new Array(2)},
- ];
element.selectedIndex = 0;
- element.changes = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
+ element.sections = [
+ {
+ results: [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ],
+ },
+ {
+ results: [
+ {_number: 3},
+ {_number: 4},
+ {_number: 5},
+ ],
+ },
+ {
+ results: [
+ {_number: 6},
+ {_number: 7},
+ {_number: 8},
+ ],
+ },
];
flushAsynchronousOperations();
- Polymer.RenderStatus.afterNextRender(element, () => {
- const elementItems = Polymer.dom(element.root).querySelectorAll(
+ afterNextRender(element, () => {
+ const elementItems = dom(element.root).querySelectorAll(
'gr-change-list-item');
- assert.equal(elementItems.length, 3);
+ assert.equal(elementItems.length, 9);
- assert.isTrue(elementItems[0].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
assert.equal(element.selectedIndex, 1);
- assert.isTrue(elementItems[1].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.selectedIndex, 2);
- assert.isTrue(elementItems[2].hasAttribute('selected'));
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
assert.equal(element.selectedIndex, 2);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
'Should navigate to /c/2/');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
'Should navigate to /c/1/');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.selectedIndex, 0);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 4);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
+ 'Should navigate to /c/4/');
- const reloadStub = sandbox.stub(element, '_reloadWindow');
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
- assert.isTrue(reloadStub.called);
-
+ MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ const change = element._changeForIndex(element.selectedIndex);
+ assert.equal(change.reviewed, true,
+ 'Should mark change as reviewed');
+ MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
+ assert.equal(change.reviewed, false,
+ 'Should mark change as unreviewed');
done();
});
});
- test('changes needing review', () => {
+ test('highlight attribute is updated correctly', () => {
element.changes = [
{
_number: 0,
status: 'NEW',
- reviewed: true,
owner: {_account_id: 0},
},
{
_number: 1,
- status: 'NEW',
- owner: {_account_id: 0},
- },
- {
- _number: 2,
- status: 'MERGED',
- owner: {_account_id: 0},
- },
- {
- _number: 3,
status: 'ABANDONED',
owner: {_account_id: 0},
},
- {
- _number: 4,
- status: 'NEW',
- work_in_progress: true,
- owner: {_account_id: 0},
- },
];
- flushAsynchronousOperations();
- let elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 5);
- for (let i = 0; i < elementItems.length; i++) {
- assert.isFalse(elementItems[i].hasAttribute('needs-review'));
- }
-
- element.showReviewedState = true;
- elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 5);
- assert.isFalse(elementItems[0].hasAttribute('needs-review'));
- assert.isTrue(elementItems[1].hasAttribute('needs-review'));
- assert.isFalse(elementItems[2].hasAttribute('needs-review'));
- assert.isFalse(elementItems[3].hasAttribute('needs-review'));
- assert.isFalse(elementItems[4].hasAttribute('needs-review'));
-
element.account = {_account_id: 42};
- elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 5);
- assert.isFalse(elementItems[0].hasAttribute('needs-review'));
- assert.isTrue(elementItems[1].hasAttribute('needs-review'));
- assert.isFalse(elementItems[2].hasAttribute('needs-review'));
- assert.isFalse(elementItems[3].hasAttribute('needs-review'));
- assert.isFalse(elementItems[4].hasAttribute('needs-review'));
- });
-
- test('no changes', () => {
- element.changes = [];
flushAsynchronousOperations();
- const listItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(listItems.length, 0);
- const noChangesMsg =
- Polymer.dom(element.root).querySelector('.noChanges');
- assert.ok(noChangesMsg);
- });
+ let items = element._getListItems();
+ assert.equal(items.length, 2);
+ assert.isFalse(items[0].hasAttribute('highlight'));
+ assert.isFalse(items[1].hasAttribute('highlight'));
- test('empty sections', () => {
- element.sections = [{results: []}, {results: []}];
+ // Assign all issues to the user, but only the first one is highlighted
+ // because the second one is abandoned.
+ element.set(['changes', 0, 'assignee'], {_account_id: 12});
+ element.set(['changes', 1, 'assignee'], {_account_id: 12});
+ element.account = {_account_id: 12};
flushAsynchronousOperations();
- const listItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(listItems.length, 0);
- const noChangesMsg = Polymer.dom(element.root).querySelectorAll(
- '.noChanges');
- assert.equal(noChangesMsg.length, 2);
+ items = element._getListItems();
+ assert.isTrue(items[0].hasAttribute('highlight'));
+ assert.isFalse(items[1].hasAttribute('highlight'));
});
- suite('empty outgoing', () => {
- test('not shown on empty non-outgoing sections', () => {
- const section = {results: []};
- assert.isTrue(element._isEmpty(section));
- assert.isFalse(element._isOutgoing(section));
- });
-
- test('shown on empty outgoing sections', () => {
- const section = {results: [], isOutgoing: true};
- assert.isTrue(element._isEmpty(section));
- assert.isTrue(element._isOutgoing(section));
- });
-
- test('not shown on non-empty outgoing sections', () => {
- const section = {isOutgoing: true, results: [
- {_number: 0, labels: {Verified: {approved: {}}}}]};
- assert.isFalse(element._isEmpty(section));
- assert.isTrue(element._isOutgoing(section));
- });
+ test('_computeItemHighlight gives false for null account', () => {
+ assert.isFalse(
+ element._computeItemHighlight(null, {assignee: {_account_id: 42}}));
});
- test('_isOutgoing', () => {
- assert.isTrue(element._isOutgoing({results: [], isOutgoing: true}));
- assert.isFalse(element._isOutgoing({results: []}));
- });
+ test('_computeItemAbsoluteIndex', () => {
+ sandbox.stub(element, '_computeLabelNames');
+ element.sections = [
+ {results: new Array(1)},
+ {results: new Array(2)},
+ {results: new Array(3)},
+ ];
- suite('empty column preference', () => {
- let element;
+ assert.equal(element._computeItemAbsoluteIndex(0, 0), 0);
+ // Out of range but no matter.
+ assert.equal(element._computeItemAbsoluteIndex(0, 1), 1);
- setup(() => {
- element = fixture('basic');
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [],
- };
- flushAsynchronousOperations();
- });
-
- test('show number enabled', () => {
- assert.isTrue(element.showNumber);
- });
-
- test('all columns visible', () => {
- for (const column of element.columnNames) {
- const elementClass = '.' + element._lowerCase(column);
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- });
- });
-
- suite('full column preference', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ],
- };
- flushAsynchronousOperations();
- });
-
- test('all columns visible', () => {
- for (const column of element.changeTableColumns) {
- const elementClass = '.' + element._lowerCase(column);
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- });
- });
-
- suite('partial column preference', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Branch',
- 'Updated',
- 'Size',
- ],
- };
- flushAsynchronousOperations();
- });
-
- test('all columns except repo visible', () => {
- for (const column of element.changeTableColumns) {
- const elementClass = '.' + column.toLowerCase();
- if (column === 'Repo') {
- assert.isTrue(element.shadowRoot
- .querySelector(elementClass).hidden);
- } else {
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- }
- });
- });
-
- suite('random column does not exist', () => {
- let element;
-
- /* This would only exist if somebody manually updated the config
- file. */
- setup(() => {
- element = fixture('basic');
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Bad',
- ],
- };
- flushAsynchronousOperations();
- });
-
- test('bad column does not exist', () => {
- const elementClass = '.bad';
- assert.isNotOk(element.shadowRoot
- .querySelector(elementClass));
- });
- });
-
- suite('dashboard queries', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('query without age and limit unchanged', () => {
- const query = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), query);
- });
-
- test('query with age and limit', () => {
- const query = 'status:closed age:1week limit:10 owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with age', () => {
- const query = 'status:closed age:1week owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with limit', () => {
- const query = 'status:closed limit:10 owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with age as value and not key', () => {
- const query = 'status:closed random:age';
- const expectedQuery = 'status:closed random:age';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with limit as value and not key', () => {
- const query = 'status:closed random:limit';
- const expectedQuery = 'status:closed random:limit';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with -age key', () => {
- const query = 'status:closed -age:1week';
- const expectedQuery = 'status:closed';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
- });
-
- suite('gr-change-list sections', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('keyboard shortcuts', done => {
- element.selectedIndex = 0;
- element.sections = [
- {
- results: [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ],
- },
- {
- results: [
- {_number: 3},
- {_number: 4},
- {_number: 5},
- ],
- },
- {
- results: [
- {_number: 6},
- {_number: 7},
- {_number: 8},
- ],
- },
- ];
- flushAsynchronousOperations();
- Polymer.RenderStatus.afterNextRender(element, () => {
- const elementItems = Polymer.dom(element.root).querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 9);
-
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
-
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- assert.equal(element.selectedIndex, 2);
-
- MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
- assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
- 'Should navigate to /c/2/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
- assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
- 'Should navigate to /c/1/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
- assert.equal(element.selectedIndex, 4);
- MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
- assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
- 'Should navigate to /c/4/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
- const change = element._changeForIndex(element.selectedIndex);
- assert.equal(change.reviewed, true,
- 'Should mark change as reviewed');
- MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
- assert.equal(change.reviewed, false,
- 'Should mark change as unreviewed');
- done();
- });
- });
-
- test('highlight attribute is updated correctly', () => {
- element.changes = [
- {
- _number: 0,
- status: 'NEW',
- owner: {_account_id: 0},
- },
- {
- _number: 1,
- status: 'ABANDONED',
- owner: {_account_id: 0},
- },
- ];
- element.account = {_account_id: 42};
- flushAsynchronousOperations();
- let items = element._getListItems();
- assert.equal(items.length, 2);
- assert.isFalse(items[0].hasAttribute('highlight'));
- assert.isFalse(items[1].hasAttribute('highlight'));
-
- // Assign all issues to the user, but only the first one is highlighted
- // because the second one is abandoned.
- element.set(['changes', 0, 'assignee'], {_account_id: 12});
- element.set(['changes', 1, 'assignee'], {_account_id: 12});
- element.account = {_account_id: 12};
- flushAsynchronousOperations();
- items = element._getListItems();
- assert.isTrue(items[0].hasAttribute('highlight'));
- assert.isFalse(items[1].hasAttribute('highlight'));
- });
-
- test('_computeItemHighlight gives false for null account', () => {
- assert.isFalse(
- element._computeItemHighlight(null, {assignee: {_account_id: 42}}));
- });
-
- test('_computeItemAbsoluteIndex', () => {
- sandbox.stub(element, '_computeLabelNames');
- element.sections = [
- {results: new Array(1)},
- {results: new Array(2)},
- {results: new Array(3)},
- ];
-
- assert.equal(element._computeItemAbsoluteIndex(0, 0), 0);
- // Out of range but no matter.
- assert.equal(element._computeItemAbsoluteIndex(0, 1), 1);
-
- assert.equal(element._computeItemAbsoluteIndex(1, 0), 1);
- assert.equal(element._computeItemAbsoluteIndex(1, 1), 2);
- assert.equal(element._computeItemAbsoluteIndex(1, 2), 3);
- assert.equal(element._computeItemAbsoluteIndex(2, 0), 3);
- assert.equal(element._computeItemAbsoluteIndex(3, 0), 6);
- });
+ assert.equal(element._computeItemAbsoluteIndex(1, 0), 1);
+ assert.equal(element._computeItemAbsoluteIndex(1, 1), 2);
+ assert.equal(element._computeItemAbsoluteIndex(1, 2), 3);
+ assert.equal(element._computeItemAbsoluteIndex(2, 0), 3);
+ assert.equal(element._computeItemAbsoluteIndex(3, 0), 6);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
index 64d2486..3758a78 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -14,27 +14,35 @@
* 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 GrCreateChangeHelp extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-create-change-help'; }
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.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-create-change-help_html.js';
- /**
- * Fired when the "Create change" button is tapped.
- *
- * @event create-tap
- */
+/** @extends Polymer.Element */
+class GrCreateChangeHelp extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleCreateTap(e) {
- e.preventDefault();
- this.dispatchEvent(
- new CustomEvent('create-tap', {bubbles: true, composed: true}));
- }
+ static get is() { return 'gr-create-change-help'; }
+
+ /**
+ * Fired when the "Create change" button is tapped.
+ *
+ * @event create-tap
+ */
+
+ _handleCreateTap(e) {
+ e.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent('create-tap', {bubbles: true, composed: true}));
}
+}
- customElements.define(GrCreateChangeHelp.is, GrCreateChangeHelp);
-})();
+customElements.define(GrCreateChangeHelp.is, GrCreateChangeHelp);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
index 842c402..f40bda7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
@@ -1,28 +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">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-
-<dom-module id="gr-create-change-help">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -82,11 +76,9 @@
<h1>Push your first change for code review</h1>
<p>
Pushing a change for review is easy, but a little different from
- other git code review tools. Click on the `Create Change' button
+ other git code review tools. Click on the \`Create Change' button
and follow the step by step instructions.
</p>
<gr-button on-click="_handleCreateTap">Create Change</gr-button>
</div>
- </template>
- <script src="gr-create-change-help.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
index abc4a9b..3cbab30 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-change-help</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-create-change-help.html">
+<script type="module" src="./gr-create-change-help.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-create-change-help.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,19 +43,22 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-change-help tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-create-change-help.js';
+suite('gr-create-change-help tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('Create change tap', done => {
- element.addEventListener('create-tap', () => done());
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button'));
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('Create change tap', done => {
+ element.addEventListener('create-tap', () => done());
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
index 7abd784..7e5e749 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -14,53 +14,61 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const Commands = {
- CREATE: 'git commit',
- AMEND: 'git commit --amend',
- PUSH_PREFIX: 'git push origin HEAD:refs/for/',
- };
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-shell-command/gr-shell-command.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-create-commands-dialog_html.js';
- /** @extends Polymer.Element */
- class GrCreateCommandsDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-create-commands-dialog'; }
+const Commands = {
+ CREATE: 'git commit',
+ AMEND: 'git commit --amend',
+ PUSH_PREFIX: 'git push origin HEAD:refs/for/',
+};
- static get properties() {
- return {
- branch: String,
- _createNewCommitCommand: {
- type: String,
- readonly: true,
- value: Commands.CREATE,
- },
- _amendExistingCommitCommand: {
- type: String,
- readonly: true,
- value: Commands.AMEND,
- },
- _pushCommand: {
- type: String,
- computed: '_computePushCommand(branch)',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrCreateCommandsDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- open() {
- this.$.commandsOverlay.open();
- }
+ static get is() { return 'gr-create-commands-dialog'; }
- _handleClose() {
- this.$.commandsOverlay.close();
- }
-
- _computePushCommand(branch) {
- return Commands.PUSH_PREFIX + branch;
- }
+ static get properties() {
+ return {
+ branch: String,
+ _createNewCommitCommand: {
+ type: String,
+ readonly: true,
+ value: Commands.CREATE,
+ },
+ _amendExistingCommitCommand: {
+ type: String,
+ readonly: true,
+ value: Commands.AMEND,
+ },
+ _pushCommand: {
+ type: String,
+ computed: '_computePushCommand(branch)',
+ },
+ };
}
- customElements.define(GrCreateCommandsDialog.is, GrCreateCommandsDialog);
-})();
+ open() {
+ this.$.commandsOverlay.open();
+ }
+
+ _handleClose() {
+ this.$.commandsOverlay.close();
+ }
+
+ _computePushCommand(branch) {
+ return Commands.PUSH_PREFIX + branch;
+ }
+}
+
+customElements.define(GrCreateCommandsDialog.is, GrCreateCommandsDialog);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
index 9e86058..aa13dca 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
@@ -1,27 +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="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
-
-<dom-module id="gr-create-commands-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
ol {
list-style: decimal;
@@ -34,13 +29,8 @@
max-width: 40em;
}
</style>
- <gr-overlay id="commandsOverlay" with-backdrop>
- <gr-dialog
- id="commandsDialog"
- confirm-label="Done"
- cancel-label=""
- confirm-on-enter
- on-confirm="_handleClose">
+ <gr-overlay id="commandsOverlay" with-backdrop="">
+ <gr-dialog id="commandsDialog" confirm-label="Done" cancel-label="" confirm-on-enter="" on-confirm="_handleClose">
<div class="header" slot="header">
Create change commands
</div>
@@ -82,6 +72,4 @@
</div>
</gr-dialog>
</gr-overlay>
- </template>
- <script src="gr-create-commands-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
index 41709a6..f992b0b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-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-create-commands-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-create-commands-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-create-commands-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-create-commands-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,23 +40,25 @@
</template>
</test-fixture>
-<script>
- suite('gr-create-commands-dialog tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-create-commands-dialog.js';
+suite('gr-create-commands-dialog tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('_computePushCommand', () => {
- element.branch = 'master';
- assert.equal(element._pushCommand,
- 'git push origin HEAD:refs/for/master');
-
- element.branch = 'stable-2.15';
- assert.equal(element._pushCommand,
- 'git push origin HEAD:refs/for/stable-2.15');
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('_computePushCommand', () => {
+ element.branch = 'master';
+ assert.equal(element._pushCommand,
+ 'git push origin HEAD:refs/for/master');
+
+ element.branch = 'stable-2.15';
+ assert.equal(element._pushCommand,
+ 'git push origin HEAD:refs/for/stable-2.15');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index 35f7450..f8757ba 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -14,58 +14,66 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * Fired when a destination has been picked. Event details contain the repo
- * name and the branch name.
- *
- * @event confirm
- * @extends Polymer.Element
- */
- class GrCreateDestinationDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-create-destination-dialog'; }
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-repo-branch-picker/gr-repo-branch-picker.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-create-destination-dialog_html.js';
- static get properties() {
- return {
- _repo: String,
- _branch: String,
- _repoAndBranchSelected: {
- type: Boolean,
- value: false,
- computed: '_computeRepoAndBranchSelected(_repo, _branch)',
- },
- };
- }
+/**
+ * Fired when a destination has been picked. Event details contain the repo
+ * name and the branch name.
+ *
+ * @event confirm
+ * @extends Polymer.Element
+ */
+class GrCreateDestinationDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- open() {
- this._repo = '';
- this._branch = '';
- this.$.createOverlay.open();
- }
+ static get is() { return 'gr-create-destination-dialog'; }
- _handleClose() {
- this.$.createOverlay.close();
- }
-
- _pickerConfirm(e) {
- this.$.createOverlay.close();
- const detail = {repo: this._repo, branch: this._branch};
- // e is a 'confirm' event from gr-dialog. We want to fire a more detailed
- // 'confirm' event here, so let's stop propagation of the bare event.
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false}));
- }
-
- _computeRepoAndBranchSelected(repo, branch) {
- return !!(repo && branch);
- }
+ static get properties() {
+ return {
+ _repo: String,
+ _branch: String,
+ _repoAndBranchSelected: {
+ type: Boolean,
+ value: false,
+ computed: '_computeRepoAndBranchSelected(_repo, _branch)',
+ },
+ };
}
- customElements.define(GrCreateDestinationDialog.is,
- GrCreateDestinationDialog);
-})();
+ open() {
+ this._repo = '';
+ this._branch = '';
+ this.$.createOverlay.open();
+ }
+
+ _handleClose() {
+ this.$.createOverlay.close();
+ }
+
+ _pickerConfirm(e) {
+ this.$.createOverlay.close();
+ const detail = {repo: this._repo, branch: this._branch};
+ // e is a 'confirm' event from gr-dialog. We want to fire a more detailed
+ // 'confirm' event here, so let's stop propagation of the bare event.
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false}));
+ }
+
+ _computeRepoAndBranchSelected(repo, branch) {
+ return !!(repo && branch);
+ }
+}
+
+customElements.define(GrCreateDestinationDialog.is,
+ GrCreateDestinationDialog);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
index def5228..73f6ec0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
@@ -1,48 +1,35 @@
-<!--
-@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="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-repo-branch-picker/gr-repo-branch-picker.html">
-
-<dom-module id="gr-create-destination-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
</style>
- <gr-overlay id="createOverlay" with-backdrop>
- <gr-dialog
- confirm-label="View commands"
- on-confirm="_pickerConfirm"
- on-cancel="_handleClose"
- disabled="[[!_repoAndBranchSelected]]">
+ <gr-overlay id="createOverlay" with-backdrop="">
+ <gr-dialog confirm-label="View commands" on-confirm="_pickerConfirm" on-cancel="_handleClose" disabled="[[!_repoAndBranchSelected]]">
<div class="header" slot="header">
Create change
</div>
<div class="main" slot="main">
- <gr-repo-branch-picker
- repo="{{_repo}}"
- branch="{{_branch}}"></gr-repo-branch-picker>
+ <gr-repo-branch-picker repo="{{_repo}}" branch="{{_branch}}"></gr-repo-branch-picker>
<p>
If you haven't done so, you will need to clone the repository.
</p>
</div>
</gr-dialog>
</gr-overlay>
- </template>
- <script src="gr-create-destination-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index d0e1db2..a4ed814 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.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,306 +14,325 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-change-list/gr-change-list.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-create-commands-dialog/gr-create-commands-dialog.js';
+import '../gr-create-change-help/gr-create-change-help.js';
+import '../gr-create-destination-dialog/gr-create-destination-dialog.js';
+import '../gr-user-header/gr-user-header.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-dashboard-view_html.js';
+const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDashboardView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dashboard-view'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
*/
- class GrDashboardView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-dashboard-view'; }
- /**
- * Fired when the title of the page should change.
- *
- * @event title-change
- */
- static get properties() {
- return {
- account: {
- type: Object,
- value: null,
+ static get properties() {
+ return {
+ account: {
+ type: Object,
+ value: null,
+ },
+ preferences: Object,
+ /** @type {{ selectedChangeIndex: number }} */
+ viewState: Object,
+
+ /** @type {{ project: string, user: string }} */
+ params: {
+ type: Object,
+ },
+
+ createChangeTap: {
+ type: Function,
+ value() {
+ return this._createChangeTap.bind(this);
},
- preferences: Object,
- /** @type {{ selectedChangeIndex: number }} */
- viewState: Object,
+ },
- /** @type {{ project: string, user: string }} */
- params: {
- type: Object,
- },
+ _results: Array,
- createChangeTap: {
- type: Function,
- value() {
- return this._createChangeTap.bind(this);
- },
- },
+ /**
+ * For showing a "loading..." string during ajax requests.
+ */
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
- _results: Array,
+ _showDraftsBanner: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * For showing a "loading..." string during ajax requests.
- */
- _loading: {
- type: Boolean,
- value: true,
- },
-
- _showDraftsBanner: {
- type: Boolean,
- value: false,
- },
-
- _showNewUserHelp: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- static get observers() {
- return [
- '_paramsChanged(params.*)',
- ];
- }
-
- get options() {
- return this.listChangesOptionsToHex(
- this.ListChangesOption.LABELS,
- this.ListChangesOption.DETAILED_ACCOUNTS,
- this.ListChangesOption.REVIEWED
- );
- }
-
- /** @override */
- attached() {
- super.attached();
- this._loadPreferences();
- }
-
- _loadPreferences() {
- return this.$.restAPI.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- this.$.restAPI.getPreferences().then(preferences => {
- this.preferences = preferences;
- });
- } else {
- this.preferences = {};
- }
- });
- }
-
- _getProjectDashboard(project, dashboard) {
- const errFn = response => {
- this.fire('page-error', {response});
- };
- return this.$.restAPI.getDashboard(
- project, dashboard, errFn).then(response => {
- if (!response) {
- return;
- }
- return {
- title: response.title,
- sections: response.sections.map(section => {
- const suffix = response.foreach ? ' ' + response.foreach : '';
- return {
- name: section.name,
- query: (section.query + suffix).replace(
- PROJECT_PLACEHOLDER_PATTERN, project),
- };
- }),
- };
- });
- }
-
- _computeTitle(user) {
- if (!user || user === 'self') {
- return 'My Reviews';
- }
- return 'Dashboard for ' + user;
- }
-
- _isViewActive(params) {
- return params.view === Gerrit.Nav.View.DASHBOARD;
- }
-
- _paramsChanged(paramsChangeRecord) {
- const params = paramsChangeRecord.base;
-
- if (!this._isViewActive(params)) {
- return Promise.resolve();
- }
-
- return this._reload();
- }
-
- /**
- * Reloads the element.
- *
- * @return {Promise<!Object>}
- */
- _reload() {
- this._loading = true;
- const {project, dashboard, title, user, sections} = this.params;
- const dashboardPromise = project ?
- this._getProjectDashboard(project, dashboard) :
- Promise.resolve(Gerrit.Nav.getUserDashboard(
- user,
- sections,
- title || this._computeTitle(user)));
-
- const checkForNewUser = !project && user === 'self';
- return dashboardPromise
- .then(res => {
- if (res && res.title) {
- this.fire('title-change', {title: res.title});
- }
- return this._fetchDashboardChanges(res, checkForNewUser);
- })
- .then(() => {
- this._maybeShowDraftsBanner();
- this.$.reporting.dashboardDisplayed();
- })
- .catch(err => {
- this.fire('title-change', {
- title: title || this._computeTitle(user),
- });
- console.warn(err);
- })
- .then(() => { this._loading = false; });
- }
-
- /**
- * Fetches the changes for each dashboard section and sets this._results
- * with the response.
- *
- * @param {!Object} res
- * @param {boolean} checkForNewUser
- * @return {Promise}
- */
- _fetchDashboardChanges(res, checkForNewUser) {
- if (!res) { return Promise.resolve(); }
-
- const queries = res.sections
- .map(section => (section.suffixForDashboard ?
- section.query + ' ' + section.suffixForDashboard :
- section.query));
-
- if (checkForNewUser) {
- queries.push('owner:self limit:1');
- }
-
- return this.$.restAPI.getChanges(null, queries, null, this.options)
- .then(changes => {
- if (checkForNewUser) {
- // Last set of results is not meant for dashboard display.
- const lastResultSet = changes.pop();
- this._showNewUserHelp = lastResultSet.length == 0;
- }
- this._results = changes.map((results, i) => {
- return {
- name: res.sections[i].name,
- countLabel: this._computeSectionCountLabel(results),
- query: res.sections[i].query,
- results,
- isOutgoing: res.sections[i].isOutgoing,
- };
- }).filter((section, i) => i < res.sections.length && (
- !res.sections[i].hideIfEmpty ||
- section.results.length));
- });
- }
-
- _computeSectionCountLabel(changes) {
- if (!changes || !changes.length || changes.length == 0) {
- return '';
- }
- const more = changes[changes.length - 1]._more_changes;
- const numChanges = changes.length;
- const andMore = more ? ' and more' : '';
- return `(${numChanges}${andMore})`;
- }
-
- _computeUserHeaderClass(params) {
- if (!params || !!params.project || !params.user ||
- params.user === 'self') {
- return 'hide';
- }
- return '';
- }
-
- _handleToggleStar(e) {
- this.$.restAPI.saveChangeStarred(e.detail.change._number,
- e.detail.starred);
- }
-
- _handleToggleReviewed(e) {
- this.$.restAPI.saveChangeReviewed(e.detail.change._number,
- e.detail.reviewed);
- }
-
- /**
- * Banner is shown if a user is on their own dashboard and they have draft
- * comments on closed changes.
- */
- _maybeShowDraftsBanner() {
- this._showDraftsBanner = false;
- if (!(this.params.user === 'self')) { return; }
-
- const draftSection = this._results
- .find(section => section.query === 'has:draft');
- if (!draftSection || !draftSection.results.length) { return; }
-
- const closedChanges = draftSection.results
- .filter(change => !this.changeIsOpen(change));
- if (!closedChanges.length) { return; }
-
- this._showDraftsBanner = true;
- }
-
- _computeBannerClass(show) {
- return show ? '' : 'hide';
- }
-
- _handleOpenDeleteDialog() {
- this.$.confirmDeleteOverlay.open();
- }
-
- _handleConfirmDelete() {
- this.$.confirmDeleteDialog.disabled = true;
- return this.$.restAPI.deleteDraftComments('-is:open').then(() => {
- this._closeConfirmDeleteOverlay();
- this._reload();
- });
- }
-
- _closeConfirmDeleteOverlay() {
- this.$.confirmDeleteOverlay.close();
- }
-
- _computeDraftsLink() {
- return Gerrit.Nav.getUrlForSearchQuery('has:draft -is:open');
- }
-
- _createChangeTap(e) {
- this.$.destinationDialog.open();
- }
-
- _handleDestinationConfirm(e) {
- this.$.commandsDialog.branch = e.detail.branch;
- this.$.commandsDialog.open();
- }
+ _showNewUserHelp: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrDashboardView.is, GrDashboardView);
-})();
+ static get observers() {
+ return [
+ '_paramsChanged(params.*)',
+ ];
+ }
+
+ get options() {
+ return this.listChangesOptionsToHex(
+ this.ListChangesOption.LABELS,
+ this.ListChangesOption.DETAILED_ACCOUNTS,
+ this.ListChangesOption.REVIEWED
+ );
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadPreferences();
+ }
+
+ _loadPreferences() {
+ return this.$.restAPI.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ this.$.restAPI.getPreferences().then(preferences => {
+ this.preferences = preferences;
+ });
+ } else {
+ this.preferences = {};
+ }
+ });
+ }
+
+ _getProjectDashboard(project, dashboard) {
+ const errFn = response => {
+ this.fire('page-error', {response});
+ };
+ return this.$.restAPI.getDashboard(
+ project, dashboard, errFn).then(response => {
+ if (!response) {
+ return;
+ }
+ return {
+ title: response.title,
+ sections: response.sections.map(section => {
+ const suffix = response.foreach ? ' ' + response.foreach : '';
+ return {
+ name: section.name,
+ query: (section.query + suffix).replace(
+ PROJECT_PLACEHOLDER_PATTERN, project),
+ };
+ }),
+ };
+ });
+ }
+
+ _computeTitle(user) {
+ if (!user || user === 'self') {
+ return 'My Reviews';
+ }
+ return 'Dashboard for ' + user;
+ }
+
+ _isViewActive(params) {
+ return params.view === Gerrit.Nav.View.DASHBOARD;
+ }
+
+ _paramsChanged(paramsChangeRecord) {
+ const params = paramsChangeRecord.base;
+
+ if (!this._isViewActive(params)) {
+ return Promise.resolve();
+ }
+
+ return this._reload();
+ }
+
+ /**
+ * Reloads the element.
+ *
+ * @return {Promise<!Object>}
+ */
+ _reload() {
+ this._loading = true;
+ const {project, dashboard, title, user, sections} = this.params;
+ const dashboardPromise = project ?
+ this._getProjectDashboard(project, dashboard) :
+ Promise.resolve(Gerrit.Nav.getUserDashboard(
+ user,
+ sections,
+ title || this._computeTitle(user)));
+
+ const checkForNewUser = !project && user === 'self';
+ return dashboardPromise
+ .then(res => {
+ if (res && res.title) {
+ this.fire('title-change', {title: res.title});
+ }
+ return this._fetchDashboardChanges(res, checkForNewUser);
+ })
+ .then(() => {
+ this._maybeShowDraftsBanner();
+ this.$.reporting.dashboardDisplayed();
+ })
+ .catch(err => {
+ this.fire('title-change', {
+ title: title || this._computeTitle(user),
+ });
+ console.warn(err);
+ })
+ .then(() => { this._loading = false; });
+ }
+
+ /**
+ * Fetches the changes for each dashboard section and sets this._results
+ * with the response.
+ *
+ * @param {!Object} res
+ * @param {boolean} checkForNewUser
+ * @return {Promise}
+ */
+ _fetchDashboardChanges(res, checkForNewUser) {
+ if (!res) { return Promise.resolve(); }
+
+ const queries = res.sections
+ .map(section => (section.suffixForDashboard ?
+ section.query + ' ' + section.suffixForDashboard :
+ section.query));
+
+ if (checkForNewUser) {
+ queries.push('owner:self limit:1');
+ }
+
+ return this.$.restAPI.getChanges(null, queries, null, this.options)
+ .then(changes => {
+ if (checkForNewUser) {
+ // Last set of results is not meant for dashboard display.
+ const lastResultSet = changes.pop();
+ this._showNewUserHelp = lastResultSet.length == 0;
+ }
+ this._results = changes.map((results, i) => {
+ return {
+ name: res.sections[i].name,
+ countLabel: this._computeSectionCountLabel(results),
+ query: res.sections[i].query,
+ results,
+ isOutgoing: res.sections[i].isOutgoing,
+ };
+ }).filter((section, i) => i < res.sections.length && (
+ !res.sections[i].hideIfEmpty ||
+ section.results.length));
+ });
+ }
+
+ _computeSectionCountLabel(changes) {
+ if (!changes || !changes.length || changes.length == 0) {
+ return '';
+ }
+ const more = changes[changes.length - 1]._more_changes;
+ const numChanges = changes.length;
+ const andMore = more ? ' and more' : '';
+ return `(${numChanges}${andMore})`;
+ }
+
+ _computeUserHeaderClass(params) {
+ if (!params || !!params.project || !params.user ||
+ params.user === 'self') {
+ return 'hide';
+ }
+ return '';
+ }
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ }
+
+ _handleToggleReviewed(e) {
+ this.$.restAPI.saveChangeReviewed(e.detail.change._number,
+ e.detail.reviewed);
+ }
+
+ /**
+ * Banner is shown if a user is on their own dashboard and they have draft
+ * comments on closed changes.
+ */
+ _maybeShowDraftsBanner() {
+ this._showDraftsBanner = false;
+ if (!(this.params.user === 'self')) { return; }
+
+ const draftSection = this._results
+ .find(section => section.query === 'has:draft');
+ if (!draftSection || !draftSection.results.length) { return; }
+
+ const closedChanges = draftSection.results
+ .filter(change => !this.changeIsOpen(change));
+ if (!closedChanges.length) { return; }
+
+ this._showDraftsBanner = true;
+ }
+
+ _computeBannerClass(show) {
+ return show ? '' : 'hide';
+ }
+
+ _handleOpenDeleteDialog() {
+ this.$.confirmDeleteOverlay.open();
+ }
+
+ _handleConfirmDelete() {
+ this.$.confirmDeleteDialog.disabled = true;
+ return this.$.restAPI.deleteDraftComments('-is:open').then(() => {
+ this._closeConfirmDeleteOverlay();
+ this._reload();
+ });
+ }
+
+ _closeConfirmDeleteOverlay() {
+ this.$.confirmDeleteOverlay.close();
+ }
+
+ _computeDraftsLink() {
+ return Gerrit.Nav.getUrlForSearchQuery('has:draft -is:open');
+ }
+
+ _createChangeTap(e) {
+ this.$.destinationDialog.open();
+ }
+
+ _handleDestinationConfirm(e) {
+ this.$.commandsDialog.branch = e.detail.branch;
+ this.$.commandsDialog.open();
+ }
+}
+
+customElements.define(GrDashboardView.is, GrDashboardView);
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
index f119e98..07d638c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
@@ -1,37 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-commands-dialog/gr-create-commands-dialog.html">
-<link rel="import" href="../gr-create-change-help/gr-create-change-help.html">
-<link rel="import" href="../gr-create-destination-dialog/gr-create-destination-dialog.html">
-<link rel="import" href="../gr-user-header/gr-user-header.html">
-
-<dom-module id="gr-dashboard-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -71,32 +56,19 @@
}
}
</style>
- <div class$="banner [[_computeBannerClass(_showDraftsBanner)]]">
+ <div class\$="banner [[_computeBannerClass(_showDraftsBanner)]]">
<div>
You have draft comments on closed changes.
- <a href$="[[_computeDraftsLink(_showDraftsBanner)]]" target="_blank">(view all)</a>
+ <a href\$="[[_computeDraftsLink(_showDraftsBanner)]]" target="_blank">(view all)</a>
</div>
<div>
- <gr-button
- class="delete"
- link
- on-click="_handleOpenDeleteDialog">Delete All</gr-button>
+ <gr-button class="delete" link="" on-click="_handleOpenDeleteDialog">Delete All</gr-button>
</div>
</div>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <div hidden$="[[_loading]]" hidden>
- <gr-user-header
- user-id="[[params.user]]"
- class$="[[_computeUserHeaderClass(params)]]"></gr-user-header>
- <gr-change-list
- show-star
- show-reviewed-state
- account="[[account]]"
- preferences="[[preferences]]"
- selected-index="{{viewState.selectedChangeIndex}}"
- sections="[[_results]]"
- on-toggle-star="_handleToggleStar"
- on-toggle-reviewed="_handleToggleReviewed">
+ <div class="loading" hidden\$="[[!_loading]]">Loading...</div>
+ <div hidden\$="[[_loading]]" hidden="">
+ <gr-user-header user-id="[[params.user]]" class\$="[[_computeUserHeaderClass(params)]]"></gr-user-header>
+ <gr-change-list show-star="" show-reviewed-state="" account="[[account]]" preferences="[[preferences]]" selected-index="{{viewState.selectedChangeIndex}}" sections="[[_results]]" on-toggle-star="_handleToggleStar" on-toggle-reviewed="_handleToggleReviewed">
<div id="emptyOutgoing" slot="empty-outgoing">
<template is="dom-if" if="[[_showNewUserHelp]]">
<gr-create-change-help on-create-tap="createChangeTap"></gr-create-change-help>
@@ -107,12 +79,8 @@
</div>
</gr-change-list>
</div>
- <gr-overlay id="confirmDeleteOverlay" with-backdrop>
- <gr-dialog
- id="confirmDeleteDialog"
- confirm-label="Delete"
- on-confirm="_handleConfirmDelete"
- on-cancel="_closeConfirmDeleteOverlay">
+ <gr-overlay id="confirmDeleteOverlay" with-backdrop="">
+ <gr-dialog id="confirmDeleteDialog" confirm-label="Delete" on-confirm="_handleConfirmDelete" on-cancel="_closeConfirmDeleteOverlay">
<div class="header" slot="header">
Delete comments
</div>
@@ -122,12 +90,8 @@
</div>
</gr-dialog>
</gr-overlay>
- <gr-create-destination-dialog
- id="destinationDialog"
- on-confirm="_handleDestinationConfirm"></gr-create-destination-dialog>
+ <gr-create-destination-dialog id="destinationDialog" on-confirm="_handleDestinationConfirm"></gr-create-destination-dialog>
<gr-create-commands-dialog id="commandsDialog"></gr-create-commands-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-dashboard-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 3d83e99..c879923 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_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-dashboard-view</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-dashboard-view.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-dashboard-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-dashboard-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,351 +40,353 @@
</template>
</test-fixture>
-<script>
- suite('gr-dashboard-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let paramsChangedPromise;
- let getChangesStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-dashboard-view.js';
+suite('gr-dashboard-view tests', () => {
+ let element;
+ let sandbox;
+ let paramsChangedPromise;
+ let getChangesStub;
- setup(() => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getAccountDetails() { return Promise.resolve({}); },
- getAccountStatus() { return Promise.resolve(false); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges',
- (_, qs) => Promise.resolve(qs.map(() => [])));
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getAccountDetails() { return Promise.resolve({}); },
+ getAccountStatus() { return Promise.resolve(false); },
+ });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges',
+ (_, qs) => Promise.resolve(qs.map(() => [])));
- let resolver;
- paramsChangedPromise = new Promise(resolve => {
- resolver = resolve;
+ let resolver;
+ paramsChangedPromise = new Promise(resolve => {
+ resolver = resolve;
+ });
+ const paramsChanged = element._paramsChanged.bind(element);
+ sandbox.stub(element, '_paramsChanged', params => {
+ paramsChanged(params).then(() => resolver());
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('drafts banner functionality', () => {
+ suite('_maybeShowDraftsBanner', () => {
+ test('not dashboard/self', () => {
+ element.params = {user: 'notself'};
+ element._maybeShowDraftsBanner();
+ assert.isFalse(element._showDraftsBanner);
});
- const paramsChanged = element._paramsChanged.bind(element);
- sandbox.stub(element, '_paramsChanged', params => {
- paramsChanged(params).then(() => resolver());
+
+ test('no drafts at all', () => {
+ element.params = {user: 'self'};
+ element._results = [];
+ element._maybeShowDraftsBanner();
+ assert.isFalse(element._showDraftsBanner);
+ });
+
+ test('no drafts on open changes', () => {
+ element.params = {user: 'self'};
+ element._results = [{query: 'has:draft', results: [{status: '_'}]}];
+ sandbox.stub(element, 'changeIsOpen').returns(true);
+ element._maybeShowDraftsBanner();
+ assert.isFalse(element._showDraftsBanner);
+ });
+
+ test('no drafts on open changes', () => {
+ element.params = {user: 'self'};
+ element._results = [{query: 'has:draft', results: [{status: '_'}]}];
+ sandbox.stub(element, 'changeIsOpen').returns(false);
+ element._maybeShowDraftsBanner();
+ assert.isTrue(element._showDraftsBanner);
});
});
- teardown(() => {
- sandbox.restore();
+ test('_showDraftsBanner', () => {
+ element._showDraftsBanner = false;
+ flushAsynchronousOperations();
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.banner')));
+
+ element._showDraftsBanner = true;
+ flushAsynchronousOperations();
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.banner')));
});
- suite('drafts banner functionality', () => {
- suite('_maybeShowDraftsBanner', () => {
- test('not dashboard/self', () => {
- element.params = {user: 'notself'};
- element._maybeShowDraftsBanner();
- assert.isFalse(element._showDraftsBanner);
- });
+ test('delete tap opens dialog', () => {
+ sandbox.stub(element, '_handleOpenDeleteDialog');
+ element._showDraftsBanner = true;
+ flushAsynchronousOperations();
- test('no drafts at all', () => {
- element.params = {user: 'self'};
- element._results = [];
- element._maybeShowDraftsBanner();
- assert.isFalse(element._showDraftsBanner);
- });
-
- test('no drafts on open changes', () => {
- element.params = {user: 'self'};
- element._results = [{query: 'has:draft', results: [{status: '_'}]}];
- sandbox.stub(element, 'changeIsOpen').returns(true);
- element._maybeShowDraftsBanner();
- assert.isFalse(element._showDraftsBanner);
- });
-
- test('no drafts on open changes', () => {
- element.params = {user: 'self'};
- element._results = [{query: 'has:draft', results: [{status: '_'}]}];
- sandbox.stub(element, 'changeIsOpen').returns(false);
- element._maybeShowDraftsBanner();
- assert.isTrue(element._showDraftsBanner);
- });
- });
-
- test('_showDraftsBanner', () => {
- element._showDraftsBanner = false;
- flushAsynchronousOperations();
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('.banner')));
-
- element._showDraftsBanner = true;
- flushAsynchronousOperations();
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.banner')));
- });
-
- test('delete tap opens dialog', () => {
- sandbox.stub(element, '_handleOpenDeleteDialog');
- element._showDraftsBanner = true;
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.banner .delete'));
- assert.isTrue(element._handleOpenDeleteDialog.called);
- });
-
- test('delete comments flow', async () => {
- sandbox.spy(element, '_handleConfirmDelete');
- sandbox.stub(element, '_reload');
-
- // Set up control over timing of when RPC resolves.
- let deleteDraftCommentsPromiseResolver;
- const deleteDraftCommentsPromise = new Promise(resolve => {
- deleteDraftCommentsPromiseResolver = resolve;
- });
- sandbox.stub(element.$.restAPI, 'deleteDraftComments')
- .returns(deleteDraftCommentsPromise);
-
- // Open confirmation dialog and tap confirm button.
- await element.$.confirmDeleteOverlay.open();
- MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm);
- flushAsynchronousOperations();
- assert.isTrue(element.$.restAPI.deleteDraftComments
- .calledWithExactly('-is:open'));
- assert.isTrue(element.$.confirmDeleteDialog.disabled);
- assert.equal(element._reload.callCount, 0);
-
- // Verify state after RPC resolves.
- deleteDraftCommentsPromiseResolver([]);
- await deleteDraftCommentsPromise;
- assert.equal(element._reload.callCount, 1);
- });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.banner .delete'));
+ assert.isTrue(element._handleOpenDeleteDialog.called);
});
- test('_computeTitle', () => {
- assert.equal(element._computeTitle('self'), 'My Reviews');
- assert.equal(element._computeTitle('not self'), 'Dashboard for not self');
+ test('delete comments flow', async () => {
+ sandbox.spy(element, '_handleConfirmDelete');
+ sandbox.stub(element, '_reload');
+
+ // Set up control over timing of when RPC resolves.
+ let deleteDraftCommentsPromiseResolver;
+ const deleteDraftCommentsPromise = new Promise(resolve => {
+ deleteDraftCommentsPromiseResolver = resolve;
+ });
+ sandbox.stub(element.$.restAPI, 'deleteDraftComments')
+ .returns(deleteDraftCommentsPromise);
+
+ // Open confirmation dialog and tap confirm button.
+ await element.$.confirmDeleteOverlay.open();
+ MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm);
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.restAPI.deleteDraftComments
+ .calledWithExactly('-is:open'));
+ assert.isTrue(element.$.confirmDeleteDialog.disabled);
+ assert.equal(element._reload.callCount, 0);
+
+ // Verify state after RPC resolves.
+ deleteDraftCommentsPromiseResolver([]);
+ await deleteDraftCommentsPromise;
+ assert.equal(element._reload.callCount, 1);
+ });
+ });
+
+ test('_computeTitle', () => {
+ assert.equal(element._computeTitle('self'), 'My Reviews');
+ assert.equal(element._computeTitle('not self'), 'Dashboard for not self');
+ });
+
+ suite('_computeSectionCountLabel', () => {
+ test('empty changes dont count label', () => {
+ assert.equal('', element._computeSectionCountLabel([]));
});
- suite('_computeSectionCountLabel', () => {
- test('empty changes dont count label', () => {
- assert.equal('', element._computeSectionCountLabel([]));
- });
-
- test('1 change', () => {
- assert.equal('(1)',
- element._computeSectionCountLabel(['1']));
- });
-
- test('2 changes', () => {
- assert.equal('(2)',
- element._computeSectionCountLabel(['1', '2']));
- });
-
- test('1 change and more', () => {
- assert.equal('(1 and more)',
- element._computeSectionCountLabel([{_more_changes: true}]));
- });
+ test('1 change', () => {
+ assert.equal('(1)',
+ element._computeSectionCountLabel(['1']));
});
- suite('_isViewActive', () => {
- test('nothing happens when user param is falsy', () => {
- element.params = {};
- flushAsynchronousOperations();
- assert.equal(getChangesStub.callCount, 0);
-
- element.params = {user: ''};
- flushAsynchronousOperations();
- assert.equal(getChangesStub.callCount, 0);
- });
-
- test('content is refreshed when user param is updated', () => {
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- user: 'self',
- };
- return paramsChangedPromise.then(() => {
- assert.equal(getChangesStub.callCount, 1);
- });
- });
+ test('2 changes', () => {
+ assert.equal('(2)',
+ element._computeSectionCountLabel(['1', '2']));
});
- suite('selfOnly sections', () => {
- test('viewing self dashboard includes selfOnly sections', () => {
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- sections: [
- {query: '1'},
- {query: '2', selfOnly: true},
- ],
- user: 'self',
- };
- return paramsChangedPromise.then(() => {
- assert.isTrue(
- getChangesStub.calledWith(
- null, ['1', '2', 'owner:self limit:1'], null, element.options));
- });
- });
+ test('1 change and more', () => {
+ assert.equal('(1 and more)',
+ element._computeSectionCountLabel([{_more_changes: true}]));
+ });
+ });
- test('viewing another user\'s dashboard omits selfOnly sections', () => {
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- sections: [
- {query: '1'},
- {query: '2', selfOnly: true},
- ],
- user: 'user',
- };
- return paramsChangedPromise.then(() => {
- assert.isTrue(
- getChangesStub.calledWith(
- null, ['1'], null, element.options));
- });
- });
+ suite('_isViewActive', () => {
+ test('nothing happens when user param is falsy', () => {
+ element.params = {};
+ flushAsynchronousOperations();
+ assert.equal(getChangesStub.callCount, 0);
+
+ element.params = {user: ''};
+ flushAsynchronousOperations();
+ assert.equal(getChangesStub.callCount, 0);
});
- test('suffixForDashboard is included in getChanges query', () => {
+ test('content is refreshed when user param is updated', () => {
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ user: 'self',
+ };
+ return paramsChangedPromise.then(() => {
+ assert.equal(getChangesStub.callCount, 1);
+ });
+ });
+ });
+
+ suite('selfOnly sections', () => {
+ test('viewing self dashboard includes selfOnly sections', () => {
element.params = {
view: Gerrit.Nav.View.DASHBOARD,
sections: [
{query: '1'},
- {query: '2', suffixForDashboard: 'suffix'},
+ {query: '2', selfOnly: true},
],
+ user: 'self',
};
return paramsChangedPromise.then(() => {
- assert.isTrue(getChangesStub.calledOnce);
- assert.deepEqual(
- getChangesStub.firstCall.args,
- [null, ['1', '2 suffix'], null, element.options]);
+ assert.isTrue(
+ getChangesStub.calledWith(
+ null, ['1', '2', 'owner:self limit:1'], null, element.options));
});
});
- suite('_getProjectDashboard', () => {
- test('dashboard with foreach', () => {
- sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
- title: 'title',
- foreach: 'foreach for ${project}',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: '${project} query 2'},
- ],
- }));
- return element._getProjectDashboard('project', '').then(dashboard => {
- assert.deepEqual(
- dashboard,
- {
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1 foreach for project'},
- {
- name: 'section 2',
- query: 'project query 2 foreach for project',
- },
- ],
- });
- });
- });
-
- test('dashboard without foreach', () => {
- sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: '${project} query 2'},
- ],
- }));
- return element._getProjectDashboard('project', '').then(dashboard => {
- assert.deepEqual(
- dashboard,
- {
- title: 'title',
- sections: [
- {name: 'section 1', query: 'query 1'},
- {name: 'section 2', query: 'project query 2'},
- ],
- });
- });
- });
- });
-
- test('hideIfEmpty sections', () => {
- const sections = [
- {name: 'test1', query: 'test1', hideIfEmpty: true},
- {name: 'test2', query: 'test2', hideIfEmpty: true},
- ];
- getChangesStub.restore();
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([[], ['nonempty']]));
-
- return element._fetchDashboardChanges({sections}, false).then(() => {
- assert.equal(element._results.length, 1);
- assert.equal(element._results[0].name, 'test2');
- });
- });
-
- test('preserve isOutgoing sections', () => {
- const sections = [
- {name: 'test1', query: 'test1', isOutgoing: true},
- {name: 'test2', query: 'test2'},
- ];
- getChangesStub.restore();
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([[], []]));
-
- return element._fetchDashboardChanges({sections}, false).then(() => {
- assert.equal(element._results.length, 2);
- assert.isTrue(element._results[0].isOutgoing);
- assert.isNotOk(element._results[1].isOutgoing);
- });
- });
-
- test('_showNewUserHelp', () => {
- element._loading = false;
- element._showNewUserHelp = false;
- flushAsynchronousOperations();
-
- assert.equal(element.$.emptyOutgoing.textContent.trim(), 'No changes');
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-create-change-help'));
- element._showNewUserHelp = true;
- flushAsynchronousOperations();
-
- assert.notEqual(element.$.emptyOutgoing.textContent.trim(), 'No changes');
- assert.isOk(element.shadowRoot
- .querySelector('gr-create-change-help'));
- });
-
- test('_computeUserHeaderClass', () => {
- assert.equal(element._computeUserHeaderClass(undefined), 'hide');
- assert.equal(element._computeUserHeaderClass({}), 'hide');
- assert.equal(element._computeUserHeaderClass({user: 'self'}), 'hide');
- assert.equal(element._computeUserHeaderClass({user: 'user'}), '');
- assert.equal(
- element._computeUserHeaderClass({project: 'p', user: 'user'}),
- 'hide');
- });
-
- test('404 page', done => {
- const response = {status: 404};
- sandbox.stub(element.$.restAPI, 'getDashboard',
- async (project, dashboard, errFn) => {
- errFn(response);
- });
- element.addEventListener('page-error', e => {
- assert.strictEqual(e.detail.response, response);
- done();
- });
+ test('viewing another user\'s dashboard omits selfOnly sections', () => {
element.params = {
view: Gerrit.Nav.View.DASHBOARD,
- project: 'project',
- dashboard: 'dashboard',
- };
- });
-
- test('params change triggers dashboardDisplayed()', () => {
- sandbox.stub(element.$.reporting, 'dashboardDisplayed');
- element.params = {
- view: Gerrit.Nav.View.DASHBOARD,
- project: 'project',
- dashboard: 'dashboard',
+ sections: [
+ {query: '1'},
+ {query: '2', selfOnly: true},
+ ],
+ user: 'user',
};
return paramsChangedPromise.then(() => {
- assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce);
+ assert.isTrue(
+ getChangesStub.calledWith(
+ null, ['1'], null, element.options));
});
});
});
+
+ test('suffixForDashboard is included in getChanges query', () => {
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ sections: [
+ {query: '1'},
+ {query: '2', suffixForDashboard: 'suffix'},
+ ],
+ };
+ return paramsChangedPromise.then(() => {
+ assert.isTrue(getChangesStub.calledOnce);
+ assert.deepEqual(
+ getChangesStub.firstCall.args,
+ [null, ['1', '2 suffix'], null, element.options]);
+ });
+ });
+
+ suite('_getProjectDashboard', () => {
+ test('dashboard with foreach', () => {
+ sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
+ title: 'title',
+ foreach: 'foreach for ${project}',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: '${project} query 2'},
+ ],
+ }));
+ return element._getProjectDashboard('project', '').then(dashboard => {
+ assert.deepEqual(
+ dashboard,
+ {
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1 foreach for project'},
+ {
+ name: 'section 2',
+ query: 'project query 2 foreach for project',
+ },
+ ],
+ });
+ });
+ });
+
+ test('dashboard without foreach', () => {
+ sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: '${project} query 2'},
+ ],
+ }));
+ return element._getProjectDashboard('project', '').then(dashboard => {
+ assert.deepEqual(
+ dashboard,
+ {
+ title: 'title',
+ sections: [
+ {name: 'section 1', query: 'query 1'},
+ {name: 'section 2', query: 'project query 2'},
+ ],
+ });
+ });
+ });
+ });
+
+ test('hideIfEmpty sections', () => {
+ const sections = [
+ {name: 'test1', query: 'test1', hideIfEmpty: true},
+ {name: 'test2', query: 'test2', hideIfEmpty: true},
+ ];
+ getChangesStub.restore();
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([[], ['nonempty']]));
+
+ return element._fetchDashboardChanges({sections}, false).then(() => {
+ assert.equal(element._results.length, 1);
+ assert.equal(element._results[0].name, 'test2');
+ });
+ });
+
+ test('preserve isOutgoing sections', () => {
+ const sections = [
+ {name: 'test1', query: 'test1', isOutgoing: true},
+ {name: 'test2', query: 'test2'},
+ ];
+ getChangesStub.restore();
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([[], []]));
+
+ return element._fetchDashboardChanges({sections}, false).then(() => {
+ assert.equal(element._results.length, 2);
+ assert.isTrue(element._results[0].isOutgoing);
+ assert.isNotOk(element._results[1].isOutgoing);
+ });
+ });
+
+ test('_showNewUserHelp', () => {
+ element._loading = false;
+ element._showNewUserHelp = false;
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.emptyOutgoing.textContent.trim(), 'No changes');
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-create-change-help'));
+ element._showNewUserHelp = true;
+ flushAsynchronousOperations();
+
+ assert.notEqual(element.$.emptyOutgoing.textContent.trim(), 'No changes');
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-create-change-help'));
+ });
+
+ test('_computeUserHeaderClass', () => {
+ assert.equal(element._computeUserHeaderClass(undefined), 'hide');
+ assert.equal(element._computeUserHeaderClass({}), 'hide');
+ assert.equal(element._computeUserHeaderClass({user: 'self'}), 'hide');
+ assert.equal(element._computeUserHeaderClass({user: 'user'}), '');
+ assert.equal(
+ element._computeUserHeaderClass({project: 'p', user: 'user'}),
+ 'hide');
+ });
+
+ test('404 page', done => {
+ const response = {status: 404};
+ sandbox.stub(element.$.restAPI, 'getDashboard',
+ async (project, dashboard, errFn) => {
+ errFn(response);
+ });
+ element.addEventListener('page-error', e => {
+ assert.strictEqual(e.detail.response, response);
+ done();
+ });
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ project: 'project',
+ dashboard: 'dashboard',
+ };
+ });
+
+ test('params change triggers dashboardDisplayed()', () => {
+ sandbox.stub(element.$.reporting, 'dashboardDisplayed');
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ project: 'project',
+ dashboard: 'dashboard',
+ };
+ return paramsChangedPromise.then(() => {
+ assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
index de0a56e..2523700 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -14,24 +14,31 @@
* 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 GrEmbedDashboard extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-embed-dashboard'; }
+import '../gr-change-list/gr-change-list.js';
+import '../gr-create-change-help/gr-create-change-help.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-embed-dashboard_html.js';
- static get properties() {
- return {
- account: Object,
- sections: Array,
- preferences: Object,
- showNewUserHelp: Boolean,
- };
- }
+/** @extends Polymer.Element */
+class GrEmbedDashboard extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-embed-dashboard'; }
+
+ static get properties() {
+ return {
+ account: Object,
+ sections: Array,
+ preferences: Object,
+ showNewUserHelp: Boolean,
+ };
}
+}
- customElements.define(GrEmbedDashboard.is, GrEmbedDashboard);
-})();
+customElements.define(GrEmbedDashboard.is, GrEmbedDashboard);
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
index d445185..2c122f3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
@@ -1,32 +1,23 @@
-<!--
-@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="../../change-list/gr-change-list/gr-change-list.html">
-<link rel="import" href="../gr-create-change-help/gr-create-change-help.html">
-
-<dom-module id="gr-embed-dashboard">
- <template>
- <gr-change-list
- show-star
- account="[[account]]"
- preferences="[[preferences]]"
- sections="[[sections]]">
+export const htmlTemplate = html`
+ <gr-change-list show-star="" account="[[account]]" preferences="[[preferences]]" sections="[[sections]]">
<div id="emptyOutgoing" slot="empty-outgoing">
<template is="dom-if" if="[[showNewUserHelp]]">
<gr-create-change-help></gr-create-change-help>
@@ -36,6 +27,4 @@
</template>
</div>
</gr-change-list>
- </template>
- <script src="gr-embed-dashboard.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
index c0e472a..e9a0387 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -14,35 +14,45 @@
* 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 GrRepoHeader extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-repo-header'; }
+import '../../../styles/dashboard-header-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.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-repo-header_html.js';
- static get properties() {
- return {
- /** @type {?string} */
- repo: {
- type: String,
- observer: '_repoChanged',
- },
- /** @type {string|null} */
- _repoUrl: String,
- };
- }
+/** @extends Polymer.Element */
+class GrRepoHeader extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _repoChanged(repoName) {
- if (!repoName) {
- this._repoUrl = null;
- return;
- }
- this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName);
- }
+ static get is() { return 'gr-repo-header'; }
+
+ static get properties() {
+ return {
+ /** @type {?string} */
+ repo: {
+ type: String,
+ observer: '_repoChanged',
+ },
+ /** @type {string|null} */
+ _repoUrl: String,
+ };
}
- customElements.define(GrRepoHeader.is, GrRepoHeader);
-})();
+ _repoChanged(repoName) {
+ if (!repoName) {
+ this._repoUrl = null;
+ return;
+ }
+ this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName);
+ }
+}
+
+customElements.define(GrRepoHeader.is, GrRepoHeader);
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
index 5d4b8a3..d1274ee 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
@@ -1,29 +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/dashboard-header-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-repo-header">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -31,15 +24,13 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<div class="info">
- <h1 class$="name">
+ <h1 class\$="name">
[[repo]]
- <hr/>
+ <hr>
</h1>
<div>
- <span>Detail:</span> <a href$="[[_repoUrl]]">Repo settings</a>
+ <span>Detail:</span> <a href\$="[[_repoUrl]]">Repo settings</a>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-header.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
index d9cd75e..02fb715 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-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-repo-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-repo-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-repo-header.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-header.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,26 +40,28 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-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-repo-header.js';
+suite('gr-repo-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('repoUrl reset once repo changed', () => {
- sandbox.stub(Gerrit.Nav, 'getUrlForRepo',
- repoName => `http://test.com/${repoName}`
- );
- assert.equal(element._repoUrl, undefined);
- element.repo = 'test';
- assert.equal(element._repoUrl, 'http://test.com/test');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('repoUrl reset once repo changed', () => {
+ sandbox.stub(Gerrit.Nav, 'getUrlForRepo',
+ repoName => `http://test.com/${repoName}`
+ );
+ assert.equal(element._repoUrl, undefined);
+ element.repo = 'test';
+ assert.equal(element._repoUrl, 'http://test.com/test');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index 2fc8170..3fc4291 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -14,91 +14,104 @@
* 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 GrUserHeader extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-user-header'; }
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-avatar/gr-avatar.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/dashboard-header-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-user-header_html.js';
- static get properties() {
- return {
+/**
+ * @extends Polymer.Element
+ */
+class GrUserHeader extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-user-header'; }
+
+ static get properties() {
+ return {
+ /** @type {?string} */
+ userId: {
+ type: String,
+ observer: '_accountChanged',
+ },
+
+ showDashboardLink: {
+ type: Boolean,
+ value: false,
+ },
+
+ loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * @type {?{name: ?, email: ?, registered_on: ?}}
+ */
+ _accountDetails: {
+ type: Object,
+ value: null,
+ },
+
/** @type {?string} */
- userId: {
- type: String,
- observer: '_accountChanged',
- },
-
- showDashboardLink: {
- type: Boolean,
- value: false,
- },
-
- loggedIn: {
- type: Boolean,
- value: false,
- },
-
- /**
- * @type {?{name: ?, email: ?, registered_on: ?}}
- */
- _accountDetails: {
- type: Object,
- value: null,
- },
-
- /** @type {?string} */
- _status: {
- type: String,
- value: null,
- },
- };
- }
-
- _accountChanged(userId) {
- if (!userId) {
- this._accountDetails = null;
- this._status = null;
- return;
- }
-
- this.$.restAPI.getAccountDetails(userId).then(details => {
- this._accountDetails = details;
- });
- this.$.restAPI.getAccountStatus(userId).then(status => {
- this._status = status;
- });
- }
-
- _computeDisplayClass(status) {
- return status ? ' ' : 'hide';
- }
-
- _computeDetail(accountDetails, name) {
- return accountDetails ? accountDetails[name] : '';
- }
-
- _computeStatusClass(accountDetails) {
- return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
- }
-
- _computeDashboardUrl(accountDetails) {
- if (!accountDetails) { return null; }
- const id = accountDetails._account_id;
- const email = accountDetails.email;
- if (!id && !email ) { return null; }
- return Gerrit.Nav.getUrlForUserDashboard(id ? id : email);
- }
-
- _computeDashboardLinkClass(showDashboardLink, loggedIn) {
- return showDashboardLink && loggedIn ?
- 'dashboardLink' : 'dashboardLink hide';
- }
+ _status: {
+ type: String,
+ value: null,
+ },
+ };
}
- customElements.define(GrUserHeader.is, GrUserHeader);
-})();
+ _accountChanged(userId) {
+ if (!userId) {
+ this._accountDetails = null;
+ this._status = null;
+ return;
+ }
+
+ this.$.restAPI.getAccountDetails(userId).then(details => {
+ this._accountDetails = details;
+ });
+ this.$.restAPI.getAccountStatus(userId).then(status => {
+ this._status = status;
+ });
+ }
+
+ _computeDisplayClass(status) {
+ return status ? ' ' : 'hide';
+ }
+
+ _computeDetail(accountDetails, name) {
+ return accountDetails ? accountDetails[name] : '';
+ }
+
+ _computeStatusClass(accountDetails) {
+ return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
+ }
+
+ _computeDashboardUrl(accountDetails) {
+ if (!accountDetails) { return null; }
+ const id = accountDetails._account_id;
+ const email = accountDetails.email;
+ if (!id && !email ) { return null; }
+ return Gerrit.Nav.getUrlForUserDashboard(id ? id : email);
+ }
+
+ _computeDashboardLinkClass(showDashboardLink, loggedIn) {
+ return showDashboardLink && loggedIn ?
+ 'dashboardLink' : 'dashboardLink hide';
+ }
+}
+
+customElements.define(GrUserHeader.is, GrUserHeader);
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
index 8175849..3de4277 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
@@ -1,32 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/dashboard-header-styles.html">
-
-<dom-module id="gr-user-header">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -43,16 +33,13 @@
display: none;
}
</style>
- <gr-avatar
- account="[[_accountDetails]]"
- image-size="100"
- aria-label="Account avatar"></gr-avatar>
+ <gr-avatar account="[[_accountDetails]]" image-size="100" aria-label="Account avatar"></gr-avatar>
<div class="info">
<h1 class="name">
[[_computeDetail(_accountDetails, 'name')]]
</h1>
- <hr/>
- <div class$="status [[_computeStatusClass(_accountDetails)]]">
+ <hr>
+ <div class\$="status [[_computeStatusClass(_accountDetails)]]">
<span>Status:</span> [[_status]]
</div>
<div>
@@ -62,8 +49,7 @@
</div>
<div>
<span>Joined:</span>
- <gr-date-formatter
- date-str="[[_computeDetail(_accountDetails, 'registered_on')]]">
+ <gr-date-formatter date-str="[[_computeDetail(_accountDetails, 'registered_on')]]">
</gr-date-formatter>
</div>
<gr-endpoint-decorator name="user-header">
@@ -74,11 +60,9 @@
</gr-endpoint-decorator>
</div>
<div class="info">
- <div class$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]">
- <a href$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a>
+ <div class\$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]">
+ <a href\$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-user-header.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
index 26ebeb2..169c028 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-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-user-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-user-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-user-header.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-user-header.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,50 +40,52 @@
</template>
</test-fixture>
-<script>
- suite('gr-user-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-user-header.js';
+suite('gr-user-header 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('loads and clears account info', done => {
- sandbox.stub(element.$.restAPI, 'getAccountDetails')
- .returns(Promise.resolve({
- name: 'foo',
- email: 'bar',
- registered_on: '2015-03-12 18:32:08.000000000',
- }));
- sandbox.stub(element.$.restAPI, 'getAccountStatus')
- .returns(Promise.resolve('baz'));
+ test('loads and clears account info', done => {
+ sandbox.stub(element.$.restAPI, 'getAccountDetails')
+ .returns(Promise.resolve({
+ name: 'foo',
+ email: 'bar',
+ registered_on: '2015-03-12 18:32:08.000000000',
+ }));
+ sandbox.stub(element.$.restAPI, 'getAccountStatus')
+ .returns(Promise.resolve('baz'));
- element.userId = 'foo.bar@baz';
+ element.userId = 'foo.bar@baz';
+ flush(() => {
+ assert.isOk(element._accountDetails);
+ assert.isOk(element._status);
+
+ element.userId = null;
flush(() => {
- assert.isOk(element._accountDetails);
- assert.isOk(element._status);
+ flushAsynchronousOperations();
+ assert.isNull(element._accountDetails);
+ assert.isNull(element._status);
- element.userId = null;
- flush(() => {
- flushAsynchronousOperations();
- assert.isNull(element._accountDetails);
- assert.isNull(element._status);
-
- done();
- });
+ done();
});
});
-
- test('_computeDashboardLinkClass', () => {
- assert.include(element._computeDashboardLinkClass(false, false), 'hide');
- assert.include(element._computeDashboardLinkClass(true, false), 'hide');
- assert.include(element._computeDashboardLinkClass(false, true), 'hide');
- assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
- });
});
+
+ test('_computeDashboardLinkClass', () => {
+ assert.include(element._computeDashboardLinkClass(false, false), 'hide');
+ assert.include(element._computeDashboardLinkClass(true, false), 'hide');
+ assert.include(element._computeDashboardLinkClass(false, true), 'hide');
+ assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 12c6b58..51888d6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -14,1613 +14,1642 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
- const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
- const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../admin/gr-create-change-dialog/gr-create-change-dialog.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dialog/gr-dialog.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-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js';
+import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js';
+import '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js';
+import '../gr-confirm-move-dialog/gr-confirm-move-dialog.js';
+import '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js';
+import '../gr-confirm-revert-dialog/gr-confirm-revert-dialog.js';
+import '../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js';
+import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog.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-change-actions_html.js';
+
+const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
+const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
+const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.';
+/**
+ * @enum {string}
+ */
+const LabelStatus = {
/**
- * @enum {string}
+ * This label provides what is necessary for submission.
*/
- const LabelStatus = {
- /**
- * This label provides what is necessary for submission.
- */
- OK: 'OK',
- /**
- * This label prevents the change from being submitted.
- */
- REJECT: 'REJECT',
- /**
- * The label may be set, but it's neither necessary for submission
- * nor does it block submission if set.
- */
- MAY: 'MAY',
- /**
- * The label is required for submission, but has not been satisfied.
- */
- NEED: 'NEED',
- /**
- * The label is required for submission, but is impossible to complete.
- * The likely cause is access has not been granted correctly by the
- * project owner or site administrator.
- */
- IMPOSSIBLE: 'IMPOSSIBLE',
- OPTIONAL: 'OPTIONAL',
- };
+ OK: 'OK',
+ /**
+ * This label prevents the change from being submitted.
+ */
+ REJECT: 'REJECT',
+ /**
+ * The label may be set, but it's neither necessary for submission
+ * nor does it block submission if set.
+ */
+ MAY: 'MAY',
+ /**
+ * The label is required for submission, but has not been satisfied.
+ */
+ NEED: 'NEED',
+ /**
+ * The label is required for submission, but is impossible to complete.
+ * The likely cause is access has not been granted correctly by the
+ * project owner or site administrator.
+ */
+ IMPOSSIBLE: 'IMPOSSIBLE',
+ OPTIONAL: 'OPTIONAL',
+};
- const ChangeActions = {
- ABANDON: 'abandon',
- DELETE: '/',
- DELETE_EDIT: 'deleteEdit',
- EDIT: 'edit',
- FOLLOW_UP: 'followup',
- IGNORE: 'ignore',
- MOVE: 'move',
- PRIVATE: 'private',
- PRIVATE_DELETE: 'private.delete',
- PUBLISH_EDIT: 'publishEdit',
- REBASE_EDIT: 'rebaseEdit',
- RESTORE: 'restore',
- REVERT: 'revert',
- REVERT_SUBMISSION: 'revert_submission',
- REVIEWED: 'reviewed',
- STOP_EDIT: 'stopEdit',
- UNIGNORE: 'unignore',
- UNREVIEWED: 'unreviewed',
- WIP: 'wip',
- };
+const ChangeActions = {
+ ABANDON: 'abandon',
+ DELETE: '/',
+ DELETE_EDIT: 'deleteEdit',
+ EDIT: 'edit',
+ FOLLOW_UP: 'followup',
+ IGNORE: 'ignore',
+ MOVE: 'move',
+ PRIVATE: 'private',
+ PRIVATE_DELETE: 'private.delete',
+ PUBLISH_EDIT: 'publishEdit',
+ REBASE_EDIT: 'rebaseEdit',
+ RESTORE: 'restore',
+ REVERT: 'revert',
+ REVERT_SUBMISSION: 'revert_submission',
+ REVIEWED: 'reviewed',
+ STOP_EDIT: 'stopEdit',
+ UNIGNORE: 'unignore',
+ UNREVIEWED: 'unreviewed',
+ WIP: 'wip',
+};
- const RevisionActions = {
- CHERRYPICK: 'cherrypick',
- REBASE: 'rebase',
- SUBMIT: 'submit',
- DOWNLOAD: 'download',
- };
+const RevisionActions = {
+ CHERRYPICK: 'cherrypick',
+ REBASE: 'rebase',
+ SUBMIT: 'submit',
+ DOWNLOAD: 'download',
+};
- const ActionLoadingLabels = {
- abandon: 'Abandoning...',
- cherrypick: 'Cherry-picking...',
- delete: 'Deleting...',
- move: 'Moving..',
- rebase: 'Rebasing...',
- restore: 'Restoring...',
- revert: 'Reverting...',
- revert_submission: 'Reverting Submission...',
- submit: 'Submitting...',
- };
+const ActionLoadingLabels = {
+ abandon: 'Abandoning...',
+ cherrypick: 'Cherry-picking...',
+ delete: 'Deleting...',
+ move: 'Moving..',
+ rebase: 'Rebasing...',
+ restore: 'Restoring...',
+ revert: 'Reverting...',
+ revert_submission: 'Reverting Submission...',
+ submit: 'Submitting...',
+};
- const ActionType = {
- CHANGE: 'change',
- REVISION: 'revision',
- };
+const ActionType = {
+ CHANGE: 'change',
+ REVISION: 'revision',
+};
- const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_';
+const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_';
- const QUICK_APPROVE_ACTION = {
- __key: 'review',
- __type: 'change',
- enabled: true,
- key: 'review',
- label: 'Quick approve',
- method: 'POST',
- };
+const QUICK_APPROVE_ACTION = {
+ __key: 'review',
+ __type: 'change',
+ enabled: true,
+ key: 'review',
+ label: 'Quick approve',
+ method: 'POST',
+};
- const ActionPriority = {
- CHANGE: 2,
- DEFAULT: 0,
- PRIMARY: 3,
- REVIEW: -3,
- REVISION: 1,
- };
+const ActionPriority = {
+ CHANGE: 2,
+ DEFAULT: 0,
+ PRIMARY: 3,
+ REVIEW: -3,
+ REVISION: 1,
+};
- const DOWNLOAD_ACTION = {
- enabled: true,
- label: 'Download patch',
- title: 'Open download dialog',
- __key: 'download',
- __primary: false,
- __type: 'revision',
- };
+const DOWNLOAD_ACTION = {
+ enabled: true,
+ label: 'Download patch',
+ title: 'Open download dialog',
+ __key: 'download',
+ __primary: false,
+ __type: 'revision',
+};
- const REBASE_EDIT = {
- enabled: true,
- label: 'Rebase edit',
- title: 'Rebase change edit',
- __key: 'rebaseEdit',
- __primary: false,
- __type: 'change',
- method: 'POST',
- };
+const REBASE_EDIT = {
+ enabled: true,
+ label: 'Rebase edit',
+ title: 'Rebase change edit',
+ __key: 'rebaseEdit',
+ __primary: false,
+ __type: 'change',
+ method: 'POST',
+};
- const PUBLISH_EDIT = {
- enabled: true,
- label: 'Publish edit',
- title: 'Publish change edit',
- __key: 'publishEdit',
- __primary: false,
- __type: 'change',
- method: 'POST',
- };
+const PUBLISH_EDIT = {
+ enabled: true,
+ label: 'Publish edit',
+ title: 'Publish change edit',
+ __key: 'publishEdit',
+ __primary: false,
+ __type: 'change',
+ method: 'POST',
+};
- const DELETE_EDIT = {
- enabled: true,
- label: 'Delete edit',
- title: 'Delete change edit',
- __key: 'deleteEdit',
- __primary: false,
- __type: 'change',
- method: 'DELETE',
- };
+const DELETE_EDIT = {
+ enabled: true,
+ label: 'Delete edit',
+ title: 'Delete change edit',
+ __key: 'deleteEdit',
+ __primary: false,
+ __type: 'change',
+ method: 'DELETE',
+};
- const EDIT = {
- enabled: true,
- label: 'Edit',
- title: 'Edit this change',
- __key: 'edit',
- __primary: false,
- __type: 'change',
- };
+const EDIT = {
+ enabled: true,
+ label: 'Edit',
+ title: 'Edit this change',
+ __key: 'edit',
+ __primary: false,
+ __type: 'change',
+};
- const STOP_EDIT = {
- enabled: true,
- label: 'Stop editing',
- title: 'Stop editing this change',
- __key: 'stopEdit',
- __primary: false,
- __type: 'change',
- };
+const STOP_EDIT = {
+ enabled: true,
+ label: 'Stop editing',
+ title: 'Stop editing this change',
+ __key: 'stopEdit',
+ __primary: false,
+ __type: 'change',
+};
- // Set of keys that have icons. As more icons are added to gr-icons.html, this
- // set should be expanded.
- const ACTIONS_WITH_ICONS = new Set([
- ChangeActions.ABANDON,
- ChangeActions.DELETE_EDIT,
- ChangeActions.EDIT,
- ChangeActions.PUBLISH_EDIT,
- ChangeActions.REBASE_EDIT,
- ChangeActions.RESTORE,
- ChangeActions.REVERT,
- ChangeActions.REVERT_SUBMISSION,
- ChangeActions.STOP_EDIT,
- QUICK_APPROVE_ACTION.key,
- RevisionActions.REBASE,
- RevisionActions.SUBMIT,
- ]);
+// Set of keys that have icons. As more icons are added to gr-icons.html, this
+// set should be expanded.
+const ACTIONS_WITH_ICONS = new Set([
+ ChangeActions.ABANDON,
+ ChangeActions.DELETE_EDIT,
+ ChangeActions.EDIT,
+ ChangeActions.PUBLISH_EDIT,
+ ChangeActions.REBASE_EDIT,
+ ChangeActions.RESTORE,
+ ChangeActions.REVERT,
+ ChangeActions.REVERT_SUBMISSION,
+ ChangeActions.STOP_EDIT,
+ QUICK_APPROVE_ACTION.key,
+ RevisionActions.REBASE,
+ RevisionActions.SUBMIT,
+]);
- const AWAIT_CHANGE_ATTEMPTS = 5;
- const AWAIT_CHANGE_TIMEOUT_MS = 1000;
+const AWAIT_CHANGE_ATTEMPTS = 5;
+const AWAIT_CHANGE_TIMEOUT_MS = 1000;
- const REVERT_TYPES = {
- REVERT_SINGLE_CHANGE: 1,
- REVERT_SUBMISSION: 2,
- };
+const REVERT_TYPES = {
+ REVERT_SINGLE_CHANGE: 1,
+ REVERT_SUBMISSION: 2,
+};
- /* Revert submission is skipped as the normal revert dialog will now show
- the user a choice between reverting single change or an entire submission.
- Hence, a second button is not needed.
- */
- const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION];
+/* Revert submission is skipped as the normal revert dialog will now show
+the user a choice between reverting single change or an entire submission.
+Hence, a second button is not needed.
+*/
+const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeActions extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-actions'; }
+ /**
+ * Fired when the change should be reloaded.
+ *
+ * @event reload-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when an action is tapped.
+ *
+ * @event custom-tap - naming pattern: <action key>-tap
*/
- class GrChangeActions extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-actions'; }
+
+ /**
+ * Fires to show an alert when a send is attempted on the non-latest patch.
+ *
+ * @event show-alert
+ */
+
+ /**
+ * Fires when a change action fails.
+ *
+ * @event show-error
+ */
+
+ constructor() {
+ super();
+ this.ActionType = ActionType;
+ this.ChangeActions = ChangeActions;
+ this.RevisionActions = RevisionActions;
+ }
+
+ static get properties() {
+ return {
/**
- * Fired when the change should be reloaded.
- *
- * @event reload-change
+ * @type {{
+ * _number: number,
+ * branch: string,
+ * id: string,
+ * project: string,
+ * subject: string,
+ * }}
*/
+ change: Object,
+ actions: {
+ type: Object,
+ value() { return {}; },
+ },
+ primaryActionKeys: {
+ type: Array,
+ value() {
+ return [
+ RevisionActions.SUBMIT,
+ ];
+ },
+ },
+ disableEdit: {
+ type: Boolean,
+ value: false,
+ },
+ _hasKnownChainState: {
+ type: Boolean,
+ value: false,
+ },
+ _hideQuickApproveAction: {
+ type: Boolean,
+ value: false,
+ },
+ changeNum: String,
+ changeStatus: String,
+ commitNum: String,
+ hasParent: {
+ type: Boolean,
+ observer: '_computeChainState',
+ },
+ latestPatchNum: String,
+ commitMessage: {
+ type: String,
+ value: '',
+ },
+ /** @type {?} */
+ revisionActions: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ },
+ // If property binds directly to [[revisionActions.submit]] it is not
+ // updated when revisionActions doesn't contain submit action.
+ /** @type {?} */
+ _revisionSubmitAction: {
+ type: Object,
+ computed: '_getSubmitAction(revisionActions)',
+ },
+ // If property binds directly to [[revisionActions.rebase]] it is not
+ // updated when revisionActions doesn't contain rebase action.
+ /** @type {?} */
+ _revisionRebaseAction: {
+ type: Object,
+ computed: '_getRebaseAction(revisionActions)',
+ },
+ privateByDefault: String,
- /**
- * Fired when an action is tapped.
- *
- * @event custom-tap - naming pattern: <action key>-tap
- */
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _actionLoadingMessage: {
+ type: String,
+ value: '',
+ },
+ _allActionValues: {
+ type: Array,
+ computed: '_computeAllActions(actions.*, revisionActions.*,' +
+ 'primaryActionKeys.*, _additionalActions.*, change, ' +
+ '_actionPriorityOverrides.*)',
+ },
+ _topLevelActions: {
+ type: Array,
+ computed: '_computeTopLevelActions(_allActionValues.*, ' +
+ '_hiddenActions.*, _overflowActions.*)',
+ observer: '_filterPrimaryActions',
+ },
+ _topLevelPrimaryActions: Array,
+ _topLevelSecondaryActions: Array,
+ _menuActions: {
+ type: Array,
+ computed: '_computeMenuActions(_allActionValues.*, ' +
+ '_hiddenActions.*, _overflowActions.*)',
+ },
+ _overflowActions: {
+ type: Array,
+ value() {
+ const value = [
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.WIP,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.DELETE,
+ },
+ {
+ type: ActionType.REVISION,
+ key: RevisionActions.CHERRYPICK,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.MOVE,
+ },
+ {
+ type: ActionType.REVISION,
+ key: RevisionActions.DOWNLOAD,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.IGNORE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.UNIGNORE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.REVIEWED,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.UNREVIEWED,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.PRIVATE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.PRIVATE_DELETE,
+ },
+ {
+ type: ActionType.CHANGE,
+ key: ChangeActions.FOLLOW_UP,
+ },
+ ];
+ return value;
+ },
+ },
+ _actionPriorityOverrides: {
+ type: Array,
+ value() { return []; },
+ },
+ _additionalActions: {
+ type: Array,
+ value() { return []; },
+ },
+ _hiddenActions: {
+ type: Array,
+ value() { return []; },
+ },
+ _disabledMenuActions: {
+ type: Array,
+ value() { return []; },
+ },
+ // editPatchsetLoaded == "does the current selected patch range have
+ // 'edit' as one of either basePatchNum or patchNum".
+ editPatchsetLoaded: {
+ type: Boolean,
+ value: false,
+ },
+ // editMode == "is edit mode enabled in the file list".
+ editMode: {
+ type: Boolean,
+ value: false,
+ },
+ editBasedOnCurrentPatchSet: {
+ type: Boolean,
+ value: true,
+ },
+ _revertChanges: Array,
+ };
+ }
- /**
- * Fires to show an alert when a send is attempted on the non-latest patch.
- *
- * @event show-alert
- */
+ static get observers() {
+ return [
+ '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)',
+ '_changeChanged(change)',
+ '_editStatusChanged(editMode, editPatchsetLoaded, ' +
+ 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)',
+ ];
+ }
- /**
- * Fires when a change action fails.
- *
- * @event show-error
- */
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('fullscreen-overlay-opened',
+ () => this._handleHideBackgroundContent());
+ this.addEventListener('fullscreen-overlay-closed',
+ () => this._handleShowBackgroundContent());
+ }
- constructor() {
- super();
- this.ActionType = ActionType;
- this.ChangeActions = ChangeActions;
- this.RevisionActions = RevisionActions;
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
+ this._handleLoadingComplete();
+ }
+
+ _getSubmitAction(revisionActions) {
+ return this._getRevisionAction(revisionActions, 'submit', null);
+ }
+
+ _getRebaseAction(revisionActions) {
+ return this._getRevisionAction(revisionActions, 'rebase',
+ {rebaseOnCurrent: null}
+ );
+ }
+
+ _getRevisionAction(revisionActions, actionName, emptyActionValue) {
+ if (!revisionActions) {
+ return undefined;
+ }
+ if (revisionActions[actionName] === undefined) {
+ // Return null to fire an event when reveisionActions was loaded
+ // but doesn't contain actionName. undefined doesn't fire an event
+ return emptyActionValue;
+ }
+ return revisionActions[actionName];
+ }
+
+ reload() {
+ if (!this.changeNum || !this.latestPatchNum) {
+ return Promise.resolve();
}
- static get properties() {
- return {
- /**
- * @type {{
- * _number: number,
- * branch: string,
- * id: string,
- * project: string,
- * subject: string,
- * }}
- */
- change: Object,
- actions: {
- type: Object,
- value() { return {}; },
- },
- primaryActionKeys: {
- type: Array,
- value() {
- return [
- RevisionActions.SUBMIT,
- ];
- },
- },
- disableEdit: {
- type: Boolean,
- value: false,
- },
- _hasKnownChainState: {
- type: Boolean,
- value: false,
- },
- _hideQuickApproveAction: {
- type: Boolean,
- value: false,
- },
- changeNum: String,
- changeStatus: String,
- commitNum: String,
- hasParent: {
- type: Boolean,
- observer: '_computeChainState',
- },
- latestPatchNum: String,
- commitMessage: {
- type: String,
- value: '',
- },
- /** @type {?} */
- revisionActions: {
- type: Object,
- notify: true,
- value() { return {}; },
- },
- // If property binds directly to [[revisionActions.submit]] it is not
- // updated when revisionActions doesn't contain submit action.
- /** @type {?} */
- _revisionSubmitAction: {
- type: Object,
- computed: '_getSubmitAction(revisionActions)',
- },
- // If property binds directly to [[revisionActions.rebase]] it is not
- // updated when revisionActions doesn't contain rebase action.
- /** @type {?} */
- _revisionRebaseAction: {
- type: Object,
- computed: '_getRebaseAction(revisionActions)',
- },
- privateByDefault: String,
+ this._loading = true;
+ return this._getRevisionActions()
+ .then(revisionActions => {
+ if (!revisionActions) { return; }
- _loading: {
- type: Boolean,
- value: true,
- },
- _actionLoadingMessage: {
- type: String,
- value: '',
- },
- _allActionValues: {
- type: Array,
- computed: '_computeAllActions(actions.*, revisionActions.*,' +
- 'primaryActionKeys.*, _additionalActions.*, change, ' +
- '_actionPriorityOverrides.*)',
- },
- _topLevelActions: {
- type: Array,
- computed: '_computeTopLevelActions(_allActionValues.*, ' +
- '_hiddenActions.*, _overflowActions.*)',
- observer: '_filterPrimaryActions',
- },
- _topLevelPrimaryActions: Array,
- _topLevelSecondaryActions: Array,
- _menuActions: {
- type: Array,
- computed: '_computeMenuActions(_allActionValues.*, ' +
- '_hiddenActions.*, _overflowActions.*)',
- },
- _overflowActions: {
- type: Array,
- value() {
- const value = [
- {
- type: ActionType.CHANGE,
- key: ChangeActions.WIP,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.DELETE,
- },
- {
- type: ActionType.REVISION,
- key: RevisionActions.CHERRYPICK,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.MOVE,
- },
- {
- type: ActionType.REVISION,
- key: RevisionActions.DOWNLOAD,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.IGNORE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.UNIGNORE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.REVIEWED,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.UNREVIEWED,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.PRIVATE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.PRIVATE_DELETE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.FOLLOW_UP,
- },
- ];
- return value;
- },
- },
- _actionPriorityOverrides: {
- type: Array,
- value() { return []; },
- },
- _additionalActions: {
- type: Array,
- value() { return []; },
- },
- _hiddenActions: {
- type: Array,
- value() { return []; },
- },
- _disabledMenuActions: {
- type: Array,
- value() { return []; },
- },
- // editPatchsetLoaded == "does the current selected patch range have
- // 'edit' as one of either basePatchNum or patchNum".
- editPatchsetLoaded: {
- type: Boolean,
- value: false,
- },
- // editMode == "is edit mode enabled in the file list".
- editMode: {
- type: Boolean,
- value: false,
- },
- editBasedOnCurrentPatchSet: {
- type: Boolean,
- value: true,
- },
- _revertChanges: Array,
- };
- }
-
- static get observers() {
- return [
- '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)',
- '_changeChanged(change)',
- '_editStatusChanged(editMode, editPatchsetLoaded, ' +
- 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)',
- ];
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('fullscreen-overlay-opened',
- () => this._handleHideBackgroundContent());
- this.addEventListener('fullscreen-overlay-closed',
- () => this._handleShowBackgroundContent());
- }
-
- /** @override */
- ready() {
- super.ready();
- this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
- this._handleLoadingComplete();
- }
-
- _getSubmitAction(revisionActions) {
- return this._getRevisionAction(revisionActions, 'submit', null);
- }
-
- _getRebaseAction(revisionActions) {
- return this._getRevisionAction(revisionActions, 'rebase',
- {rebaseOnCurrent: null}
- );
- }
-
- _getRevisionAction(revisionActions, actionName, emptyActionValue) {
- if (!revisionActions) {
- return undefined;
- }
- if (revisionActions[actionName] === undefined) {
- // Return null to fire an event when reveisionActions was loaded
- // but doesn't contain actionName. undefined doesn't fire an event
- return emptyActionValue;
- }
- return revisionActions[actionName];
- }
-
- reload() {
- if (!this.changeNum || !this.latestPatchNum) {
- return Promise.resolve();
- }
-
- this._loading = true;
- return this._getRevisionActions()
- .then(revisionActions => {
- if (!revisionActions) { return; }
-
- this.revisionActions = this._updateRebaseAction(revisionActions);
- this._sendShowRevisionActions({
- change: this.change,
- revisionActions,
- });
- this._handleLoadingComplete();
- })
- .catch(err => {
- this.fire('show-alert', {message: ERR_REVISION_ACTIONS});
- this._loading = false;
- throw err;
+ this.revisionActions = this._updateRebaseAction(revisionActions);
+ this._sendShowRevisionActions({
+ change: this.change,
+ revisionActions,
});
- }
+ this._handleLoadingComplete();
+ })
+ .catch(err => {
+ this.fire('show-alert', {message: ERR_REVISION_ACTIONS});
+ this._loading = false;
+ throw err;
+ });
+ }
- _handleLoadingComplete() {
- Gerrit.awaitPluginsLoaded().then(() => this._loading = false);
- }
+ _handleLoadingComplete() {
+ Gerrit.awaitPluginsLoaded().then(() => this._loading = false);
+ }
- _sendShowRevisionActions(detail) {
- this.$.jsAPI.handleEvent(
- this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS,
- detail
- );
- }
+ _sendShowRevisionActions(detail) {
+ this.$.jsAPI.handleEvent(
+ this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS,
+ detail
+ );
+ }
- _updateRebaseAction(revisionActions) {
- if (revisionActions && revisionActions.rebase) {
- revisionActions.rebase.rebaseOnCurrent =
- !!revisionActions.rebase.enabled;
- this._parentIsCurrent = !revisionActions.rebase.enabled;
- revisionActions.rebase.enabled = true;
- } else {
- this._parentIsCurrent = true;
- }
- return revisionActions;
+ _updateRebaseAction(revisionActions) {
+ if (revisionActions && revisionActions.rebase) {
+ revisionActions.rebase.rebaseOnCurrent =
+ !!revisionActions.rebase.enabled;
+ this._parentIsCurrent = !revisionActions.rebase.enabled;
+ revisionActions.rebase.enabled = true;
+ } else {
+ this._parentIsCurrent = true;
}
+ return revisionActions;
+ }
- _changeChanged() {
- this.reload();
- }
+ _changeChanged() {
+ this.reload();
+ }
- addActionButton(type, label) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type: ${type}`);
- }
- const action = {
- enabled: true,
- label,
- __type: type,
- __key: ADDITIONAL_ACTION_KEY_PREFIX +
- Math.random().toString(36)
- .substr(2),
- };
- this.push('_additionalActions', action);
- return action.__key;
+ addActionButton(type, label) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type: ${type}`);
}
+ const action = {
+ enabled: true,
+ label,
+ __type: type,
+ __key: ADDITIONAL_ACTION_KEY_PREFIX +
+ Math.random().toString(36)
+ .substr(2),
+ };
+ this.push('_additionalActions', action);
+ return action.__key;
+ }
- removeActionButton(key) {
- const idx = this._indexOfActionButtonWithKey(key);
- if (idx === -1) {
- return;
- }
- this.splice('_additionalActions', idx, 1);
+ removeActionButton(key) {
+ const idx = this._indexOfActionButtonWithKey(key);
+ if (idx === -1) {
+ return;
}
+ this.splice('_additionalActions', idx, 1);
+ }
- setActionButtonProp(key, prop, value) {
- this.set([
- '_additionalActions',
- this._indexOfActionButtonWithKey(key),
- prop,
- ], value);
- }
+ setActionButtonProp(key, prop, value) {
+ this.set([
+ '_additionalActions',
+ this._indexOfActionButtonWithKey(key),
+ prop,
+ ], value);
+ }
- setActionOverflow(type, key, overflow) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type given: ${type}`);
- }
- const index = this._getActionOverflowIndex(type, key);
- const action = {
- type,
- key,
- overflow,
- };
- if (!overflow && index !== -1) {
- this.splice('_overflowActions', index, 1);
- } else if (overflow) {
- this.push('_overflowActions', action);
- }
+ setActionOverflow(type, key, overflow) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type given: ${type}`);
}
-
- setActionPriority(type, key, priority) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type given: ${type}`);
- }
- const index = this._actionPriorityOverrides
- .findIndex(action => action.type === type && action.key === key);
- const action = {
- type,
- key,
- priority,
- };
- if (index !== -1) {
- this.set('_actionPriorityOverrides', index, action);
- } else {
- this.push('_actionPriorityOverrides', action);
- }
- }
-
- setActionHidden(type, key, hidden) {
- if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
- throw Error(`Invalid action type given: ${type}`);
- }
-
- const idx = this._hiddenActions.indexOf(key);
- if (hidden && idx === -1) {
- this.push('_hiddenActions', key);
- } else if (!hidden && idx !== -1) {
- this.splice('_hiddenActions', idx, 1);
- }
- }
-
- getActionDetails(action) {
- if (this.revisionActions[action]) {
- return this.revisionActions[action];
- } else if (this.actions[action]) {
- return this.actions[action];
- }
- }
-
- _indexOfActionButtonWithKey(key) {
- for (let i = 0; i < this._additionalActions.length; i++) {
- if (this._additionalActions[i].__key === key) {
- return i;
- }
- }
- return -1;
- }
-
- _getRevisionActions() {
- return this.$.restAPI.getChangeRevisionActions(this.changeNum,
- this.latestPatchNum);
- }
-
- _shouldHideActions(actions, loading) {
- return loading || !actions || !actions.base || !actions.base.length;
- }
-
- _keyCount(changeRecord) {
- return Object.keys((changeRecord && changeRecord.base) || {}).length;
- }
-
- _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord,
- additionalActionsChangeRecord) {
- // Polymer 2: check for undefined
- if ([
- actionsChangeRecord,
- revisionActionsChangeRecord,
- additionalActionsChangeRecord,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- const additionalActions = (additionalActionsChangeRecord &&
- additionalActionsChangeRecord.base) || [];
- this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
- this._keyCount(revisionActionsChangeRecord) === 0 &&
- additionalActions.length === 0;
- this._actionLoadingMessage = '';
- this._disabledMenuActions = [];
-
- const revisionActions = revisionActionsChangeRecord.base || {};
- if (Object.keys(revisionActions).length !== 0) {
- if (!revisionActions.download) {
- this.set('revisionActions.download', DOWNLOAD_ACTION);
- }
- }
- }
-
- /**
- * @param {string=} actionName
- */
- _deleteAndNotify(actionName) {
- if (this.actions && this.actions[actionName]) {
- delete this.actions[actionName];
- // We assign a fake value of 'false' to support Polymer 2
- // see https://github.com/Polymer/polymer/issues/2631
- this.notifyPath('actions.' + actionName, false);
- }
- }
-
- _editStatusChanged(editMode, editPatchsetLoaded,
- editBasedOnCurrentPatchSet, disableEdit) {
- // Polymer 2: check for undefined
- if ([
- editMode,
- editBasedOnCurrentPatchSet,
- disableEdit,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (disableEdit) {
- this._deleteAndNotify('publishEdit');
- this._deleteAndNotify('rebaseEdit');
- this._deleteAndNotify('deleteEdit');
- this._deleteAndNotify('stopEdit');
- this._deleteAndNotify('edit');
- return;
- }
- if (this.actions && editPatchsetLoaded) {
- // Only show actions that mutate an edit if an actual edit patch set
- // is loaded.
- if (this.changeIsOpen(this.change)) {
- if (editBasedOnCurrentPatchSet) {
- if (!this.actions.publishEdit) {
- this.set('actions.publishEdit', PUBLISH_EDIT);
- }
- this._deleteAndNotify('rebaseEdit');
- } else {
- if (!this.actions.rebaseEdit) {
- this.set('actions.rebaseEdit', REBASE_EDIT);
- }
- this._deleteAndNotify('publishEdit');
- }
- }
- if (!this.actions.deleteEdit) {
- this.set('actions.deleteEdit', DELETE_EDIT);
- }
- } else {
- this._deleteAndNotify('publishEdit');
- this._deleteAndNotify('rebaseEdit');
- this._deleteAndNotify('deleteEdit');
- }
-
- if (this.actions && this.changeIsOpen(this.change)) {
- // Only show edit button if there is no edit patchset loaded and the
- // file list is not in edit mode.
- if (editPatchsetLoaded || editMode) {
- this._deleteAndNotify('edit');
- } else {
- if (!this.actions.edit) { this.set('actions.edit', EDIT); }
- }
- // Only show STOP_EDIT if edit mode is enabled, but no edit patch set
- // is loaded.
- if (editMode && !editPatchsetLoaded) {
- if (!this.actions.stopEdit) {
- this.set('actions.stopEdit', STOP_EDIT);
- }
- } else {
- this._deleteAndNotify('stopEdit');
- }
- } else {
- // Remove edit button.
- this._deleteAndNotify('edit');
- }
- }
-
- _getValuesFor(obj) {
- return Object.keys(obj).map(key => obj[key]);
- }
-
- _getLabelStatus(label) {
- if (label.approved) {
- return LabelStatus.OK;
- } else if (label.rejected) {
- return LabelStatus.REJECT;
- } else if (label.optional) {
- return LabelStatus.OPTIONAL;
- } else {
- return LabelStatus.NEED;
- }
- }
-
- /**
- * Get highest score for last missing permitted label for current change.
- * Returns null if no labels permitted or more than one label missing.
- *
- * @return {{label: string, score: string}|null}
- */
- _getTopMissingApproval() {
- if (!this.change ||
- !this.change.labels ||
- !this.change.permitted_labels) {
- return null;
- }
- let result;
- for (const label in this.change.labels) {
- if (!(label in this.change.permitted_labels)) {
- continue;
- }
- if (this.change.permitted_labels[label].length === 0) {
- continue;
- }
- const status = this._getLabelStatus(this.change.labels[label]);
- if (status === LabelStatus.NEED) {
- if (result) {
- // More than one label is missing, so it's unclear which to quick
- // approve, return null;
- return null;
- }
- result = label;
- } else if (status === LabelStatus.REJECT ||
- status === LabelStatus.IMPOSSIBLE) {
- return null;
- }
- }
- if (result) {
- const score = this.change.permitted_labels[result].slice(-1)[0];
- const maxScore =
- Object.keys(this.change.labels[result].values).slice(-1)[0];
- if (score === maxScore) {
- // Allow quick approve only for maximal score.
- return {
- label: result,
- score,
- };
- }
- }
- return null;
- }
-
- hideQuickApproveAction() {
- this._topLevelSecondaryActions =
- this._topLevelSecondaryActions
- .filter(sa => sa.key !== QUICK_APPROVE_ACTION.key);
- this._hideQuickApproveAction = true;
- }
-
- _getQuickApproveAction() {
- if (this._hideQuickApproveAction) {
- return null;
- }
- const approval = this._getTopMissingApproval();
- if (!approval) {
- return null;
- }
- const action = Object.assign({}, QUICK_APPROVE_ACTION);
- action.label = approval.label + approval.score;
- const review = {
- drafts: 'PUBLISH_ALL_REVISIONS',
- labels: {},
- };
- review.labels[approval.label] = approval.score;
- action.payload = review;
- return action;
- }
-
- _getActionValues(actionsChangeRecord, primariesChangeRecord,
- additionalActionsChangeRecord, type) {
- if (!actionsChangeRecord || !primariesChangeRecord) { return []; }
-
- const actions = actionsChangeRecord.base || {};
- const primaryActionKeys = primariesChangeRecord.base || [];
- const result = [];
- const values = this._getValuesFor(
- type === ActionType.CHANGE ? ChangeActions : RevisionActions);
- const pluginActions = [];
- Object.keys(actions).forEach(a => {
- actions[a].__key = a;
- actions[a].__type = type;
- actions[a].__primary = primaryActionKeys.includes(a);
- // Plugin actions always contain ~ in the key.
- if (a.indexOf('~') !== -1) {
- this._populateActionUrl(actions[a]);
- pluginActions.push(actions[a]);
- // Add server-side provided plugin actions to overflow menu.
- this._overflowActions.push({
- type,
- key: a,
- });
- return;
- } else if (!values.includes(a)) {
- return;
- }
- actions[a].label = this._getActionLabel(actions[a]);
-
- // Triggers a re-render by ensuring object inequality.
- result.push(Object.assign({}, actions[a]));
- });
-
- let additionalActions = (additionalActionsChangeRecord &&
- additionalActionsChangeRecord.base) || [];
- additionalActions = additionalActions
- .filter(a => a.__type === type)
- .map(a => {
- a.__primary = primaryActionKeys.includes(a.__key);
- // Triggers a re-render by ensuring object inequality.
- return Object.assign({}, a);
- });
- return result.concat(additionalActions).concat(pluginActions);
- }
-
- _populateActionUrl(action) {
- const patchNum =
- action.__type === ActionType.REVISION ? this.latestPatchNum : null;
- this.$.restAPI.getChangeActionURL(
- this.changeNum, patchNum, '/' + action.__key)
- .then(url => action.__url = url);
- }
-
- /**
- * Given a change action, return a display label that uses the appropriate
- * casing or includes explanatory details.
- */
- _getActionLabel(action) {
- if (action.label === 'Delete') {
- // This label is common within change and revision actions. Make it more
- // explicit to the user.
- return 'Delete change';
- } else if (action.label === 'WIP') {
- return 'Mark as work in progress';
- }
- // Otherwise, just map the name to sentence case.
- return this._toSentenceCase(action.label);
- }
-
- /**
- * Capitalize the first letter and lowecase all others.
- *
- * @param {string} s
- * @return {string}
- */
- _toSentenceCase(s) {
- if (!s.length) { return ''; }
- return s[0].toUpperCase() + s.slice(1).toLowerCase();
- }
-
- _computeLoadingLabel(action) {
- return ActionLoadingLabels[action] || 'Working...';
- }
-
- _canSubmitChange() {
- return this.$.jsAPI.canSubmitChange(this.change,
- this._getRevision(this.change, this.latestPatchNum));
- }
-
- _getRevision(change, patchNum) {
- for (const rev of Object.values(change.revisions)) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- return rev;
- }
- }
- return null;
- }
-
- showRevertDialog() {
- const query = 'submissionid:' + this.change.submission_id;
- /* A chromium plugin expects that the modifyRevertMsg hook will only
- be called after the revert button is pressed, hence we populate the
- revert dialog after revert button is pressed. */
- this.$.restAPI.getChanges('', query)
- .then(changes => {
- this.$.confirmRevertDialog.populate(this.change,
- this.commitMessage, changes);
- this._showActionDialog(this.$.confirmRevertDialog);
- });
- }
-
- showRevertSubmissionDialog() {
- const query = 'submissionid:' + this.change.submission_id;
- this.$.restAPI.getChanges('', query)
- .then(changes => {
- this.$.confirmRevertSubmissionDialog.
- _populateRevertSubmissionMessage(
- this.commitMessage, this.change, changes);
- this._showActionDialog(this.$.confirmRevertSubmissionDialog);
- });
- }
-
- _handleActionTap(e) {
- e.preventDefault();
- let el = Polymer.dom(e).localTarget;
- while (el.tagName.toLowerCase() !== 'gr-button') {
- if (!el.parentElement) { return; }
- el = el.parentElement;
- }
-
- const key = el.getAttribute('data-action-key');
- if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
- key.indexOf('~') !== -1) {
- this.fire(`${key}-tap`, {node: el});
- return;
- }
- const type = el.getAttribute('data-action-type');
- this._handleAction(type, key);
- }
-
- _handleOveflowItemTap(e) {
- e.preventDefault();
- const el = Polymer.dom(e).localTarget;
- const key = e.detail.action.__key;
- if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
- key.indexOf('~') !== -1) {
- this.fire(`${key}-tap`, {node: el});
- return;
- }
- this._handleAction(e.detail.action.__type, e.detail.action.__key);
- }
-
- _handleAction(type, key) {
- this.$.reporting.reportInteraction(`${type}-${key}`);
- switch (type) {
- case ActionType.REVISION:
- this._handleRevisionAction(key);
- break;
- case ActionType.CHANGE:
- this._handleChangeAction(key);
- break;
- default:
- this._fireAction(this._prependSlash(key), this.actions[key], false);
- }
- }
-
- _handleChangeAction(key) {
- let action;
- switch (key) {
- case ChangeActions.REVERT:
- this.showRevertDialog();
- break;
- case ChangeActions.REVERT_SUBMISSION:
- this.showRevertSubmissionDialog();
- break;
- case ChangeActions.ABANDON:
- this._showActionDialog(this.$.confirmAbandonDialog);
- break;
- case QUICK_APPROVE_ACTION.key:
- action = this._allActionValues.find(o => o.key === key);
- this._fireAction(
- this._prependSlash(key), action, true, action.payload);
- break;
- case ChangeActions.EDIT:
- this._handleEditTap();
- break;
- case ChangeActions.STOP_EDIT:
- this._handleStopEditTap();
- break;
- case ChangeActions.DELETE:
- this._handleDeleteTap();
- break;
- case ChangeActions.DELETE_EDIT:
- this._handleDeleteEditTap();
- break;
- case ChangeActions.FOLLOW_UP:
- this._handleFollowUpTap();
- break;
- case ChangeActions.WIP:
- this._handleWipTap();
- break;
- case ChangeActions.MOVE:
- this._handleMoveTap();
- break;
- case ChangeActions.PUBLISH_EDIT:
- this._handlePublishEditTap();
- break;
- case ChangeActions.REBASE_EDIT:
- this._handleRebaseEditTap();
- break;
- default:
- this._fireAction(this._prependSlash(key), this.actions[key], false);
- }
- }
-
- _handleRevisionAction(key) {
- switch (key) {
- case RevisionActions.REBASE:
- this._showActionDialog(this.$.confirmRebase);
- this.$.confirmRebase.fetchRecentChanges();
- break;
- case RevisionActions.CHERRYPICK:
- this._handleCherrypickTap();
- break;
- case RevisionActions.DOWNLOAD:
- this._handleDownloadTap();
- break;
- case RevisionActions.SUBMIT:
- if (!this._canSubmitChange()) { return; }
- this._showActionDialog(this.$.confirmSubmitDialog);
- break;
- default:
- this._fireAction(this._prependSlash(key),
- this.revisionActions[key], true);
- }
- }
-
- _prependSlash(key) {
- return key === '/' ? key : `/${key}`;
- }
-
- /**
- * _hasKnownChainState set to true true if hasParent is defined (can be
- * either true or false). set to false otherwise.
- */
- _computeChainState(hasParent) {
- this._hasKnownChainState = true;
- }
-
- _calculateDisabled(action, hasKnownChainState) {
- if (action.__key === 'rebase' && hasKnownChainState === false) {
- return true;
- }
- return !action.enabled;
- }
-
- _handleConfirmDialogCancel() {
- this._hideAllDialogs();
- }
-
- _hideAllDialogs() {
- const dialogEls =
- Polymer.dom(this.root).querySelectorAll('.confirmDialog');
- for (const dialogEl of dialogEls) { dialogEl.hidden = true; }
- this.$.overlay.close();
- }
-
- _handleRebaseConfirm(e) {
- const el = this.$.confirmRebase;
- const payload = {base: e.detail.base};
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction('/rebase', this.revisionActions.rebase, true, payload);
- }
-
- _handleCherrypickConfirm() {
- this._handleCherryPickRestApi(false);
- }
-
- _handleCherrypickConflictConfirm() {
- this._handleCherryPickRestApi(true);
- }
-
- _handleCherryPickRestApi(conflicts) {
- const el = this.$.confirmCherrypick;
- if (!el.branch) {
- this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
- return;
- }
- if (!el.message) {
- this.fire('show-alert', {message: ERR_COMMIT_EMPTY});
- return;
- }
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction(
- '/cherrypick',
- this.revisionActions.cherrypick,
- true,
- {
- destination: el.branch,
- base: el.baseCommit ? el.baseCommit : null,
- message: el.message,
- allow_conflicts: conflicts,
- }
- );
- }
-
- _handleMoveConfirm() {
- const el = this.$.confirmMove;
- if (!el.branch) {
- this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
- return;
- }
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction(
- '/move',
- this.actions.move,
- false,
- {
- destination_branch: el.branch,
- message: el.message,
- }
- );
- }
-
- _handleRevertDialogConfirm(e) {
- const revertType = e.detail.revertType;
- const message = e.detail.message;
- const el = this.$.confirmRevertDialog;
- this.$.overlay.close();
- el.hidden = true;
- switch (revertType) {
- case REVERT_TYPES.REVERT_SINGLE_CHANGE:
- this._fireAction('/revert', this.actions.revert, false,
- {message});
- break;
- case REVERT_TYPES.REVERT_SUBMISSION:
- this._fireAction('/revert_submission', this.actions.revert_submission,
- false, {message});
- break;
- default:
- console.error('invalid revert type');
- }
- }
-
- _handleRevertSubmissionDialogConfirm() {
- const el = this.$.confirmRevertSubmissionDialog;
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction('/revert_submission', this.actions.revert_submission,
- false, {message: el.message});
- }
-
- _handleAbandonDialogConfirm() {
- const el = this.$.confirmAbandonDialog;
- this.$.overlay.close();
- el.hidden = true;
- this._fireAction('/abandon', this.actions.abandon, false,
- {message: el.message});
- }
-
- _handleCreateFollowUpChange() {
- this.$.createFollowUpChange.handleCreateChange();
- this._handleCloseCreateFollowUpChange();
- }
-
- _handleCloseCreateFollowUpChange() {
- this.$.overlay.close();
- }
-
- _handleDeleteConfirm() {
- this._fireAction('/', this.actions[ChangeActions.DELETE], false);
- }
-
- _handleDeleteEditConfirm() {
- this._hideAllDialogs();
-
- this._fireAction('/edit', this.actions.deleteEdit, false);
- }
-
- _handleSubmitConfirm() {
- if (!this._canSubmitChange()) { return; }
- this._hideAllDialogs();
- this._fireAction('/submit', this.revisionActions.submit, true);
- }
-
- _getActionOverflowIndex(type, key) {
- return this._overflowActions
- .findIndex(action => action.type === type && action.key === key);
- }
-
- _setLoadingOnButtonWithKey(type, key) {
- this._actionLoadingMessage = this._computeLoadingLabel(key);
- let buttonKey = key;
- // TODO(dhruvsri): clean this up later
- // If key is revert-submission, then button key should be 'revert'
- if (buttonKey === ChangeActions.REVERT_SUBMISSION) {
- // Revert submission button no longer exists
- buttonKey = ChangeActions.REVERT;
- }
-
- // If the action appears in the overflow menu.
- if (this._getActionOverflowIndex(type, buttonKey) !== -1) {
- this.push('_disabledMenuActions', buttonKey === '/' ? 'delete' :
- buttonKey);
- return function() {
- this._actionLoadingMessage = '';
- this._disabledMenuActions = [];
- }.bind(this);
- }
-
- // Otherwise it's a top-level action.
- const buttonEl = this.shadowRoot
- .querySelector(`[data-action-key="${buttonKey}"]`);
- buttonEl.setAttribute('loading', true);
- buttonEl.disabled = true;
- return function() {
- this._actionLoadingMessage = '';
- buttonEl.removeAttribute('loading');
- buttonEl.disabled = false;
- }.bind(this);
- }
-
- /**
- * @param {string} endpoint
- * @param {!Object|undefined} action
- * @param {boolean} revAction
- * @param {!Object|string=} opt_payload
- */
- _fireAction(endpoint, action, revAction, opt_payload) {
- const cleanupFn =
- this._setLoadingOnButtonWithKey(action.__type, action.__key);
-
- this._send(action.method, opt_payload, endpoint, revAction, cleanupFn,
- action).then(this._handleResponse.bind(this, action));
- }
-
- _showActionDialog(dialog) {
- this._hideAllDialogs();
-
- dialog.hidden = false;
- this.$.overlay.open().then(() => {
- if (dialog.resetFocus) {
- dialog.resetFocus();
- }
- });
- }
-
- // TODO(rmistry): Redo this after
- // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
- _setLabelValuesOnRevert(newChangeId) {
- const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
- if (!labels) { return Promise.resolve(); }
- return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels});
- }
-
- _handleResponse(action, response) {
- if (!response) { return; }
- return this.$.restAPI.getResponseObject(response).then(obj => {
- switch (action.__key) {
- case ChangeActions.REVERT:
- this._waitForChangeReachable(obj._number)
- .then(() => this._setLabelValuesOnRevert(obj._number))
- .then(() => {
- Gerrit.Nav.navigateToChange(obj);
- });
- break;
- case RevisionActions.CHERRYPICK:
- this._waitForChangeReachable(obj._number).then(() => {
- Gerrit.Nav.navigateToChange(obj);
- });
- break;
- case ChangeActions.DELETE:
- if (action.__type === ActionType.CHANGE) {
- Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot());
- }
- break;
- case ChangeActions.WIP:
- case ChangeActions.DELETE_EDIT:
- case ChangeActions.PUBLISH_EDIT:
- case ChangeActions.REBASE_EDIT:
- Gerrit.Nav.navigateToChange(this.change);
- break;
- case ChangeActions.REVERT_SUBMISSION:
- if (!obj.revert_changes || !obj.revert_changes.length) return;
- /* If there is only 1 change then gerrit will automatically
- redirect to that change */
- Gerrit.Nav.navigateToSearchQuery('topic: ' +
- obj.revert_changes[0].topic);
- break;
- default:
- this.dispatchEvent(new CustomEvent('reload-change',
- {detail: {action: action.__key}, bubbles: false}));
- break;
- }
- });
- }
-
- _handleShowRevertSubmissionChangesConfirm() {
- this._hideAllDialogs();
- }
-
- _handleResponseError(action, response, body) {
- if (action && action.__key === RevisionActions.CHERRYPICK) {
- if (response && response.status === 409 &&
- body && !body.allow_conflicts) {
- return this._showActionDialog(
- this.$.confirmCherrypickConflict);
- }
- }
- return response.text().then(errText => {
- this.fire('show-error',
- {message: `Could not perform action: ${errText}`});
- if (!errText.startsWith('Change is already up to date')) {
- throw Error(errText);
- }
- });
- }
-
- /**
- * @param {string} method
- * @param {string|!Object|undefined} payload
- * @param {string} actionEndpoint
- * @param {boolean} revisionAction
- * @param {?Function} cleanupFn
- * @param {!Object|undefined} action
- */
- _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) {
- const handleError = response => {
- cleanupFn.call(this);
- this._handleResponseError(action, response, payload);
- };
-
- return this.fetchChangeUpdates(this.change, this.$.restAPI)
- .then(result => {
- if (!result.isLatest) {
- this.fire('show-alert', {
- message: 'Cannot set label: a newer patch has been ' +
- 'uploaded to this change.',
- action: 'Reload',
- callback: () => {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this.change);
- },
- });
-
- // Because this is not a network error, call the cleanup function
- // but not the error handler.
- cleanupFn();
-
- return Promise.resolve();
- }
- const patchNum = revisionAction ? this.latestPatchNum : null;
- return this.$.restAPI.executeChangeAction(this.changeNum, method,
- actionEndpoint, patchNum, payload, handleError)
- .then(response => {
- cleanupFn.call(this);
- return response;
- });
- });
- }
-
- _handleAbandonTap() {
- this._showActionDialog(this.$.confirmAbandonDialog);
- }
-
- _handleCherrypickTap() {
- this.$.confirmCherrypick.branch = '';
- this._showActionDialog(this.$.confirmCherrypick);
- }
-
- _handleMoveTap() {
- this.$.confirmMove.branch = '';
- this.$.confirmMove.message = '';
- this._showActionDialog(this.$.confirmMove);
- }
-
- _handleDownloadTap() {
- this.fire('download-tap', null, {bubbles: false});
- }
-
- _handleDeleteTap() {
- this._showActionDialog(this.$.confirmDeleteDialog);
- }
-
- _handleDeleteEditTap() {
- this._showActionDialog(this.$.confirmDeleteEditDialog);
- }
-
- _handleFollowUpTap() {
- this._showActionDialog(this.$.createFollowUpDialog);
- }
-
- _handleWipTap() {
- this._fireAction('/wip', this.actions.wip, false);
- }
-
- _handlePublishEditTap() {
- this._fireAction('/edit:publish', this.actions.publishEdit, false);
- }
-
- _handleRebaseEditTap() {
- this._fireAction('/edit:rebase', this.actions.rebaseEdit, false);
- }
-
- _handleHideBackgroundContent() {
- this.$.mainContent.classList.add('overlayOpen');
- }
-
- _handleShowBackgroundContent() {
- this.$.mainContent.classList.remove('overlayOpen');
- }
-
- /**
- * Merge sources of change actions into a single ordered array of action
- * values.
- *
- * @param {!Array} changeActionsRecord
- * @param {!Array} revisionActionsRecord
- * @param {!Array} primariesRecord
- * @param {!Array} additionalActionsRecord
- * @param {!Object} change The change object.
- * @return {!Array}
- */
- _computeAllActions(changeActionsRecord, revisionActionsRecord,
- primariesRecord, additionalActionsRecord, change) {
- // Polymer 2: check for undefined
- if ([
- changeActionsRecord,
- revisionActionsRecord,
- primariesRecord,
- additionalActionsRecord,
- change,
- ].some(arg => arg === undefined)) {
- return [];
- }
-
- const revisionActionValues = this._getActionValues(revisionActionsRecord,
- primariesRecord, additionalActionsRecord, ActionType.REVISION);
- const changeActionValues = this._getActionValues(changeActionsRecord,
- primariesRecord, additionalActionsRecord, ActionType.CHANGE);
- const quickApprove = this._getQuickApproveAction();
- if (quickApprove) {
- changeActionValues.unshift(quickApprove);
- }
-
- return revisionActionValues
- .concat(changeActionValues)
- .sort(this._actionComparator.bind(this))
- .map(action => {
- if (ACTIONS_WITH_ICONS.has(action.__key)) {
- action.icon = action.__key;
- }
- return action;
- })
- .filter(action => !this._shouldSkipAction(action));
- }
-
- _getActionPriority(action) {
- if (action.__type && action.__key) {
- const overrideAction = this._actionPriorityOverrides
- .find(i => i.type === action.__type && i.key === action.__key);
-
- if (overrideAction !== undefined) {
- return overrideAction.priority;
- }
- }
- if (action.__key === 'review') {
- return ActionPriority.REVIEW;
- } else if (action.__primary) {
- return ActionPriority.PRIMARY;
- } else if (action.__type === ActionType.CHANGE) {
- return ActionPriority.CHANGE;
- } else if (action.__type === ActionType.REVISION) {
- return ActionPriority.REVISION;
- }
- return ActionPriority.DEFAULT;
- }
-
- /**
- * Sort comparator to define the order of change actions.
- */
- _actionComparator(actionA, actionB) {
- const priorityDelta = this._getActionPriority(actionA) -
- this._getActionPriority(actionB);
- // Sort by the button label if same priority.
- if (priorityDelta === 0) {
- return actionA.label > actionB.label ? 1 : -1;
- } else {
- return priorityDelta;
- }
- }
-
- _shouldSkipAction(action) {
- return SKIP_ACTION_KEYS.includes(action.__key);
- }
-
- _computeTopLevelActions(actionRecord, hiddenActionsRecord) {
- const hiddenActions = hiddenActionsRecord.base || [];
- return actionRecord.base.filter(a => {
- const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
- return !(overflow || hiddenActions.includes(a.__key));
- });
- }
-
- _filterPrimaryActions(_topLevelActions) {
- this._topLevelPrimaryActions = _topLevelActions.filter(action =>
- action.__primary);
- this._topLevelSecondaryActions = _topLevelActions.filter(action =>
- !action.__primary);
- }
-
- _computeMenuActions(actionRecord, hiddenActionsRecord) {
- const hiddenActions = hiddenActionsRecord.base || [];
- return actionRecord.base.filter(a => {
- const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
- return overflow && !hiddenActions.includes(a.__key);
- }).map(action => {
- let key = action.__key;
- if (key === '/') { key = 'delete'; }
- return {
- name: action.label,
- id: `${key}-${action.__type}`,
- action,
- tooltip: action.title,
- };
- });
- }
-
- /**
- * Occasionally, a change created by a change action is not yet knwon to the
- * API for a brief time. Wait for the given change number to be recognized.
- *
- * Returns a promise that resolves with true if a request is recognized, or
- * false if the change was never recognized after all attempts.
- *
- * @param {number} changeNum
- * @return {Promise<boolean>}
- */
- _waitForChangeReachable(changeNum) {
- let attempsRemaining = AWAIT_CHANGE_ATTEMPTS;
- return new Promise(resolve => {
- const check = () => {
- attempsRemaining--;
- // Pass a no-op error handler to avoid the "not found" error toast.
- this.$.restAPI.getChange(changeNum, () => {}).then(response => {
- // If the response is 404, the response will be undefined.
- if (response) {
- resolve(true);
- return;
- }
-
- if (attempsRemaining) {
- this.async(check, AWAIT_CHANGE_TIMEOUT_MS);
- } else {
- resolve(false);
- }
- });
- };
- check();
- });
- }
-
- _handleEditTap() {
- this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false}));
- }
-
- _handleStopEditTap() {
- this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false}));
- }
-
- _computeHasTooltip(title) {
- return !!title;
- }
-
- _computeHasIcon(action) {
- return action.icon ? '' : 'hidden';
+ const index = this._getActionOverflowIndex(type, key);
+ const action = {
+ type,
+ key,
+ overflow,
+ };
+ if (!overflow && index !== -1) {
+ this.splice('_overflowActions', index, 1);
+ } else if (overflow) {
+ this.push('_overflowActions', action);
}
}
- customElements.define(GrChangeActions.is, GrChangeActions);
-})();
+ setActionPriority(type, key, priority) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type given: ${type}`);
+ }
+ const index = this._actionPriorityOverrides
+ .findIndex(action => action.type === type && action.key === key);
+ const action = {
+ type,
+ key,
+ priority,
+ };
+ if (index !== -1) {
+ this.set('_actionPriorityOverrides', index, action);
+ } else {
+ this.push('_actionPriorityOverrides', action);
+ }
+ }
+
+ setActionHidden(type, key, hidden) {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
+ throw Error(`Invalid action type given: ${type}`);
+ }
+
+ const idx = this._hiddenActions.indexOf(key);
+ if (hidden && idx === -1) {
+ this.push('_hiddenActions', key);
+ } else if (!hidden && idx !== -1) {
+ this.splice('_hiddenActions', idx, 1);
+ }
+ }
+
+ getActionDetails(action) {
+ if (this.revisionActions[action]) {
+ return this.revisionActions[action];
+ } else if (this.actions[action]) {
+ return this.actions[action];
+ }
+ }
+
+ _indexOfActionButtonWithKey(key) {
+ for (let i = 0; i < this._additionalActions.length; i++) {
+ if (this._additionalActions[i].__key === key) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ _getRevisionActions() {
+ return this.$.restAPI.getChangeRevisionActions(this.changeNum,
+ this.latestPatchNum);
+ }
+
+ _shouldHideActions(actions, loading) {
+ return loading || !actions || !actions.base || !actions.base.length;
+ }
+
+ _keyCount(changeRecord) {
+ return Object.keys((changeRecord && changeRecord.base) || {}).length;
+ }
+
+ _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord,
+ additionalActionsChangeRecord) {
+ // Polymer 2: check for undefined
+ if ([
+ actionsChangeRecord,
+ revisionActionsChangeRecord,
+ additionalActionsChangeRecord,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ const additionalActions = (additionalActionsChangeRecord &&
+ additionalActionsChangeRecord.base) || [];
+ this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
+ this._keyCount(revisionActionsChangeRecord) === 0 &&
+ additionalActions.length === 0;
+ this._actionLoadingMessage = '';
+ this._disabledMenuActions = [];
+
+ const revisionActions = revisionActionsChangeRecord.base || {};
+ if (Object.keys(revisionActions).length !== 0) {
+ if (!revisionActions.download) {
+ this.set('revisionActions.download', DOWNLOAD_ACTION);
+ }
+ }
+ }
+
+ /**
+ * @param {string=} actionName
+ */
+ _deleteAndNotify(actionName) {
+ if (this.actions && this.actions[actionName]) {
+ delete this.actions[actionName];
+ // We assign a fake value of 'false' to support Polymer 2
+ // see https://github.com/Polymer/polymer/issues/2631
+ this.notifyPath('actions.' + actionName, false);
+ }
+ }
+
+ _editStatusChanged(editMode, editPatchsetLoaded,
+ editBasedOnCurrentPatchSet, disableEdit) {
+ // Polymer 2: check for undefined
+ if ([
+ editMode,
+ editBasedOnCurrentPatchSet,
+ disableEdit,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (disableEdit) {
+ this._deleteAndNotify('publishEdit');
+ this._deleteAndNotify('rebaseEdit');
+ this._deleteAndNotify('deleteEdit');
+ this._deleteAndNotify('stopEdit');
+ this._deleteAndNotify('edit');
+ return;
+ }
+ if (this.actions && editPatchsetLoaded) {
+ // Only show actions that mutate an edit if an actual edit patch set
+ // is loaded.
+ if (this.changeIsOpen(this.change)) {
+ if (editBasedOnCurrentPatchSet) {
+ if (!this.actions.publishEdit) {
+ this.set('actions.publishEdit', PUBLISH_EDIT);
+ }
+ this._deleteAndNotify('rebaseEdit');
+ } else {
+ if (!this.actions.rebaseEdit) {
+ this.set('actions.rebaseEdit', REBASE_EDIT);
+ }
+ this._deleteAndNotify('publishEdit');
+ }
+ }
+ if (!this.actions.deleteEdit) {
+ this.set('actions.deleteEdit', DELETE_EDIT);
+ }
+ } else {
+ this._deleteAndNotify('publishEdit');
+ this._deleteAndNotify('rebaseEdit');
+ this._deleteAndNotify('deleteEdit');
+ }
+
+ if (this.actions && this.changeIsOpen(this.change)) {
+ // Only show edit button if there is no edit patchset loaded and the
+ // file list is not in edit mode.
+ if (editPatchsetLoaded || editMode) {
+ this._deleteAndNotify('edit');
+ } else {
+ if (!this.actions.edit) { this.set('actions.edit', EDIT); }
+ }
+ // Only show STOP_EDIT if edit mode is enabled, but no edit patch set
+ // is loaded.
+ if (editMode && !editPatchsetLoaded) {
+ if (!this.actions.stopEdit) {
+ this.set('actions.stopEdit', STOP_EDIT);
+ }
+ } else {
+ this._deleteAndNotify('stopEdit');
+ }
+ } else {
+ // Remove edit button.
+ this._deleteAndNotify('edit');
+ }
+ }
+
+ _getValuesFor(obj) {
+ return Object.keys(obj).map(key => obj[key]);
+ }
+
+ _getLabelStatus(label) {
+ if (label.approved) {
+ return LabelStatus.OK;
+ } else if (label.rejected) {
+ return LabelStatus.REJECT;
+ } else if (label.optional) {
+ return LabelStatus.OPTIONAL;
+ } else {
+ return LabelStatus.NEED;
+ }
+ }
+
+ /**
+ * Get highest score for last missing permitted label for current change.
+ * Returns null if no labels permitted or more than one label missing.
+ *
+ * @return {{label: string, score: string}|null}
+ */
+ _getTopMissingApproval() {
+ if (!this.change ||
+ !this.change.labels ||
+ !this.change.permitted_labels) {
+ return null;
+ }
+ let result;
+ for (const label in this.change.labels) {
+ if (!(label in this.change.permitted_labels)) {
+ continue;
+ }
+ if (this.change.permitted_labels[label].length === 0) {
+ continue;
+ }
+ const status = this._getLabelStatus(this.change.labels[label]);
+ if (status === LabelStatus.NEED) {
+ if (result) {
+ // More than one label is missing, so it's unclear which to quick
+ // approve, return null;
+ return null;
+ }
+ result = label;
+ } else if (status === LabelStatus.REJECT ||
+ status === LabelStatus.IMPOSSIBLE) {
+ return null;
+ }
+ }
+ if (result) {
+ const score = this.change.permitted_labels[result].slice(-1)[0];
+ const maxScore =
+ Object.keys(this.change.labels[result].values).slice(-1)[0];
+ if (score === maxScore) {
+ // Allow quick approve only for maximal score.
+ return {
+ label: result,
+ score,
+ };
+ }
+ }
+ return null;
+ }
+
+ hideQuickApproveAction() {
+ this._topLevelSecondaryActions =
+ this._topLevelSecondaryActions
+ .filter(sa => sa.key !== QUICK_APPROVE_ACTION.key);
+ this._hideQuickApproveAction = true;
+ }
+
+ _getQuickApproveAction() {
+ if (this._hideQuickApproveAction) {
+ return null;
+ }
+ const approval = this._getTopMissingApproval();
+ if (!approval) {
+ return null;
+ }
+ const action = Object.assign({}, QUICK_APPROVE_ACTION);
+ action.label = approval.label + approval.score;
+ const review = {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {},
+ };
+ review.labels[approval.label] = approval.score;
+ action.payload = review;
+ return action;
+ }
+
+ _getActionValues(actionsChangeRecord, primariesChangeRecord,
+ additionalActionsChangeRecord, type) {
+ if (!actionsChangeRecord || !primariesChangeRecord) { return []; }
+
+ const actions = actionsChangeRecord.base || {};
+ const primaryActionKeys = primariesChangeRecord.base || [];
+ const result = [];
+ const values = this._getValuesFor(
+ type === ActionType.CHANGE ? ChangeActions : RevisionActions);
+ const pluginActions = [];
+ Object.keys(actions).forEach(a => {
+ actions[a].__key = a;
+ actions[a].__type = type;
+ actions[a].__primary = primaryActionKeys.includes(a);
+ // Plugin actions always contain ~ in the key.
+ if (a.indexOf('~') !== -1) {
+ this._populateActionUrl(actions[a]);
+ pluginActions.push(actions[a]);
+ // Add server-side provided plugin actions to overflow menu.
+ this._overflowActions.push({
+ type,
+ key: a,
+ });
+ return;
+ } else if (!values.includes(a)) {
+ return;
+ }
+ actions[a].label = this._getActionLabel(actions[a]);
+
+ // Triggers a re-render by ensuring object inequality.
+ result.push(Object.assign({}, actions[a]));
+ });
+
+ let additionalActions = (additionalActionsChangeRecord &&
+ additionalActionsChangeRecord.base) || [];
+ additionalActions = additionalActions
+ .filter(a => a.__type === type)
+ .map(a => {
+ a.__primary = primaryActionKeys.includes(a.__key);
+ // Triggers a re-render by ensuring object inequality.
+ return Object.assign({}, a);
+ });
+ return result.concat(additionalActions).concat(pluginActions);
+ }
+
+ _populateActionUrl(action) {
+ const patchNum =
+ action.__type === ActionType.REVISION ? this.latestPatchNum : null;
+ this.$.restAPI.getChangeActionURL(
+ this.changeNum, patchNum, '/' + action.__key)
+ .then(url => action.__url = url);
+ }
+
+ /**
+ * Given a change action, return a display label that uses the appropriate
+ * casing or includes explanatory details.
+ */
+ _getActionLabel(action) {
+ if (action.label === 'Delete') {
+ // This label is common within change and revision actions. Make it more
+ // explicit to the user.
+ return 'Delete change';
+ } else if (action.label === 'WIP') {
+ return 'Mark as work in progress';
+ }
+ // Otherwise, just map the name to sentence case.
+ return this._toSentenceCase(action.label);
+ }
+
+ /**
+ * Capitalize the first letter and lowecase all others.
+ *
+ * @param {string} s
+ * @return {string}
+ */
+ _toSentenceCase(s) {
+ if (!s.length) { return ''; }
+ return s[0].toUpperCase() + s.slice(1).toLowerCase();
+ }
+
+ _computeLoadingLabel(action) {
+ return ActionLoadingLabels[action] || 'Working...';
+ }
+
+ _canSubmitChange() {
+ return this.$.jsAPI.canSubmitChange(this.change,
+ this._getRevision(this.change, this.latestPatchNum));
+ }
+
+ _getRevision(change, patchNum) {
+ for (const rev of Object.values(change.revisions)) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ return rev;
+ }
+ }
+ return null;
+ }
+
+ showRevertDialog() {
+ const query = 'submissionid:' + this.change.submission_id;
+ /* A chromium plugin expects that the modifyRevertMsg hook will only
+ be called after the revert button is pressed, hence we populate the
+ revert dialog after revert button is pressed. */
+ this.$.restAPI.getChanges('', query)
+ .then(changes => {
+ this.$.confirmRevertDialog.populate(this.change,
+ this.commitMessage, changes);
+ this._showActionDialog(this.$.confirmRevertDialog);
+ });
+ }
+
+ showRevertSubmissionDialog() {
+ const query = 'submissionid:' + this.change.submission_id;
+ this.$.restAPI.getChanges('', query)
+ .then(changes => {
+ this.$.confirmRevertSubmissionDialog.
+ _populateRevertSubmissionMessage(
+ this.commitMessage, this.change, changes);
+ this._showActionDialog(this.$.confirmRevertSubmissionDialog);
+ });
+ }
+
+ _handleActionTap(e) {
+ e.preventDefault();
+ let el = dom(e).localTarget;
+ while (el.tagName.toLowerCase() !== 'gr-button') {
+ if (!el.parentElement) { return; }
+ el = el.parentElement;
+ }
+
+ const key = el.getAttribute('data-action-key');
+ if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
+ key.indexOf('~') !== -1) {
+ this.fire(`${key}-tap`, {node: el});
+ return;
+ }
+ const type = el.getAttribute('data-action-type');
+ this._handleAction(type, key);
+ }
+
+ _handleOveflowItemTap(e) {
+ e.preventDefault();
+ const el = dom(e).localTarget;
+ const key = e.detail.action.__key;
+ if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) ||
+ key.indexOf('~') !== -1) {
+ this.fire(`${key}-tap`, {node: el});
+ return;
+ }
+ this._handleAction(e.detail.action.__type, e.detail.action.__key);
+ }
+
+ _handleAction(type, key) {
+ this.$.reporting.reportInteraction(`${type}-${key}`);
+ switch (type) {
+ case ActionType.REVISION:
+ this._handleRevisionAction(key);
+ break;
+ case ActionType.CHANGE:
+ this._handleChangeAction(key);
+ break;
+ default:
+ this._fireAction(this._prependSlash(key), this.actions[key], false);
+ }
+ }
+
+ _handleChangeAction(key) {
+ let action;
+ switch (key) {
+ case ChangeActions.REVERT:
+ this.showRevertDialog();
+ break;
+ case ChangeActions.REVERT_SUBMISSION:
+ this.showRevertSubmissionDialog();
+ break;
+ case ChangeActions.ABANDON:
+ this._showActionDialog(this.$.confirmAbandonDialog);
+ break;
+ case QUICK_APPROVE_ACTION.key:
+ action = this._allActionValues.find(o => o.key === key);
+ this._fireAction(
+ this._prependSlash(key), action, true, action.payload);
+ break;
+ case ChangeActions.EDIT:
+ this._handleEditTap();
+ break;
+ case ChangeActions.STOP_EDIT:
+ this._handleStopEditTap();
+ break;
+ case ChangeActions.DELETE:
+ this._handleDeleteTap();
+ break;
+ case ChangeActions.DELETE_EDIT:
+ this._handleDeleteEditTap();
+ break;
+ case ChangeActions.FOLLOW_UP:
+ this._handleFollowUpTap();
+ break;
+ case ChangeActions.WIP:
+ this._handleWipTap();
+ break;
+ case ChangeActions.MOVE:
+ this._handleMoveTap();
+ break;
+ case ChangeActions.PUBLISH_EDIT:
+ this._handlePublishEditTap();
+ break;
+ case ChangeActions.REBASE_EDIT:
+ this._handleRebaseEditTap();
+ break;
+ default:
+ this._fireAction(this._prependSlash(key), this.actions[key], false);
+ }
+ }
+
+ _handleRevisionAction(key) {
+ switch (key) {
+ case RevisionActions.REBASE:
+ this._showActionDialog(this.$.confirmRebase);
+ this.$.confirmRebase.fetchRecentChanges();
+ break;
+ case RevisionActions.CHERRYPICK:
+ this._handleCherrypickTap();
+ break;
+ case RevisionActions.DOWNLOAD:
+ this._handleDownloadTap();
+ break;
+ case RevisionActions.SUBMIT:
+ if (!this._canSubmitChange()) { return; }
+ this._showActionDialog(this.$.confirmSubmitDialog);
+ break;
+ default:
+ this._fireAction(this._prependSlash(key),
+ this.revisionActions[key], true);
+ }
+ }
+
+ _prependSlash(key) {
+ return key === '/' ? key : `/${key}`;
+ }
+
+ /**
+ * _hasKnownChainState set to true true if hasParent is defined (can be
+ * either true or false). set to false otherwise.
+ */
+ _computeChainState(hasParent) {
+ this._hasKnownChainState = true;
+ }
+
+ _calculateDisabled(action, hasKnownChainState) {
+ if (action.__key === 'rebase' && hasKnownChainState === false) {
+ return true;
+ }
+ return !action.enabled;
+ }
+
+ _handleConfirmDialogCancel() {
+ this._hideAllDialogs();
+ }
+
+ _hideAllDialogs() {
+ const dialogEls =
+ dom(this.root).querySelectorAll('.confirmDialog');
+ for (const dialogEl of dialogEls) { dialogEl.hidden = true; }
+ this.$.overlay.close();
+ }
+
+ _handleRebaseConfirm(e) {
+ const el = this.$.confirmRebase;
+ const payload = {base: e.detail.base};
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction('/rebase', this.revisionActions.rebase, true, payload);
+ }
+
+ _handleCherrypickConfirm() {
+ this._handleCherryPickRestApi(false);
+ }
+
+ _handleCherrypickConflictConfirm() {
+ this._handleCherryPickRestApi(true);
+ }
+
+ _handleCherryPickRestApi(conflicts) {
+ const el = this.$.confirmCherrypick;
+ if (!el.branch) {
+ this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
+ return;
+ }
+ if (!el.message) {
+ this.fire('show-alert', {message: ERR_COMMIT_EMPTY});
+ return;
+ }
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction(
+ '/cherrypick',
+ this.revisionActions.cherrypick,
+ true,
+ {
+ destination: el.branch,
+ base: el.baseCommit ? el.baseCommit : null,
+ message: el.message,
+ allow_conflicts: conflicts,
+ }
+ );
+ }
+
+ _handleMoveConfirm() {
+ const el = this.$.confirmMove;
+ if (!el.branch) {
+ this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
+ return;
+ }
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction(
+ '/move',
+ this.actions.move,
+ false,
+ {
+ destination_branch: el.branch,
+ message: el.message,
+ }
+ );
+ }
+
+ _handleRevertDialogConfirm(e) {
+ const revertType = e.detail.revertType;
+ const message = e.detail.message;
+ const el = this.$.confirmRevertDialog;
+ this.$.overlay.close();
+ el.hidden = true;
+ switch (revertType) {
+ case REVERT_TYPES.REVERT_SINGLE_CHANGE:
+ this._fireAction('/revert', this.actions.revert, false,
+ {message});
+ break;
+ case REVERT_TYPES.REVERT_SUBMISSION:
+ this._fireAction('/revert_submission', this.actions.revert_submission,
+ false, {message});
+ break;
+ default:
+ console.error('invalid revert type');
+ }
+ }
+
+ _handleRevertSubmissionDialogConfirm() {
+ const el = this.$.confirmRevertSubmissionDialog;
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction('/revert_submission', this.actions.revert_submission,
+ false, {message: el.message});
+ }
+
+ _handleAbandonDialogConfirm() {
+ const el = this.$.confirmAbandonDialog;
+ this.$.overlay.close();
+ el.hidden = true;
+ this._fireAction('/abandon', this.actions.abandon, false,
+ {message: el.message});
+ }
+
+ _handleCreateFollowUpChange() {
+ this.$.createFollowUpChange.handleCreateChange();
+ this._handleCloseCreateFollowUpChange();
+ }
+
+ _handleCloseCreateFollowUpChange() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteConfirm() {
+ this._fireAction('/', this.actions[ChangeActions.DELETE], false);
+ }
+
+ _handleDeleteEditConfirm() {
+ this._hideAllDialogs();
+
+ this._fireAction('/edit', this.actions.deleteEdit, false);
+ }
+
+ _handleSubmitConfirm() {
+ if (!this._canSubmitChange()) { return; }
+ this._hideAllDialogs();
+ this._fireAction('/submit', this.revisionActions.submit, true);
+ }
+
+ _getActionOverflowIndex(type, key) {
+ return this._overflowActions
+ .findIndex(action => action.type === type && action.key === key);
+ }
+
+ _setLoadingOnButtonWithKey(type, key) {
+ this._actionLoadingMessage = this._computeLoadingLabel(key);
+ let buttonKey = key;
+ // TODO(dhruvsri): clean this up later
+ // If key is revert-submission, then button key should be 'revert'
+ if (buttonKey === ChangeActions.REVERT_SUBMISSION) {
+ // Revert submission button no longer exists
+ buttonKey = ChangeActions.REVERT;
+ }
+
+ // If the action appears in the overflow menu.
+ if (this._getActionOverflowIndex(type, buttonKey) !== -1) {
+ this.push('_disabledMenuActions', buttonKey === '/' ? 'delete' :
+ buttonKey);
+ return function() {
+ this._actionLoadingMessage = '';
+ this._disabledMenuActions = [];
+ }.bind(this);
+ }
+
+ // Otherwise it's a top-level action.
+ const buttonEl = this.shadowRoot
+ .querySelector(`[data-action-key="${buttonKey}"]`);
+ buttonEl.setAttribute('loading', true);
+ buttonEl.disabled = true;
+ return function() {
+ this._actionLoadingMessage = '';
+ buttonEl.removeAttribute('loading');
+ buttonEl.disabled = false;
+ }.bind(this);
+ }
+
+ /**
+ * @param {string} endpoint
+ * @param {!Object|undefined} action
+ * @param {boolean} revAction
+ * @param {!Object|string=} opt_payload
+ */
+ _fireAction(endpoint, action, revAction, opt_payload) {
+ const cleanupFn =
+ this._setLoadingOnButtonWithKey(action.__type, action.__key);
+
+ this._send(action.method, opt_payload, endpoint, revAction, cleanupFn,
+ action).then(this._handleResponse.bind(this, action));
+ }
+
+ _showActionDialog(dialog) {
+ this._hideAllDialogs();
+
+ dialog.hidden = false;
+ this.$.overlay.open().then(() => {
+ if (dialog.resetFocus) {
+ dialog.resetFocus();
+ }
+ });
+ }
+
+ // TODO(rmistry): Redo this after
+ // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
+ _setLabelValuesOnRevert(newChangeId) {
+ const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
+ if (!labels) { return Promise.resolve(); }
+ return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels});
+ }
+
+ _handleResponse(action, response) {
+ if (!response) { return; }
+ return this.$.restAPI.getResponseObject(response).then(obj => {
+ switch (action.__key) {
+ case ChangeActions.REVERT:
+ this._waitForChangeReachable(obj._number)
+ .then(() => this._setLabelValuesOnRevert(obj._number))
+ .then(() => {
+ Gerrit.Nav.navigateToChange(obj);
+ });
+ break;
+ case RevisionActions.CHERRYPICK:
+ this._waitForChangeReachable(obj._number).then(() => {
+ Gerrit.Nav.navigateToChange(obj);
+ });
+ break;
+ case ChangeActions.DELETE:
+ if (action.__type === ActionType.CHANGE) {
+ Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot());
+ }
+ break;
+ case ChangeActions.WIP:
+ case ChangeActions.DELETE_EDIT:
+ case ChangeActions.PUBLISH_EDIT:
+ case ChangeActions.REBASE_EDIT:
+ Gerrit.Nav.navigateToChange(this.change);
+ break;
+ case ChangeActions.REVERT_SUBMISSION:
+ if (!obj.revert_changes || !obj.revert_changes.length) return;
+ /* If there is only 1 change then gerrit will automatically
+ redirect to that change */
+ Gerrit.Nav.navigateToSearchQuery('topic: ' +
+ obj.revert_changes[0].topic);
+ break;
+ default:
+ this.dispatchEvent(new CustomEvent('reload-change',
+ {detail: {action: action.__key}, bubbles: false}));
+ break;
+ }
+ });
+ }
+
+ _handleShowRevertSubmissionChangesConfirm() {
+ this._hideAllDialogs();
+ }
+
+ _handleResponseError(action, response, body) {
+ if (action && action.__key === RevisionActions.CHERRYPICK) {
+ if (response && response.status === 409 &&
+ body && !body.allow_conflicts) {
+ return this._showActionDialog(
+ this.$.confirmCherrypickConflict);
+ }
+ }
+ return response.text().then(errText => {
+ this.fire('show-error',
+ {message: `Could not perform action: ${errText}`});
+ if (!errText.startsWith('Change is already up to date')) {
+ throw Error(errText);
+ }
+ });
+ }
+
+ /**
+ * @param {string} method
+ * @param {string|!Object|undefined} payload
+ * @param {string} actionEndpoint
+ * @param {boolean} revisionAction
+ * @param {?Function} cleanupFn
+ * @param {!Object|undefined} action
+ */
+ _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) {
+ const handleError = response => {
+ cleanupFn.call(this);
+ this._handleResponseError(action, response, payload);
+ };
+
+ return this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ if (!result.isLatest) {
+ this.fire('show-alert', {
+ message: 'Cannot set label: a newer patch has been ' +
+ 'uploaded to this change.',
+ action: 'Reload',
+ callback: () => {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this.change);
+ },
+ });
+
+ // Because this is not a network error, call the cleanup function
+ // but not the error handler.
+ cleanupFn();
+
+ return Promise.resolve();
+ }
+ const patchNum = revisionAction ? this.latestPatchNum : null;
+ return this.$.restAPI.executeChangeAction(this.changeNum, method,
+ actionEndpoint, patchNum, payload, handleError)
+ .then(response => {
+ cleanupFn.call(this);
+ return response;
+ });
+ });
+ }
+
+ _handleAbandonTap() {
+ this._showActionDialog(this.$.confirmAbandonDialog);
+ }
+
+ _handleCherrypickTap() {
+ this.$.confirmCherrypick.branch = '';
+ this._showActionDialog(this.$.confirmCherrypick);
+ }
+
+ _handleMoveTap() {
+ this.$.confirmMove.branch = '';
+ this.$.confirmMove.message = '';
+ this._showActionDialog(this.$.confirmMove);
+ }
+
+ _handleDownloadTap() {
+ this.fire('download-tap', null, {bubbles: false});
+ }
+
+ _handleDeleteTap() {
+ this._showActionDialog(this.$.confirmDeleteDialog);
+ }
+
+ _handleDeleteEditTap() {
+ this._showActionDialog(this.$.confirmDeleteEditDialog);
+ }
+
+ _handleFollowUpTap() {
+ this._showActionDialog(this.$.createFollowUpDialog);
+ }
+
+ _handleWipTap() {
+ this._fireAction('/wip', this.actions.wip, false);
+ }
+
+ _handlePublishEditTap() {
+ this._fireAction('/edit:publish', this.actions.publishEdit, false);
+ }
+
+ _handleRebaseEditTap() {
+ this._fireAction('/edit:rebase', this.actions.rebaseEdit, false);
+ }
+
+ _handleHideBackgroundContent() {
+ this.$.mainContent.classList.add('overlayOpen');
+ }
+
+ _handleShowBackgroundContent() {
+ this.$.mainContent.classList.remove('overlayOpen');
+ }
+
+ /**
+ * Merge sources of change actions into a single ordered array of action
+ * values.
+ *
+ * @param {!Array} changeActionsRecord
+ * @param {!Array} revisionActionsRecord
+ * @param {!Array} primariesRecord
+ * @param {!Array} additionalActionsRecord
+ * @param {!Object} change The change object.
+ * @return {!Array}
+ */
+ _computeAllActions(changeActionsRecord, revisionActionsRecord,
+ primariesRecord, additionalActionsRecord, change) {
+ // Polymer 2: check for undefined
+ if ([
+ changeActionsRecord,
+ revisionActionsRecord,
+ primariesRecord,
+ additionalActionsRecord,
+ change,
+ ].some(arg => arg === undefined)) {
+ return [];
+ }
+
+ const revisionActionValues = this._getActionValues(revisionActionsRecord,
+ primariesRecord, additionalActionsRecord, ActionType.REVISION);
+ const changeActionValues = this._getActionValues(changeActionsRecord,
+ primariesRecord, additionalActionsRecord, ActionType.CHANGE);
+ const quickApprove = this._getQuickApproveAction();
+ if (quickApprove) {
+ changeActionValues.unshift(quickApprove);
+ }
+
+ return revisionActionValues
+ .concat(changeActionValues)
+ .sort(this._actionComparator.bind(this))
+ .map(action => {
+ if (ACTIONS_WITH_ICONS.has(action.__key)) {
+ action.icon = action.__key;
+ }
+ return action;
+ })
+ .filter(action => !this._shouldSkipAction(action));
+ }
+
+ _getActionPriority(action) {
+ if (action.__type && action.__key) {
+ const overrideAction = this._actionPriorityOverrides
+ .find(i => i.type === action.__type && i.key === action.__key);
+
+ if (overrideAction !== undefined) {
+ return overrideAction.priority;
+ }
+ }
+ if (action.__key === 'review') {
+ return ActionPriority.REVIEW;
+ } else if (action.__primary) {
+ return ActionPriority.PRIMARY;
+ } else if (action.__type === ActionType.CHANGE) {
+ return ActionPriority.CHANGE;
+ } else if (action.__type === ActionType.REVISION) {
+ return ActionPriority.REVISION;
+ }
+ return ActionPriority.DEFAULT;
+ }
+
+ /**
+ * Sort comparator to define the order of change actions.
+ */
+ _actionComparator(actionA, actionB) {
+ const priorityDelta = this._getActionPriority(actionA) -
+ this._getActionPriority(actionB);
+ // Sort by the button label if same priority.
+ if (priorityDelta === 0) {
+ return actionA.label > actionB.label ? 1 : -1;
+ } else {
+ return priorityDelta;
+ }
+ }
+
+ _shouldSkipAction(action) {
+ return SKIP_ACTION_KEYS.includes(action.__key);
+ }
+
+ _computeTopLevelActions(actionRecord, hiddenActionsRecord) {
+ const hiddenActions = hiddenActionsRecord.base || [];
+ return actionRecord.base.filter(a => {
+ const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
+ return !(overflow || hiddenActions.includes(a.__key));
+ });
+ }
+
+ _filterPrimaryActions(_topLevelActions) {
+ this._topLevelPrimaryActions = _topLevelActions.filter(action =>
+ action.__primary);
+ this._topLevelSecondaryActions = _topLevelActions.filter(action =>
+ !action.__primary);
+ }
+
+ _computeMenuActions(actionRecord, hiddenActionsRecord) {
+ const hiddenActions = hiddenActionsRecord.base || [];
+ return actionRecord.base.filter(a => {
+ const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1;
+ return overflow && !hiddenActions.includes(a.__key);
+ }).map(action => {
+ let key = action.__key;
+ if (key === '/') { key = 'delete'; }
+ return {
+ name: action.label,
+ id: `${key}-${action.__type}`,
+ action,
+ tooltip: action.title,
+ };
+ });
+ }
+
+ /**
+ * Occasionally, a change created by a change action is not yet knwon to the
+ * API for a brief time. Wait for the given change number to be recognized.
+ *
+ * Returns a promise that resolves with true if a request is recognized, or
+ * false if the change was never recognized after all attempts.
+ *
+ * @param {number} changeNum
+ * @return {Promise<boolean>}
+ */
+ _waitForChangeReachable(changeNum) {
+ let attempsRemaining = AWAIT_CHANGE_ATTEMPTS;
+ return new Promise(resolve => {
+ const check = () => {
+ attempsRemaining--;
+ // Pass a no-op error handler to avoid the "not found" error toast.
+ this.$.restAPI.getChange(changeNum, () => {}).then(response => {
+ // If the response is 404, the response will be undefined.
+ if (response) {
+ resolve(true);
+ return;
+ }
+
+ if (attempsRemaining) {
+ this.async(check, AWAIT_CHANGE_TIMEOUT_MS);
+ } else {
+ resolve(false);
+ }
+ });
+ };
+ check();
+ });
+ }
+
+ _handleEditTap() {
+ this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false}));
+ }
+
+ _handleStopEditTap() {
+ this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false}));
+ }
+
+ _computeHasTooltip(title) {
+ return !!title;
+ }
+
+ _computeHasIcon(action) {
+ return action.icon ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrChangeActions.is, GrChangeActions);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
index 097b92c..7aa5ecd 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
@@ -1,47 +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/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../admin/gr-create-change-dialog/gr-create-change-dialog.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.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-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html">
-<link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
-<link rel="import" href="../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html">
-<link rel="import" href="../gr-confirm-move-dialog/gr-confirm-move-dialog.html">
-<link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
-<link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html">
-<link rel="import" href="../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html">
-<link rel="import" href="../gr-confirm-submit-dialog/gr-confirm-submit-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-actions">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: flex;
@@ -101,144 +76,48 @@
}
</style>
<div id="mainContent">
- <span
- id="actionLoadingMessage"
- hidden$="[[!_actionLoadingMessage]]">
+ <span id="actionLoadingMessage" hidden\$="[[!_actionLoadingMessage]]">
[[_actionLoadingMessage]]</span>
- <section id="primaryActions"
- hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
- <template
- is="dom-repeat"
- items="[[_topLevelPrimaryActions]]"
- as="action">
- <gr-button
- link
- title$="[[action.title]]"
- has-tooltip="[[_computeHasTooltip(action.title)]]"
- position-below="true"
- data-action-key$="[[action.__key]]"
- data-action-type$="[[action.__type]]"
- data-label$="[[action.label]]"
- disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
- on-click="_handleActionTap">
- <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
+ <section id="primaryActions" hidden\$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
+ <template is="dom-repeat" items="[[_topLevelPrimaryActions]]" as="action">
+ <gr-button link="" title\$="[[action.title]]" has-tooltip="[[_computeHasTooltip(action.title)]]" position-below="true" data-action-key\$="[[action.__key]]" data-action-type\$="[[action.__type]]" data-label\$="[[action.label]]" disabled\$="[[_calculateDisabled(action, _hasKnownChainState)]]" on-click="_handleActionTap">
+ <iron-icon class\$="[[_computeHasIcon(action)]]" icon\$="gr-icons:[[action.icon]]"></iron-icon>
[[action.label]]
</gr-button>
</template>
</section>
- <section id="secondaryActions"
- hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
- <template
- is="dom-repeat"
- items="[[_topLevelSecondaryActions]]"
- as="action">
- <gr-button
- link
- title$="[[action.title]]"
- has-tooltip="[[_computeHasTooltip(action.title)]]"
- position-below="true"
- data-action-key$="[[action.__key]]"
- data-action-type$="[[action.__type]]"
- data-label$="[[action.label]]"
- disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
- on-click="_handleActionTap">
- <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon>
+ <section id="secondaryActions" hidden\$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
+ <template is="dom-repeat" items="[[_topLevelSecondaryActions]]" as="action">
+ <gr-button link="" title\$="[[action.title]]" has-tooltip="[[_computeHasTooltip(action.title)]]" position-below="true" data-action-key\$="[[action.__key]]" data-action-type\$="[[action.__type]]" data-label\$="[[action.label]]" disabled\$="[[_calculateDisabled(action, _hasKnownChainState)]]" on-click="_handleActionTap">
+ <iron-icon class\$="[[_computeHasIcon(action)]]" icon\$="gr-icons:[[action.icon]]"></iron-icon>
[[action.label]]
</gr-button>
</template>
</section>
- <gr-button hidden$="[[!_loading]]" disabled>Loading actions...</gr-button>
- <gr-dropdown
- id="moreActions"
- link
- tabindex="0"
- vertical-offset="32"
- horizontal-align="right"
- on-tap-item="_handleOveflowItemTap"
- hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
- disabled-ids="[[_disabledMenuActions]]"
- items="[[_menuActions]]">
+ <gr-button hidden\$="[[!_loading]]" disabled="">Loading actions...</gr-button>
+ <gr-dropdown id="moreActions" link="" tabindex="0" vertical-offset="32" horizontal-align="right" on-tap-item="_handleOveflowItemTap" hidden\$="[[_shouldHideActions(_menuActions.*, _loading)]]" disabled-ids="[[_disabledMenuActions]]" items="[[_menuActions]]">
<iron-icon icon="gr-icons:more-vert"></iron-icon>
<span id="moreMessage">More</span>
</gr-dropdown>
</div>
- <gr-overlay id="overlay" with-backdrop>
- <gr-confirm-rebase-dialog id="confirmRebase"
- class="confirmDialog"
- change-number="[[change._number]]"
- on-confirm="_handleRebaseConfirm"
- on-cancel="_handleConfirmDialogCancel"
- branch="[[change.branch]]"
- has-parent="[[hasParent]]"
- rebase-on-current="[[_revisionRebaseAction.rebaseOnCurrent]]"
- hidden></gr-confirm-rebase-dialog>
- <gr-confirm-cherrypick-dialog id="confirmCherrypick"
- class="confirmDialog"
- change-status="[[changeStatus]]"
- commit-message="[[commitMessage]]"
- commit-num="[[commitNum]]"
- on-confirm="_handleCherrypickConfirm"
- on-cancel="_handleConfirmDialogCancel"
- project="[[change.project]]"
- hidden></gr-confirm-cherrypick-dialog>
- <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict"
- class="confirmDialog"
- on-confirm="_handleCherrypickConflictConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-cherrypick-conflict-dialog>
- <gr-confirm-move-dialog id="confirmMove"
- class="confirmDialog"
- on-confirm="_handleMoveConfirm"
- on-cancel="_handleConfirmDialogCancel"
- project="[[change.project]]"
- hidden></gr-confirm-move-dialog>
- <gr-confirm-revert-dialog id="confirmRevertDialog"
- class="confirmDialog"
- on-confirm="_handleRevertDialogConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-revert-dialog>
- <gr-confirm-revert-submission-dialog id="confirmRevertSubmissionDialog"
- class="confirmDialog"
- commit-message="[[commitMessage]]"
- on-confirm="_handleRevertSubmissionDialogConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-revert-submission-dialog>
- <gr-confirm-abandon-dialog id="confirmAbandonDialog"
- class="confirmDialog"
- on-confirm="_handleAbandonDialogConfirm"
- on-cancel="_handleConfirmDialogCancel"
- hidden></gr-confirm-abandon-dialog>
- <gr-confirm-submit-dialog
- id="confirmSubmitDialog"
- class="confirmDialog"
- change="[[change]]"
- action="[[_revisionSubmitAction]]"
- on-cancel="_handleConfirmDialogCancel"
- on-confirm="_handleSubmitConfirm" hidden></gr-confirm-submit-dialog>
- <gr-dialog id="createFollowUpDialog"
- class="confirmDialog"
- confirm-label="Create"
- on-confirm="_handleCreateFollowUpChange"
- on-cancel="_handleCloseCreateFollowUpChange">
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-confirm-rebase-dialog id="confirmRebase" class="confirmDialog" change-number="[[change._number]]" on-confirm="_handleRebaseConfirm" on-cancel="_handleConfirmDialogCancel" branch="[[change.branch]]" has-parent="[[hasParent]]" rebase-on-current="[[_revisionRebaseAction.rebaseOnCurrent]]" hidden=""></gr-confirm-rebase-dialog>
+ <gr-confirm-cherrypick-dialog id="confirmCherrypick" class="confirmDialog" change-status="[[changeStatus]]" commit-message="[[commitMessage]]" commit-num="[[commitNum]]" on-confirm="_handleCherrypickConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-cherrypick-dialog>
+ <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict" class="confirmDialog" on-confirm="_handleCherrypickConflictConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-cherrypick-conflict-dialog>
+ <gr-confirm-move-dialog id="confirmMove" class="confirmDialog" on-confirm="_handleMoveConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-move-dialog>
+ <gr-confirm-revert-dialog id="confirmRevertDialog" class="confirmDialog" on-confirm="_handleRevertDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-revert-dialog>
+ <gr-confirm-revert-submission-dialog id="confirmRevertSubmissionDialog" class="confirmDialog" commit-message="[[commitMessage]]" on-confirm="_handleRevertSubmissionDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-revert-submission-dialog>
+ <gr-confirm-abandon-dialog id="confirmAbandonDialog" class="confirmDialog" on-confirm="_handleAbandonDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-abandon-dialog>
+ <gr-confirm-submit-dialog id="confirmSubmitDialog" class="confirmDialog" change="[[change]]" action="[[_revisionSubmitAction]]" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleSubmitConfirm" hidden=""></gr-confirm-submit-dialog>
+ <gr-dialog id="createFollowUpDialog" class="confirmDialog" confirm-label="Create" on-confirm="_handleCreateFollowUpChange" on-cancel="_handleCloseCreateFollowUpChange">
<div class="header" slot="header">
Create Follow-Up Change
</div>
<div class="main" slot="main">
- <gr-create-change-dialog
- id="createFollowUpChange"
- branch="[[change.branch]]"
- base-change="[[change.id]]"
- repo-name="[[change.project]]"
- private-by-default="[[privateByDefault]]"></gr-create-change-dialog>
+ <gr-create-change-dialog id="createFollowUpChange" branch="[[change.branch]]" base-change="[[change.id]]" repo-name="[[change.project]]" private-by-default="[[privateByDefault]]"></gr-create-change-dialog>
</div>
</gr-dialog>
- <gr-dialog
- id="confirmDeleteDialog"
- class="confirmDialog"
- confirm-label="Delete"
- confirm-on-enter
- on-cancel="_handleConfirmDialogCancel"
- on-confirm="_handleDeleteConfirm">
+ <gr-dialog id="confirmDeleteDialog" class="confirmDialog" confirm-label="Delete" confirm-on-enter="" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleDeleteConfirm">
<div class="header" slot="header">
Delete Change
</div>
@@ -246,13 +125,7 @@
Do you really want to delete the change?
</div>
</gr-dialog>
- <gr-dialog
- id="confirmDeleteEditDialog"
- class="confirmDialog"
- confirm-label="Delete"
- confirm-on-enter
- on-cancel="_handleConfirmDialogCancel"
- on-confirm="_handleDeleteEditConfirm">
+ <gr-dialog id="confirmDeleteEditDialog" class="confirmDialog" confirm-label="Delete" confirm-on-enter="" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleDeleteEditConfirm">
<div class="header" slot="header">
Delete Change Edit
</div>
@@ -264,6 +137,4 @@
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting" category="change-actions"></gr-reporting>
- </template>
- <script src="gr-change-actions.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index ee036cf..0d78fb4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-change-actions.html">
+<script type="module" src="./gr-change-actions.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-change-actions.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,885 +43,1704 @@
</template>
</test-fixture>
-<script>
- // TODO(dhruvsri): remove use of _populateRevertMessage as it's private
- suite('gr-change-actions tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-change-actions.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
+suite('gr-change-actions tests', () => {
+ let element;
+ let sandbox;
- suite('basic tests', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getChangeRevisionActions() {
+ suite('basic tests', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getChangeRevisionActions() {
+ return Promise.resolve({
+ cherrypick: {
+ method: 'POST',
+ label: 'Cherry Pick',
+ title: 'Cherry pick change to a different branch',
+ enabled: true,
+ },
+ rebase: {
+ method: 'POST',
+ label: 'Rebase',
+ title: 'Rebase onto tip of branch or parent change',
+ enabled: true,
+ },
+ submit: {
+ method: 'POST',
+ label: 'Submit',
+ title: 'Submit patch set 2 into master',
+ enabled: true,
+ },
+ revert_submission: {
+ method: 'POST',
+ label: 'Revert submission',
+ title: 'Revert this submission',
+ enabled: true,
+ },
+ });
+ },
+ send(method, url, payload) {
+ if (method !== 'POST') {
+ return Promise.reject(new Error('bad method'));
+ }
+
+ if (url === '/changes/test~42/revisions/2/submit') {
return Promise.resolve({
- cherrypick: {
- method: 'POST',
- label: 'Cherry Pick',
- title: 'Cherry pick change to a different branch',
- enabled: true,
- },
- rebase: {
- method: 'POST',
- label: 'Rebase',
- title: 'Rebase onto tip of branch or parent change',
- enabled: true,
- },
- submit: {
- method: 'POST',
- label: 'Submit',
- title: 'Submit patch set 2 into master',
- enabled: true,
- },
- revert_submission: {
- method: 'POST',
- label: 'Revert submission',
- title: 'Revert this submission',
- enabled: true,
- },
+ ok: true,
+ text() { return Promise.resolve(')]}\'\n{}'); },
});
- },
- send(method, url, payload) {
- if (method !== 'POST') {
- return Promise.reject(new Error('bad method'));
- }
+ } else if (url === '/changes/test~42/revisions/2/rebase') {
+ return Promise.resolve({
+ ok: true,
+ text() { return Promise.resolve(')]}\'\n{}'); },
+ });
+ }
- if (url === '/changes/test~42/revisions/2/submit') {
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(')]}\'\n{}'); },
- });
- } else if (url === '/changes/test~42/revisions/2/rebase') {
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(')]}\'\n{}'); },
- });
- }
-
- return Promise.reject(new Error('bad url'));
- },
- getProjectConfig() { return Promise.resolve({}); },
- });
-
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
-
- element = fixture('basic');
- element.change = {};
- element.changeNum = '42';
- element.latestPatchNum = '2';
- element.actions = {
- '/': {
- method: 'DELETE',
- label: 'Delete Change',
- title: 'Delete change X_X',
- enabled: true,
- },
- };
- sandbox.stub(element.$.confirmCherrypick.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
- sandbox.stub(element.$.confirmMove.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
-
- return element.reload();
+ return Promise.reject(new Error('bad url'));
+ },
+ getProjectConfig() { return Promise.resolve({}); },
});
- teardown(() => {
- sandbox.restore();
- });
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
- test('show-revision-actions event should fire', done => {
- const spy = sinon.spy(element, '_sendShowRevisionActions');
- element.reload();
- flush(() => {
- assert.isTrue(spy.called);
- done();
- });
- });
+ element = fixture('basic');
+ element.change = {};
+ element.changeNum = '42';
+ element.latestPatchNum = '2';
+ element.actions = {
+ '/': {
+ method: 'DELETE',
+ label: 'Delete Change',
+ title: 'Delete change X_X',
+ enabled: true,
+ },
+ };
+ sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
+ sandbox.stub(element.$.confirmMove.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
- test('primary and secondary actions split properly', () => {
- // Submit should be the only primary action.
- assert.equal(element._topLevelPrimaryActions.length, 1);
- assert.equal(element._topLevelPrimaryActions[0].label, 'Submit');
- assert.equal(element._topLevelSecondaryActions.length,
- element._topLevelActions.length - 1);
- });
+ return element.reload();
+ });
- test('revert submission action is skipped', () => {
- assert.isFalse(element._allActionValues.includes(action =>
- action.key === 'revert_submission'));
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('_shouldHideActions', () => {
- assert.isTrue(element._shouldHideActions(undefined, true));
- assert.isTrue(element._shouldHideActions({base: {}}, false));
- assert.isFalse(element._shouldHideActions({base: ['test']}, false));
+ test('show-revision-actions event should fire', done => {
+ const spy = sinon.spy(element, '_sendShowRevisionActions');
+ element.reload();
+ flush(() => {
+ assert.isTrue(spy.called);
+ done();
});
+ });
- test('plugin revision actions', done => {
- sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
- Promise.resolve('the-url'));
- element.revisionActions = {
- 'plugin~action': {},
- };
- assert.isOk(element.revisionActions['plugin~action']);
- flush(() => {
- assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
- element.changeNum, element.latestPatchNum, '/plugin~action'));
- assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
- done();
- });
+ test('primary and secondary actions split properly', () => {
+ // Submit should be the only primary action.
+ assert.equal(element._topLevelPrimaryActions.length, 1);
+ assert.equal(element._topLevelPrimaryActions[0].label, 'Submit');
+ assert.equal(element._topLevelSecondaryActions.length,
+ element._topLevelActions.length - 1);
+ });
+
+ test('revert submission action is skipped', () => {
+ assert.isFalse(element._allActionValues.includes(action =>
+ action.key === 'revert_submission'));
+ });
+
+ test('_shouldHideActions', () => {
+ assert.isTrue(element._shouldHideActions(undefined, true));
+ assert.isTrue(element._shouldHideActions({base: {}}, false));
+ assert.isFalse(element._shouldHideActions({base: ['test']}, false));
+ });
+
+ test('plugin revision actions', done => {
+ sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
+ Promise.resolve('the-url'));
+ element.revisionActions = {
+ 'plugin~action': {},
+ };
+ assert.isOk(element.revisionActions['plugin~action']);
+ flush(() => {
+ assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
+ element.changeNum, element.latestPatchNum, '/plugin~action'));
+ assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
+ done();
});
+ });
- test('plugin change actions', done => {
- sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
- Promise.resolve('the-url'));
- element.actions = {
- 'plugin~action': {},
- };
- assert.isOk(element.actions['plugin~action']);
- flush(() => {
- assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
- element.changeNum, null, '/plugin~action'));
- assert.equal(element.actions['plugin~action'].__url, 'the-url');
- done();
- });
+ test('plugin change actions', done => {
+ sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
+ Promise.resolve('the-url'));
+ element.actions = {
+ 'plugin~action': {},
+ };
+ assert.isOk(element.actions['plugin~action']);
+ flush(() => {
+ assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
+ element.changeNum, null, '/plugin~action'));
+ assert.equal(element.actions['plugin~action'].__url, 'the-url');
+ done();
});
+ });
- test('not supported actions are filtered out', () => {
- element.revisionActions = {followup: {}};
- assert.equal(element.querySelectorAll(
- 'section gr-button[data-action-type="revision"]').length, 0);
- });
+ test('not supported actions are filtered out', () => {
+ element.revisionActions = {followup: {}};
+ assert.equal(element.querySelectorAll(
+ 'section gr-button[data-action-type="revision"]').length, 0);
+ });
- test('getActionDetails', () => {
- element.revisionActions = Object.assign({
- 'plugin~action': {},
- }, element.revisionActions);
- assert.isUndefined(element.getActionDetails('rubbish'));
- assert.strictEqual(element.revisionActions['plugin~action'],
- element.getActionDetails('plugin~action'));
- assert.strictEqual(element.revisionActions['rebase'],
- element.getActionDetails('rebase'));
- });
+ test('getActionDetails', () => {
+ element.revisionActions = Object.assign({
+ 'plugin~action': {},
+ }, element.revisionActions);
+ assert.isUndefined(element.getActionDetails('rubbish'));
+ assert.strictEqual(element.revisionActions['plugin~action'],
+ element.getActionDetails('plugin~action'));
+ assert.strictEqual(element.revisionActions['rebase'],
+ element.getActionDetails('rebase'));
+ });
- test('hide revision action', done => {
+ test('hide revision action', done => {
+ flush(() => {
+ const buttonEl = element.shadowRoot
+ .querySelector('[data-action-key="submit"]');
+ assert.isOk(buttonEl);
+ assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+ element.setActionHidden(element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT, true);
+ assert.lengthOf(element._hiddenActions, 1);
+ element.setActionHidden(element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT, true);
+ assert.lengthOf(element._hiddenActions, 1);
flush(() => {
const buttonEl = element.shadowRoot
.querySelector('[data-action-key="submit"]');
- assert.isOk(buttonEl);
- assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+ assert.isNotOk(buttonEl);
+
element.setActionHidden(element.ActionType.REVISION,
- element.RevisionActions.SUBMIT, true);
- assert.lengthOf(element._hiddenActions, 1);
- element.setActionHidden(element.ActionType.REVISION,
- element.RevisionActions.SUBMIT, true);
- assert.lengthOf(element._hiddenActions, 1);
+ element.RevisionActions.SUBMIT, false);
flush(() => {
const buttonEl = element.shadowRoot
.querySelector('[data-action-key="submit"]');
- assert.isNotOk(buttonEl);
-
- element.setActionHidden(element.ActionType.REVISION,
- element.RevisionActions.SUBMIT, false);
- flush(() => {
- const buttonEl = element.shadowRoot
- .querySelector('[data-action-key="submit"]');
- assert.isOk(buttonEl);
- assert.isFalse(buttonEl.hasAttribute('hidden'));
- done();
- });
+ assert.isOk(buttonEl);
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
+ done();
});
});
});
+ });
- test('buttons exist', done => {
- element._loading = false;
- flush(() => {
- const buttonEls = Polymer.dom(element.root)
- .querySelectorAll('gr-button');
- const menuItems = element.$.moreActions.items;
+ test('buttons exist', done => {
+ element._loading = false;
+ flush(() => {
+ const buttonEls = dom(element.root)
+ .querySelectorAll('gr-button');
+ const menuItems = element.$.moreActions.items;
- // Total button number is one greater than the number of total actions
- // due to the existence of the overflow menu trigger.
- assert.equal(buttonEls.length + menuItems.length,
- element._allActionValues.length + 1);
- assert.isFalse(element.hidden);
- done();
- });
+ // Total button number is one greater than the number of total actions
+ // due to the existence of the overflow menu trigger.
+ assert.equal(buttonEls.length + menuItems.length,
+ element._allActionValues.length + 1);
+ assert.isFalse(element.hidden);
+ done();
});
+ });
- test('delete buttons have explicit labels', done => {
- flush(() => {
- const deleteItems = element.$.moreActions.items
- .filter(item => item.id.startsWith('delete'));
- assert.equal(deleteItems.length, 1);
- assert.notEqual(deleteItems[0].name);
- assert.equal(deleteItems[0].name, 'Delete change');
- done();
- });
+ test('delete buttons have explicit labels', done => {
+ flush(() => {
+ const deleteItems = element.$.moreActions.items
+ .filter(item => item.id.startsWith('delete'));
+ assert.equal(deleteItems.length, 1);
+ assert.notEqual(deleteItems[0].name);
+ assert.equal(deleteItems[0].name, 'Delete change');
+ done();
});
+ });
- test('get revision object from change', () => {
- const revObj = {_number: 2, foo: 'bar'};
- const change = {
- revisions: {
- rev1: {_number: 1},
- rev2: revObj,
- },
- };
- assert.deepEqual(element._getRevision(change, '2'), revObj);
- });
+ test('get revision object from change', () => {
+ const revObj = {_number: 2, foo: 'bar'};
+ const change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: revObj,
+ },
+ };
+ assert.deepEqual(element._getRevision(change, '2'), revObj);
+ });
- test('_actionComparator sort order', () => {
- const actions = [
- {label: '123', __type: 'change', __key: 'review'},
- {label: 'abc-ro', __type: 'revision'},
- {label: 'abc', __type: 'change'},
- {label: 'def', __type: 'change'},
- {label: 'def-p', __type: 'change', __primary: true},
- ];
+ test('_actionComparator sort order', () => {
+ const actions = [
+ {label: '123', __type: 'change', __key: 'review'},
+ {label: 'abc-ro', __type: 'revision'},
+ {label: 'abc', __type: 'change'},
+ {label: 'def', __type: 'change'},
+ {label: 'def-p', __type: 'change', __primary: true},
+ ];
- const result = actions.slice();
- result.reverse();
- result.sort(element._actionComparator.bind(element));
- assert.deepEqual(result, actions);
- });
+ const result = actions.slice();
+ result.reverse();
+ result.sort(element._actionComparator.bind(element));
+ assert.deepEqual(result, actions);
+ });
- test('submit change', () => {
- const showSpy = sandbox.spy(element, '_showActionDialog');
- sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
- sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
- element.change = {
- revisions: {
- rev1: {_number: 1},
- rev2: {_number: 2},
- },
- };
- element.latestPatchNum = '2';
+ test('submit change', () => {
+ const showSpy = sandbox.spy(element, '_showActionDialog');
+ sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+ sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
+ element.change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: {_number: 2},
+ },
+ };
+ element.latestPatchNum = '2';
+ const submitButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="submit"]');
+ assert.ok(submitButton);
+ MockInteractions.tap(submitButton);
+
+ flushAsynchronousOperations();
+ assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
+ });
+
+ test('submit change, tap on icon', done => {
+ sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done);
+ sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+ sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
+ element.change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: {_number: 2},
+ },
+ };
+ element.latestPatchNum = '2';
+
+ const submitIcon =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key="submit"] iron-icon');
+ assert.ok(submitIcon);
+ MockInteractions.tap(submitIcon);
+ });
+
+ test('_handleSubmitConfirm', () => {
+ const fireStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(element, '_canSubmitChange').returns(true);
+ element._handleSubmitConfirm();
+ assert.isTrue(fireStub.calledOnce);
+ assert.deepEqual(fireStub.lastCall.args,
+ ['/submit', element.revisionActions.submit, true]);
+ });
+
+ test('_handleSubmitConfirm when not able to submit', () => {
+ const fireStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(element, '_canSubmitChange').returns(false);
+ element._handleSubmitConfirm();
+ assert.isFalse(fireStub.called);
+ });
+
+ test('submit change with plugin hook', done => {
+ sandbox.stub(element, '_canSubmitChange',
+ () => false);
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ flush(() => {
const submitButton = element.shadowRoot
.querySelector('gr-button[data-action-key="submit"]');
assert.ok(submitButton);
MockInteractions.tap(submitButton);
+ assert.equal(fireActionStub.callCount, 0);
- flushAsynchronousOperations();
- assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog));
+ done();
});
+ });
- test('submit change, tap on icon', done => {
- sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done);
- sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
- sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve());
- element.change = {
- revisions: {
- rev1: {_number: 1},
- rev2: {_number: 2},
- },
- };
- element.latestPatchNum = '2';
+ test('chain state', () => {
+ assert.equal(element._hasKnownChainState, false);
+ element.hasParent = true;
+ assert.equal(element._hasKnownChainState, true);
+ element.hasParent = false;
+ });
- const submitIcon =
- element.shadowRoot
- .querySelector('gr-button[data-action-key="submit"] iron-icon');
- assert.ok(submitIcon);
- MockInteractions.tap(submitIcon);
- });
+ test('_calculateDisabled', () => {
+ let hasKnownChainState = false;
+ const action = {__key: 'rebase', enabled: true};
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), true);
- test('_handleSubmitConfirm', () => {
- const fireStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(element, '_canSubmitChange').returns(true);
- element._handleSubmitConfirm();
- assert.isTrue(fireStub.calledOnce);
- assert.deepEqual(fireStub.lastCall.args,
- ['/submit', element.revisionActions.submit, true]);
- });
+ action.__key = 'delete';
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), false);
- test('_handleSubmitConfirm when not able to submit', () => {
- const fireStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(element, '_canSubmitChange').returns(false);
- element._handleSubmitConfirm();
- assert.isFalse(fireStub.called);
- });
+ action.__key = 'rebase';
+ hasKnownChainState = true;
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), false);
- test('submit change with plugin hook', done => {
- sandbox.stub(element, '_canSubmitChange',
- () => false);
- const fireActionStub = sandbox.stub(element, '_fireAction');
- flush(() => {
- const submitButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="submit"]');
- assert.ok(submitButton);
- MockInteractions.tap(submitButton);
- assert.equal(fireActionStub.callCount, 0);
+ action.enabled = false;
+ assert.equal(
+ element._calculateDisabled(action, hasKnownChainState), true);
+ });
- done();
- });
- });
-
- test('chain state', () => {
- assert.equal(element._hasKnownChainState, false);
- element.hasParent = true;
- assert.equal(element._hasKnownChainState, true);
- element.hasParent = false;
- });
-
- test('_calculateDisabled', () => {
- let hasKnownChainState = false;
- const action = {__key: 'rebase', enabled: true};
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), true);
-
- action.__key = 'delete';
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), false);
-
- action.__key = 'rebase';
- hasKnownChainState = true;
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), false);
-
- action.enabled = false;
- assert.equal(
- element._calculateDisabled(action, hasKnownChainState), true);
- });
-
- test('rebase change', done => {
- const fireActionStub = sandbox.stub(element, '_fireAction');
- const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
- 'fetchRecentChanges').returns(Promise.resolve([]));
- element._hasKnownChainState = true;
- flush(() => {
- const rebaseButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="rebase"]');
- MockInteractions.tap(rebaseButton);
- const rebaseAction = {
- __key: 'rebase',
- __type: 'revision',
- __primary: false,
- enabled: true,
- label: 'Rebase',
- method: 'POST',
- title: 'Rebase onto tip of branch or parent change',
- };
- assert.isTrue(fetchChangesStub.called);
- element._handleRebaseConfirm({detail: {base: '1234'}});
- rebaseAction.rebaseOnCurrent = true;
- assert.deepEqual(fireActionStub.lastCall.args,
- ['/rebase', rebaseAction, true, {base: '1234'}]);
- done();
- });
- });
-
- test(`rebase dialog gets recent changes each time it's opened`, done => {
- const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
- 'fetchRecentChanges').returns(Promise.resolve([]));
- element._hasKnownChainState = true;
+ test('rebase change', done => {
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
+ 'fetchRecentChanges').returns(Promise.resolve([]));
+ element._hasKnownChainState = true;
+ flush(() => {
const rebaseButton = element.shadowRoot
.querySelector('gr-button[data-action-key="rebase"]');
MockInteractions.tap(rebaseButton);
- assert.isTrue(fetchChangesStub.calledOnce);
+ const rebaseAction = {
+ __key: 'rebase',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Rebase',
+ method: 'POST',
+ title: 'Rebase onto tip of branch or parent change',
+ };
+ assert.isTrue(fetchChangesStub.called);
+ element._handleRebaseConfirm({detail: {base: '1234'}});
+ rebaseAction.rebaseOnCurrent = true;
+ assert.deepEqual(fireActionStub.lastCall.args,
+ ['/rebase', rebaseAction, true, {base: '1234'}]);
+ done();
+ });
+ });
+ test(`rebase dialog gets recent changes each time it's opened`, done => {
+ const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
+ 'fetchRecentChanges').returns(Promise.resolve([]));
+ element._hasKnownChainState = true;
+ const rebaseButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebase"]');
+ MockInteractions.tap(rebaseButton);
+ assert.isTrue(fetchChangesStub.calledOnce);
+
+ flush(() => {
+ element.$.confirmRebase.fire('cancel');
+ MockInteractions.tap(rebaseButton);
+ assert.isTrue(fetchChangesStub.calledTwice);
+ done();
+ });
+ });
+
+ test('two dialogs are not shown at the same time', done => {
+ element._hasKnownChainState = true;
+ flush(() => {
+ const rebaseButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebase"]');
+ assert.ok(rebaseButton);
+ MockInteractions.tap(rebaseButton);
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.confirmRebase.hidden);
+
+ element._handleCherrypickTap();
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.confirmRebase.hidden);
+ assert.isFalse(element.$.confirmCherrypick.hidden);
+ done();
+ });
+ });
+
+ test('fullscreen-overlay-opened hides content', () => {
+ sandbox.spy(element, '_handleHideBackgroundContent');
+ element.$.overlay.fire('fullscreen-overlay-opened');
+ assert.isTrue(element._handleHideBackgroundContent.called);
+ assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
+ test('fullscreen-overlay-closed shows content', () => {
+ sandbox.spy(element, '_handleShowBackgroundContent');
+ element.$.overlay.fire('fullscreen-overlay-closed');
+ assert.isTrue(element._handleShowBackgroundContent.called);
+ assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
+ test('_setLabelValuesOnRevert', () => {
+ const labels = {'Foo': 1, 'Bar-Baz': -2};
+ const changeId = 1234;
+ sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
+ const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview')
+ .returns(Promise.resolve());
+ return element._setLabelValuesOnRevert(changeId).then(() => {
+ assert.isTrue(saveStub.calledOnce);
+ assert.equal(saveStub.lastCall.args[0], changeId);
+ assert.deepEqual(saveStub.lastCall.args[2], {labels});
+ });
+ });
+
+ suite('change edits', () => {
+ test('disableEdit', () => {
+ element.set('editMode', false);
+ element.set('editPatchsetLoaded', false);
+ element.change = {status: 'NEW'};
+ element.set('disableEdit', true);
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('shows confirm dialog for delete edit', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ element._handleDeleteEditTap();
+ assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('#confirmDeleteEditDialog')
+ .shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.equal(fireActionStub.lastCall.args[0], '/edit');
+ });
+
+ test('hide publishEdit and rebaseEdit if change is not open', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+ element.change = {status: 'MERGED'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ });
+
+ test('edit patchset is loaded, needs rebase', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+ element.change = {status: 'NEW'};
+ element.editBasedOnCurrentPatchSet = false;
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('edit patchset is loaded, does not need rebase', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', true);
+ element.change = {status: 'NEW'};
+ element.editBasedOnCurrentPatchSet = true;
+ flushAsynchronousOperations();
+
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('edit mode is loaded, no edit patchset', () => {
+ element.set('editMode', true);
+ element.set('editPatchsetLoaded', false);
+ element.change = {status: 'NEW'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('normal patch set', () => {
+ element.set('editMode', false);
+ element.set('editPatchsetLoaded', false);
+ element.change = {status: 'NEW'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="publishEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="rebaseEdit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="deleteEdit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ });
+
+ test('edit action', done => {
+ element.addEventListener('edit-tap', () => { done(); });
+ element.set('editMode', true);
+ element.change = {status: 'NEW'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ assert.isOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="stopEdit"]'));
+ element.change = {status: 'MERGED'};
+ flushAsynchronousOperations();
+
+ assert.isNotOk(element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]'));
+ element.change = {status: 'NEW'};
+ element.set('editMode', false);
+ flushAsynchronousOperations();
+
+ const editButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="edit"]');
+ assert.isOk(editButton);
+ MockInteractions.tap(editButton);
+ });
+ });
+
+ suite('cherry-pick', () => {
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(window, 'alert');
+ });
+
+ test('works', () => {
+ element._handleCherrypickTap();
+ const action = {
+ __key: 'cherrypick',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Cherry pick',
+ method: 'POST',
+ title: 'Cherry pick change to a different branch',
+ };
+
+ element._handleCherrypickConfirm();
+ assert.equal(fireActionStub.callCount, 0);
+
+ element.$.confirmCherrypick.branch = 'master';
+ element._handleCherrypickConfirm();
+ assert.equal(fireActionStub.callCount, 0); // Still needs a message.
+
+ // Add attributes that are used to determine the message.
+ element.$.confirmCherrypick.commitMessage = 'foo message';
+ element.$.confirmCherrypick.changeStatus = 'OPEN';
+ element.$.confirmCherrypick.commitNum = '123';
+
+ element._handleCherrypickConfirm();
+
+ assert.equal(element.$.confirmCherrypick.$.messageInput.value,
+ 'foo message');
+
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/cherrypick', action, true, {
+ destination: 'master',
+ base: null,
+ message: 'foo message',
+ allow_conflicts: false,
+ },
+ ]);
+ });
+
+ test('cherry pick even with conflicts', () => {
+ element._handleCherrypickTap();
+ const action = {
+ __key: 'cherrypick',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Cherry pick',
+ method: 'POST',
+ title: 'Cherry pick change to a different branch',
+ };
+
+ element.$.confirmCherrypick.branch = 'master';
+
+ // Add attributes that are used to determine the message.
+ element.$.confirmCherrypick.commitMessage = 'foo message';
+ element.$.confirmCherrypick.changeStatus = 'OPEN';
+ element.$.confirmCherrypick.commitNum = '123';
+
+ element._handleCherrypickConflictConfirm();
+
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/cherrypick', action, true, {
+ destination: 'master',
+ base: null,
+ message: 'foo message',
+ allow_conflicts: true,
+ },
+ ]);
+ });
+
+ test('branch name cleared when re-open cherrypick', () => {
+ const emptyBranchName = '';
+ element.$.confirmCherrypick.branch = 'master';
+
+ element._handleCherrypickTap();
+ assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
+ });
+ });
+
+ suite('move change', () => {
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ sandbox.stub(window, 'alert');
+ });
+
+ test('works', () => {
+ element._handleMoveTap();
+
+ element._handleMoveConfirm();
+ assert.equal(fireActionStub.callCount, 0);
+
+ element.$.confirmMove.branch = 'master';
+ element._handleMoveConfirm();
+ assert.equal(fireActionStub.callCount, 1);
+ });
+
+ test('branch name cleared when re-open move', () => {
+ const emptyBranchName = '';
+ element.$.confirmMove.branch = 'master';
+
+ element._handleMoveTap();
+ assert.equal(element.$.confirmMove.branch, emptyBranchName);
+ });
+ });
+
+ test('custom actions', done => {
+ // Add a button with the same key as a server-based one to ensure
+ // collisions are taken care of.
+ const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
+ element.addEventListener(key + '-tap', e => {
+ assert.equal(e.detail.node.getAttribute('data-action-key'), key);
+ element.removeActionButton(key);
flush(() => {
- element.$.confirmRebase.fire('cancel');
- MockInteractions.tap(rebaseButton);
- assert.isTrue(fetchChangesStub.calledTwice);
- done();
- });
- });
-
- test('two dialogs are not shown at the same time', done => {
- element._hasKnownChainState = true;
- flush(() => {
- const rebaseButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="rebase"]');
- assert.ok(rebaseButton);
- MockInteractions.tap(rebaseButton);
- flushAsynchronousOperations();
- assert.isFalse(element.$.confirmRebase.hidden);
-
- element._handleCherrypickTap();
- flushAsynchronousOperations();
- assert.isTrue(element.$.confirmRebase.hidden);
- assert.isFalse(element.$.confirmCherrypick.hidden);
- done();
- });
- });
-
- test('fullscreen-overlay-opened hides content', () => {
- sandbox.spy(element, '_handleHideBackgroundContent');
- element.$.overlay.fire('fullscreen-overlay-opened');
- assert.isTrue(element._handleHideBackgroundContent.called);
- assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
- });
-
- test('fullscreen-overlay-closed shows content', () => {
- sandbox.spy(element, '_handleShowBackgroundContent');
- element.$.overlay.fire('fullscreen-overlay-closed');
- assert.isTrue(element._handleShowBackgroundContent.called);
- assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
- });
-
- test('_setLabelValuesOnRevert', () => {
- const labels = {'Foo': 1, 'Bar-Baz': -2};
- const changeId = 1234;
- sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
- const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview')
- .returns(Promise.resolve());
- return element._setLabelValuesOnRevert(changeId).then(() => {
- assert.isTrue(saveStub.calledOnce);
- assert.equal(saveStub.lastCall.args[0], changeId);
- assert.deepEqual(saveStub.lastCall.args[2], {labels});
- });
- });
-
- suite('change edits', () => {
- test('disableEdit', () => {
- element.set('editMode', false);
- element.set('editPatchsetLoaded', false);
- element.change = {status: 'NEW'};
- element.set('disableEdit', true);
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('shows confirm dialog for delete edit', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
-
- const fireActionStub = sandbox.stub(element, '_fireAction');
- element._handleDeleteEditTap();
- assert.isFalse(element.$.confirmDeleteEditDialog.hidden);
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('#confirmDeleteEditDialog')
- .shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.equal(fireActionStub.lastCall.args[0], '/edit');
- });
-
- test('hide publishEdit and rebaseEdit if change is not open', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
- element.change = {status: 'MERGED'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- });
-
- test('edit patchset is loaded, needs rebase', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
- element.change = {status: 'NEW'};
- element.editBasedOnCurrentPatchSet = false;
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('edit patchset is loaded, does not need rebase', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', true);
- element.change = {status: 'NEW'};
- element.editBasedOnCurrentPatchSet = true;
- flushAsynchronousOperations();
-
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('edit mode is loaded, no edit patchset', () => {
- element.set('editMode', true);
- element.set('editPatchsetLoaded', false);
- element.change = {status: 'NEW'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('normal patch set', () => {
- element.set('editMode', false);
- element.set('editPatchsetLoaded', false);
- element.change = {status: 'NEW'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="publishEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="rebaseEdit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="deleteEdit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- });
-
- test('edit action', done => {
- element.addEventListener('edit-tap', () => { done(); });
- element.set('editMode', true);
- element.change = {status: 'NEW'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- assert.isOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="stopEdit"]'));
- element.change = {status: 'MERGED'};
- flushAsynchronousOperations();
-
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]'));
- element.change = {status: 'NEW'};
- element.set('editMode', false);
- flushAsynchronousOperations();
-
- const editButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="edit"]');
- assert.isOk(editButton);
- MockInteractions.tap(editButton);
- });
- });
-
- suite('cherry-pick', () => {
- let fireActionStub;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(window, 'alert');
- });
-
- test('works', () => {
- element._handleCherrypickTap();
- const action = {
- __key: 'cherrypick',
- __type: 'revision',
- __primary: false,
- enabled: true,
- label: 'Cherry pick',
- method: 'POST',
- title: 'Cherry pick change to a different branch',
- };
-
- element._handleCherrypickConfirm();
- assert.equal(fireActionStub.callCount, 0);
-
- element.$.confirmCherrypick.branch = 'master';
- element._handleCherrypickConfirm();
- assert.equal(fireActionStub.callCount, 0); // Still needs a message.
-
- // Add attributes that are used to determine the message.
- element.$.confirmCherrypick.commitMessage = 'foo message';
- element.$.confirmCherrypick.changeStatus = 'OPEN';
- element.$.confirmCherrypick.commitNum = '123';
-
- element._handleCherrypickConfirm();
-
- assert.equal(element.$.confirmCherrypick.$.messageInput.value,
- 'foo message');
-
- assert.deepEqual(fireActionStub.lastCall.args, [
- '/cherrypick', action, true, {
- destination: 'master',
- base: null,
- message: 'foo message',
- allow_conflicts: false,
- },
- ]);
- });
-
- test('cherry pick even with conflicts', () => {
- element._handleCherrypickTap();
- const action = {
- __key: 'cherrypick',
- __type: 'revision',
- __primary: false,
- enabled: true,
- label: 'Cherry pick',
- method: 'POST',
- title: 'Cherry pick change to a different branch',
- };
-
- element.$.confirmCherrypick.branch = 'master';
-
- // Add attributes that are used to determine the message.
- element.$.confirmCherrypick.commitMessage = 'foo message';
- element.$.confirmCherrypick.changeStatus = 'OPEN';
- element.$.confirmCherrypick.commitNum = '123';
-
- element._handleCherrypickConflictConfirm();
-
- assert.deepEqual(fireActionStub.lastCall.args, [
- '/cherrypick', action, true, {
- destination: 'master',
- base: null,
- message: 'foo message',
- allow_conflicts: true,
- },
- ]);
- });
-
- test('branch name cleared when re-open cherrypick', () => {
- const emptyBranchName = '';
- element.$.confirmCherrypick.branch = 'master';
-
- element._handleCherrypickTap();
- assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
- });
- });
-
- suite('move change', () => {
- let fireActionStub;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- sandbox.stub(window, 'alert');
- });
-
- test('works', () => {
- element._handleMoveTap();
-
- element._handleMoveConfirm();
- assert.equal(fireActionStub.callCount, 0);
-
- element.$.confirmMove.branch = 'master';
- element._handleMoveConfirm();
- assert.equal(fireActionStub.callCount, 1);
- });
-
- test('branch name cleared when re-open move', () => {
- const emptyBranchName = '';
- element.$.confirmMove.branch = 'master';
-
- element._handleMoveTap();
- assert.equal(element.$.confirmMove.branch, emptyBranchName);
- });
- });
-
- test('custom actions', done => {
- // Add a button with the same key as a server-based one to ensure
- // collisions are taken care of.
- const key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
- element.addEventListener(key + '-tap', e => {
- assert.equal(e.detail.node.getAttribute('data-action-key'), key);
- element.removeActionButton(key);
- flush(() => {
- assert.notOk(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- done();
- });
- });
- flush(() => {
- MockInteractions.tap(element.shadowRoot
+ assert.notOk(element.shadowRoot
.querySelector('[data-action-key="' + key + '"]'));
+ done();
});
});
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ });
+ });
- test('_setLoadingOnButtonWithKey top-level', () => {
- const key = 'rebase';
- const type = 'revision';
- const cleanup = element._setLoadingOnButtonWithKey(type, key);
- assert.equal(element._actionLoadingMessage, 'Rebasing...');
+ test('_setLoadingOnButtonWithKey top-level', () => {
+ const key = 'rebase';
+ const type = 'revision';
+ const cleanup = element._setLoadingOnButtonWithKey(type, key);
+ assert.equal(element._actionLoadingMessage, 'Rebasing...');
- const button = element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]');
- assert.isTrue(button.hasAttribute('loading'));
- assert.isTrue(button.disabled);
+ const button = element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]');
+ assert.isTrue(button.hasAttribute('loading'));
+ assert.isTrue(button.disabled);
- assert.isOk(cleanup);
- assert.isFunction(cleanup);
- cleanup();
+ assert.isOk(cleanup);
+ assert.isFunction(cleanup);
+ cleanup();
- assert.isFalse(button.hasAttribute('loading'));
- assert.isFalse(button.disabled);
- assert.isNotOk(element._actionLoadingMessage);
+ assert.isFalse(button.hasAttribute('loading'));
+ assert.isFalse(button.disabled);
+ assert.isNotOk(element._actionLoadingMessage);
+ });
+
+ test('_setLoadingOnButtonWithKey overflow menu', () => {
+ const key = 'cherrypick';
+ const type = 'revision';
+ const cleanup = element._setLoadingOnButtonWithKey(type, key);
+ assert.equal(element._actionLoadingMessage, 'Cherry-picking...');
+ assert.include(element._disabledMenuActions, 'cherrypick');
+ assert.isFunction(cleanup);
+
+ cleanup();
+
+ assert.notOk(element._actionLoadingMessage);
+ assert.notInclude(element._disabledMenuActions, 'cherrypick');
+ });
+
+ suite('abandon change', () => {
+ let alertStub;
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ alertStub = sandbox.stub(window, 'alert');
+ element.actions = {
+ abandon: {
+ method: 'POST',
+ label: 'Abandon',
+ title: 'Abandon the change',
+ enabled: true,
+ },
+ };
+ return element.reload();
});
- test('_setLoadingOnButtonWithKey overflow menu', () => {
- const key = 'cherrypick';
- const type = 'revision';
- const cleanup = element._setLoadingOnButtonWithKey(type, key);
- assert.equal(element._actionLoadingMessage, 'Cherry-picking...');
- assert.include(element._disabledMenuActions, 'cherrypick');
- assert.isFunction(cleanup);
-
- cleanup();
-
- assert.notOk(element._actionLoadingMessage);
- assert.notInclude(element._disabledMenuActions, 'cherrypick');
- });
-
- suite('abandon change', () => {
- let alertStub;
- let fireActionStub;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- alertStub = sandbox.stub(window, 'alert');
- element.actions = {
- abandon: {
- method: 'POST',
- label: 'Abandon',
- title: 'Abandon the change',
- enabled: true,
- },
- };
- return element.reload();
- });
-
- test('abandon change with message', done => {
- const newAbandonMsg = 'Test Abandon Message';
- element.$.confirmAbandonDialog.message = newAbandonMsg;
- flush(() => {
- const abandonButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key="abandon"]');
- MockInteractions.tap(abandonButton);
-
- assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
- done();
- });
- });
-
- test('abandon change with no message', done => {
- flush(() => {
- const abandonButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key="abandon"]');
- MockInteractions.tap(abandonButton);
-
- assert.isUndefined(element.$.confirmAbandonDialog.message);
- done();
- });
- });
-
- test('works', () => {
- element.$.confirmAbandonDialog.message = 'original message';
- const restoreButton =
+ test('abandon change with message', done => {
+ const newAbandonMsg = 'Test Abandon Message';
+ element.$.confirmAbandonDialog.message = newAbandonMsg;
+ flush(() => {
+ const abandonButton =
element.shadowRoot
.querySelector('gr-button[data-action-key="abandon"]');
- MockInteractions.tap(restoreButton);
+ MockInteractions.tap(abandonButton);
- element.$.confirmAbandonDialog.message = 'foo message';
- element._handleAbandonDialogConfirm();
- assert.notOk(alertStub.called);
-
- const action = {
- __key: 'abandon',
- __type: 'change',
- __primary: false,
- enabled: true,
- label: 'Abandon',
- method: 'POST',
- title: 'Abandon the change',
- };
- assert.deepEqual(fireActionStub.lastCall.args, [
- '/abandon', action, false, {
- message: 'foo message',
- }]);
+ assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
+ done();
});
});
- suite('revert change', () => {
- let fireActionStub;
+ test('abandon change with no message', done => {
+ flush(() => {
+ const abandonButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key="abandon"]');
+ MockInteractions.tap(abandonButton);
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- element.commitMessage = 'random commit message';
- element.change.current_revision = 'abcdef';
- element.actions = {
- revert: {
- method: 'POST',
- label: 'Revert',
- title: 'Revert the change',
- enabled: true,
- },
- };
- return element.reload();
+ assert.isUndefined(element.$.confirmAbandonDialog.message);
+ done();
});
+ });
- test('revert change with plugin hook', done => {
- const newRevertMsg = 'Modified revert msg';
- sandbox.stub(element.$.confirmRevertDialog, '_modifyRevertMsg',
- () => newRevertMsg);
+ test('works', () => {
+ element.$.confirmAbandonDialog.message = 'original message';
+ const restoreButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key="abandon"]');
+ MockInteractions.tap(restoreButton);
+
+ element.$.confirmAbandonDialog.message = 'foo message';
+ element._handleAbandonDialogConfirm();
+ assert.notOk(alertStub.called);
+
+ const action = {
+ __key: 'abandon',
+ __type: 'change',
+ __primary: false,
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon the change',
+ };
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/abandon', action, false, {
+ message: 'foo message',
+ }]);
+ });
+ });
+
+ suite('revert change', () => {
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ element.commitMessage = 'random commit message';
+ element.change.current_revision = 'abcdef';
+ element.actions = {
+ revert: {
+ method: 'POST',
+ label: 'Revert',
+ title: 'Revert the change',
+ enabled: true,
+ },
+ };
+ return element.reload();
+ });
+
+ test('revert change with plugin hook', done => {
+ const newRevertMsg = 'Modified revert msg';
+ sandbox.stub(element.$.confirmRevertDialog, '_modifyRevertMsg',
+ () => newRevertMsg);
+ element.change = {
+ current_revision: 'abc1234',
+ };
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([
+ {change_id: '12345678901234', topic: 'T', subject: 'random'},
+ {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
+ ]));
+ sandbox.stub(element.$.confirmRevertDialog,
+ '_populateRevertSubmissionMessage', () => 'original msg');
+ flush(() => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ MockInteractions.tap(revertButton);
+ flush(() => {
+ assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
+ done();
+ });
+ });
+ });
+
+ suite('revert change submitted together', () => {
+ setup(() => {
element.change = {
- current_revision: 'abc1234',
+ submission_id: '199',
+ current_revision: '2000',
};
sandbox.stub(element.$.restAPI, 'getChanges')
.returns(Promise.resolve([
{change_id: '12345678901234', topic: 'T', subject: 'random'},
{change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
]));
- sandbox.stub(element.$.confirmRevertDialog,
- '_populateRevertSubmissionMessage', () => 'original msg');
+ });
+
+ test('confirm revert dialog shows both options', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ MockInteractions.tap(revertButton);
flush(() => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- MockInteractions.tap(revertButton);
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ const revertSingleChangeLabel = confirmRevertDialog
+ .shadowRoot.querySelector('.revertSingleChange');
+ const revertSubmissionLabel = confirmRevertDialog.
+ shadowRoot.querySelector('.revertSubmission');
+ assert(revertSingleChangeLabel.innerText.trim() ===
+ 'Revert single change');
+ assert(revertSubmissionLabel.innerText.trim() ===
+ 'Revert entire submission (2 Changes)');
+ let expectedMsg = 'Revert submission 199' + '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
+ 'Reverted Changes:' + '\n' +
+ '1234567890:random' + '\n' +
+ '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '\n';
+ assert.equal(confirmRevertDialog._message, expectedMsg);
+ const radioInputs = confirmRevertDialog.shadowRoot
+ .querySelectorAll('input[name="revertOptions"]');
+ MockInteractions.tap(radioInputs[0]);
flush(() => {
- assert.equal(element.$.confirmRevertDialog._message, newRevertMsg);
+ expectedMsg = 'Revert "random commit message"\n\nThis reverts '
+ + 'commit 2000.\n\nReason'
+ + ' for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, expectedMsg);
done();
});
});
});
- suite('revert change submitted together', () => {
+ test('submit fails if message is not edited', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ MockInteractions.tap(revertButton);
+ const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
+ flush(() => {
+ const confirmButton = element.$.confirmRevertDialog.shadowRoot
+ .querySelector('gr-dialog')
+ .shadowRoot.querySelector('#confirm');
+ MockInteractions.tap(confirmButton);
+ flush(() => {
+ assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isFalse(fireStub.called);
+ done();
+ });
+ });
+ });
+
+ test('message modification is retained on switching', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ MockInteractions.tap(revertButton);
+ flush(() => {
+ const radioInputs = confirmRevertDialog.shadowRoot
+ .querySelectorAll('input[name="revertOptions"]');
+ const revertSubmissionMsg = 'Revert submission 199' + '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
+ 'Reverted Changes:' + '\n' +
+ '1234567890:random' + '\n' +
+ '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '\n';
+ const singleChangeMsg =
+ 'Revert "random commit message"\n\nThis reverts '
+ + 'commit 2000.\n\nReason'
+ + ' for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
+ const newRevertMsg = revertSubmissionMsg + 'random';
+ const newSingleChangeMsg = singleChangeMsg + 'random';
+ confirmRevertDialog._message = newRevertMsg;
+ MockInteractions.tap(radioInputs[0]);
+ flush(() => {
+ assert.equal(confirmRevertDialog._message, singleChangeMsg);
+ confirmRevertDialog._message = newSingleChangeMsg;
+ MockInteractions.tap(radioInputs[1]);
+ flush(() => {
+ assert.equal(confirmRevertDialog._message, newRevertMsg);
+ MockInteractions.tap(radioInputs[0]);
+ flush(() => {
+ assert.equal(
+ confirmRevertDialog._message,
+ newSingleChangeMsg
+ );
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+
+ suite('revert single change', () => {
+ setup(() => {
+ element.change = {
+ submission_id: '199',
+ current_revision: '2000',
+ };
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([
+ {change_id: '12345678901234', topic: 'T', subject: 'random'},
+ ]));
+ });
+
+ test('submit fails if message is not edited', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ MockInteractions.tap(revertButton);
+ const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
+ flush(() => {
+ const confirmButton = element.$.confirmRevertDialog.shadowRoot
+ .querySelector('gr-dialog')
+ .shadowRoot.querySelector('#confirm');
+ MockInteractions.tap(confirmButton);
+ flush(() => {
+ assert.isTrue(confirmRevertDialog._showErrorMessage);
+ assert.isFalse(fireStub.called);
+ done();
+ });
+ });
+ });
+
+ test('confirm revert dialog shows no radio button', done => {
+ const revertButton = element.shadowRoot
+ .querySelector('gr-button[data-action-key="revert"]');
+ MockInteractions.tap(revertButton);
+ flush(() => {
+ const confirmRevertDialog = element.$.confirmRevertDialog;
+ const radioInputs = confirmRevertDialog.shadowRoot
+ .querySelectorAll('input[name="revertOptions"]');
+ assert.equal(radioInputs.length, 0);
+ const msg = 'Revert "random commit message"\n\n'
+ + 'This reverts commit 2000.\n\nReason '
+ + 'for revert: <INSERT REASONING HERE>\n';
+ assert.equal(confirmRevertDialog._message, msg);
+ const editedMsg = msg + 'hello';
+ confirmRevertDialog._message += 'hello';
+ const confirmButton = element.$.confirmRevertDialog.shadowRoot
+ .querySelector('gr-dialog')
+ .shadowRoot.querySelector('#confirm');
+ MockInteractions.tap(confirmButton);
+ flush(() => {
+ assert.equal(fireActionStub.getCall(0).args[0], '/revert');
+ assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
+ assert.equal(fireActionStub.getCall(0).args[3].message,
+ editedMsg);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ suite('mark change private', () => {
+ setup(() => {
+ const privateAction = {
+ __key: 'private',
+ __type: 'change',
+ __primary: false,
+ method: 'POST',
+ label: 'Mark private',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ private: privateAction,
+ };
+
+ element.change.is_private = false;
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ return element.reload();
+ });
+
+ test('make sure the mark private change button is not outside of the ' +
+ 'overflow menu', done => {
+ flush(() => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="private"]'));
+ done();
+ });
+ });
+
+ test('private change', done => {
+ flush(() => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private-change"]'));
+ element.setActionOverflow('change', 'private', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="private"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private-change"]'));
+ done();
+ });
+ });
+ });
+
+ suite('unmark private change', () => {
+ setup(() => {
+ const unmarkPrivateAction = {
+ __key: 'private.delete',
+ __type: 'change',
+ __primary: false,
+ method: 'POST',
+ label: 'Unmark private',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ 'private.delete': unmarkPrivateAction,
+ };
+
+ element.change.is_private = true;
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ return element.reload();
+ });
+
+ test('make sure the unmark private change button is not outside of the ' +
+ 'overflow menu', done => {
+ flush(() => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="private.delete"]'));
+ done();
+ });
+ });
+
+ test('unmark the private change', done => {
+ flush(() => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private.delete-change"]')
+ );
+ element.setActionOverflow('change', 'private.delete', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="private.delete"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="private.delete-change"]')
+ );
+ done();
+ });
+ });
+ });
+
+ suite('delete change', () => {
+ let fireActionStub;
+ let deleteAction;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ element.change = {
+ current_revision: 'abc1234',
+ };
+ deleteAction = {
+ method: 'DELETE',
+ label: 'Delete Change',
+ title: 'Delete change X_X',
+ enabled: true,
+ };
+ element.actions = {
+ '/': deleteAction,
+ };
+ });
+
+ test('does not delete on action', () => {
+ element._handleDeleteTap();
+ assert.isFalse(fireActionStub.called);
+ });
+
+ test('shows confirm dialog', () => {
+ element._handleDeleteTap();
+ assert.isFalse(element.shadowRoot
+ .querySelector('#confirmDeleteDialog').hidden);
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('#confirmDeleteDialog')
+ .shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+ assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
+ });
+
+ test('hides delete confirm on cancel', () => {
+ element._handleDeleteTap();
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('#confirmDeleteDialog')
+ .shadowRoot
+ .querySelector('gr-button:not([primary])'));
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('#confirmDeleteDialog').hidden);
+ assert.isFalse(fireActionStub.called);
+ });
+ });
+
+ suite('ignore change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const IgnoreAction = {
+ __key: 'ignore',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Ignore',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ ignore: IgnoreAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('make sure the ignore button is not outside of the overflow menu',
+ () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="ignore"]'));
+ });
+
+ test('ignoring change', () => {
+ assert.isOk(element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="ignore-change"]'));
+ element.setActionOverflow('change', 'ignore', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="ignore"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="ignore-change"]'));
+ });
+ });
+
+ suite('unignore change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const UnignoreAction = {
+ __key: 'unignore',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Unignore',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ unignore: UnignoreAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('unignore button is not outside of the overflow menu', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="unignore"]'));
+ });
+
+ test('unignoring change', () => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unignore-change"]'));
+ element.setActionOverflow('change', 'unignore', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="unignore"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unignore-change"]'));
+ });
+ });
+
+ suite('reviewed change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const ReviewedAction = {
+ __key: 'reviewed',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Mark reviewed',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ reviewed: ReviewedAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('make sure the reviewed button is not outside of the overflow menu',
+ () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="reviewed"]'));
+ });
+
+ test('reviewing change', () => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="reviewed-change"]'));
+ element.setActionOverflow('change', 'reviewed', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="reviewed"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="reviewed-change"]'));
+ });
+ });
+
+ suite('unreviewed change', () => {
+ setup(done => {
+ sandbox.stub(element, '_fireAction');
+
+ const UnreviewedAction = {
+ __key: 'unreviewed',
+ __type: 'change',
+ __primary: false,
+ method: 'PUT',
+ label: 'Mark unreviewed',
+ title: 'Working...',
+ enabled: true,
+ };
+
+ element.actions = {
+ unreviewed: UnreviewedAction,
+ };
+
+ element.changeNum = '2';
+ element.latestPatchNum = '2';
+
+ element.reload().then(() => { flush(done); });
+ });
+
+ test('unreviewed button not outside of the overflow menu', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="unreviewed"]'));
+ });
+
+ test('unreviewed change', () => {
+ assert.isOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unreviewed-change"]'));
+ element.setActionOverflow('change', 'unreviewed', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="unreviewed"]'));
+ assert.isNotOk(
+ element.$.moreActions.shadowRoot
+ .querySelector('span[data-id="unreviewed-change"]'));
+ });
+ });
+
+ suite('quick approve', () => {
+ setup(() => {
+ element.change = {
+ current_revision: 'abc1234',
+ };
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {
+ values: {
+ '-1': '',
+ ' 0': '',
+ '+1': '',
+ },
+ },
+ },
+ permitted_labels: {
+ foo: ['-1', ' 0', '+1'],
+ },
+ };
+ flushAsynchronousOperations();
+ });
+
+ test('added when can approve', () => {
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNotNull(approveButton);
+ });
+
+ test('hide quick approve', () => {
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNotNull(approveButton);
+ assert.isFalse(element._hideQuickApproveAction);
+
+ // Assert approve button gets removed from list of buttons.
+ element.hideQuickApproveAction();
+ flushAsynchronousOperations();
+ const approveButtonUpdated =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButtonUpdated);
+ assert.isTrue(element._hideQuickApproveAction);
+ });
+
+ test('is first in list of secondary actions', () => {
+ const approveButton = element.$.secondaryActions
+ .querySelector('gr-button');
+ assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
+ });
+
+ test('not added when already approved', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {
+ approved: {},
+ values: {},
+ },
+ },
+ permitted_labels: {
+ foo: [' 0', '+1'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('not added when label not permitted', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {values: {}},
+ },
+ permitted_labels: {
+ bar: [],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('approves when tapped', () => {
+ const fireActionStub = sandbox.stub(element, '_fireAction');
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']'));
+ flushAsynchronousOperations();
+ assert.isTrue(fireActionStub.called);
+ assert.isTrue(fireActionStub.calledWith('/review'));
+ const payload = fireActionStub.lastCall.args[3];
+ assert.deepEqual(payload.labels, {foo: '+1'});
+ });
+
+ test('not added when multiple labels are required', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {values: {}},
+ bar: {values: {}},
+ },
+ permitted_labels: {
+ foo: [' 0', '+1'],
+ bar: [' 0', '+1', '+2'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('button label for missing approval', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ foo: {
+ values: {
+ ' 0': '',
+ '+1': '',
+ },
+ },
+ bar: {approved: {}, values: {}},
+ },
+ permitted_labels: {
+ foo: [' 0', '+1'],
+ bar: [' 0', '+1', '+2'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
+ });
+
+ test('no quick approve if score is not maximal for a label', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ bar: {
+ value: 1,
+ values: {
+ ' 0': '',
+ '+1': '',
+ '+2': '',
+ },
+ },
+ },
+ permitted_labels: {
+ bar: [' 0', '+1'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.isNull(approveButton);
+ });
+
+ test('approving label with a non-max score', () => {
+ element.change = {
+ current_revision: 'abc1234',
+ labels: {
+ bar: {
+ value: 1,
+ values: {
+ ' 0': '',
+ '+1': '',
+ '+2': '',
+ },
+ },
+ },
+ permitted_labels: {
+ bar: [' 0', '+1', '+2'],
+ },
+ };
+ flushAsynchronousOperations();
+ const approveButton =
+ element.shadowRoot
+ .querySelector('gr-button[data-action-key=\'review\']');
+ assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
+ });
+ });
+
+ test('adds download revision action', () => {
+ const handler = sandbox.stub();
+ element.addEventListener('download-tap', handler);
+ assert.ok(element.revisionActions.download);
+ element._handleDownloadTap();
+ flushAsynchronousOperations();
+
+ assert.isTrue(handler.called);
+ });
+
+ test('changing changeNum or patchNum does not reload', () => {
+ const reloadStub = sandbox.stub(element, 'reload');
+ element.changeNum = 123;
+ assert.isFalse(reloadStub.called);
+ element.latestPatchNum = 456;
+ assert.isFalse(reloadStub.called);
+ });
+
+ test('_toSentenceCase', () => {
+ assert.equal(element._toSentenceCase('blah blah'), 'Blah blah');
+ assert.equal(element._toSentenceCase('BLAH BLAH'), 'Blah blah');
+ assert.equal(element._toSentenceCase('b'), 'B');
+ assert.equal(element._toSentenceCase(''), '');
+ assert.equal(element._toSentenceCase('!@#$%^&*()'), '!@#$%^&*()');
+ });
+
+ suite('setActionOverflow', () => {
+ test('move action from overflow', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="cherrypick"]'));
+ assert.strictEqual(
+ element.$.moreActions.items[0].id, 'cherrypick-revision');
+ element.setActionOverflow('revision', 'cherrypick', false);
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="cherrypick"]'));
+ assert.notEqual(
+ element.$.moreActions.items[0].id, 'cherrypick-revision');
+ });
+
+ test('move action to overflow', () => {
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="submit"]'));
+ element.setActionOverflow('revision', 'submit', true);
+ flushAsynchronousOperations();
+ assert.isNotOk(element.shadowRoot
+ .querySelector('[data-action-key="submit"]'));
+ assert.strictEqual(
+ element.$.moreActions.items[3].id, 'submit-revision');
+ });
+
+ suite('_waitForChangeReachable', () => {
+ setup(() => {
+ sandbox.stub(element, 'async', fn => fn());
+ });
+
+ const makeGetChange = numTries => () => {
+ if (numTries === 1) {
+ return Promise.resolve({_number: 123});
+ } else {
+ numTries--;
+ return Promise.resolve(undefined);
+ }
+ };
+
+ test('succeed', () => {
+ sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5));
+ return element._waitForChangeReachable(123).then(success => {
+ assert.isTrue(success);
+ });
+ });
+
+ test('fail', () => {
+ sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6));
+ return element._waitForChangeReachable(123).then(success => {
+ assert.isFalse(success);
+ });
+ });
+ });
+ });
+
+ suite('_send', () => {
+ let cleanup;
+ let payload;
+ let onShowError;
+ let onShowAlert;
+ let getResponseObjectStub;
+
+ setup(() => {
+ cleanup = sinon.stub();
+ element.changeNum = 42;
+ element.latestPatchNum = 12;
+ payload = {foo: 'bar'};
+
+ onShowError = sinon.stub();
+ element.addEventListener('show-error', onShowError);
+ onShowAlert = sinon.stub();
+ element.addEventListener('show-alert', onShowAlert);
+ });
+
+ suite('happy path', () => {
+ let sendStub;
+ setup(() => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction')
+ .returns(Promise.resolve({}));
+ getResponseObjectStub = sandbox.stub(element.$.restAPI,
+ 'getResponseObject');
+ sandbox.stub(Gerrit.Nav,
+ 'navigateToChange').returns(Promise.resolve(true));
+ });
+
+ test('change action', done => {
+ element
+ ._send('DELETE', payload, '/endpoint', false, cleanup)
+ .then(() => {
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.calledOnce);
+ assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
+ null, payload));
+ done();
+ });
+ });
+
+ suite('show revert submission dialog', () => {
setup(() => {
- element.change = {
- submission_id: '199',
- current_revision: '2000',
- };
+ element.change.submission_id = '199';
+ element.change.current_revision = '2000';
sandbox.stub(element.$.restAPI, 'getChanges')
.returns(Promise.resolve([
{change_id: '12345678901234', topic: 'T', subject: 'random'},
@@ -923,1047 +1748,232 @@
]));
});
- test('confirm revert dialog shows both options', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- MockInteractions.tap(revertButton);
- flush(() => {
- const confirmRevertDialog = element.$.confirmRevertDialog;
- const revertSingleChangeLabel = confirmRevertDialog
- .shadowRoot.querySelector('.revertSingleChange');
- const revertSubmissionLabel = confirmRevertDialog.
- shadowRoot.querySelector('.revertSubmission');
- assert(revertSingleChangeLabel.innerText.trim() ===
- 'Revert single change');
- assert(revertSubmissionLabel.innerText.trim() ===
- 'Revert entire submission (2 Changes)');
- let expectedMsg = 'Revert submission 199' + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
- 'Reverted Changes:' + '\n' +
- '1234567890:random' + '\n' +
- '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
- '\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
- const radioInputs = confirmRevertDialog.shadowRoot
- .querySelectorAll('input[name="revertOptions"]');
- MockInteractions.tap(radioInputs[0]);
- flush(() => {
- expectedMsg = 'Revert "random commit message"\n\nThis reverts '
- + 'commit 2000.\n\nReason'
- + ' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, expectedMsg);
- done();
- });
- });
- });
-
- test('submit fails if message is not edited', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- const confirmRevertDialog = element.$.confirmRevertDialog;
- MockInteractions.tap(revertButton);
- const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
- flush(() => {
- const confirmButton = element.$.confirmRevertDialog.shadowRoot
- .querySelector('gr-dialog')
- .shadowRoot.querySelector('#confirm');
- MockInteractions.tap(confirmButton);
- flush(() => {
- assert.isTrue(confirmRevertDialog._showErrorMessage);
- assert.isFalse(fireStub.called);
- done();
- });
- });
- });
-
- test('message modification is retained on switching', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- const confirmRevertDialog = element.$.confirmRevertDialog;
- MockInteractions.tap(revertButton);
- flush(() => {
- const radioInputs = confirmRevertDialog.shadowRoot
- .querySelectorAll('input[name="revertOptions"]');
- const revertSubmissionMsg = 'Revert submission 199' + '\n\n' +
+ test('revert submission shows submissionId', done => {
+ const expectedMsg = 'Revert submission 199' + '\n\n' +
'Reason for revert: <INSERT REASONING HERE>' + '\n' +
'Reverted Changes:' + '\n' +
- '1234567890:random' + '\n' +
- '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
+ '1234567890: random' + '\n' +
+ '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
'\n';
- const singleChangeMsg =
- 'Revert "random commit message"\n\nThis reverts '
- + 'commit 2000.\n\nReason'
- + ' for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, revertSubmissionMsg);
- const newRevertMsg = revertSubmissionMsg + 'random';
- const newSingleChangeMsg = singleChangeMsg + 'random';
- confirmRevertDialog._message = newRevertMsg;
- MockInteractions.tap(radioInputs[0]);
- flush(() => {
- assert.equal(confirmRevertDialog._message, singleChangeMsg);
- confirmRevertDialog._message = newSingleChangeMsg;
- MockInteractions.tap(radioInputs[1]);
- flush(() => {
- assert.equal(confirmRevertDialog._message, newRevertMsg);
- MockInteractions.tap(radioInputs[0]);
- flush(() => {
- assert.equal(
- confirmRevertDialog._message,
- newSingleChangeMsg
- );
+ const modifiedMsg = expectedMsg + 'abcd';
+ sandbox.stub(element.$.confirmRevertSubmissionDialog,
+ '_modifyRevertSubmissionMsg').returns(modifiedMsg);
+ element.showRevertSubmissionDialog();
+ flush(() => {
+ const msg = element.$.confirmRevertSubmissionDialog.message;
+ assert.equal(msg, modifiedMsg);
+ done();
+ });
+ });
+ });
+
+ suite('single changes revert', () => {
+ let navigateToSearchQueryStub;
+ setup(() => {
+ getResponseObjectStub
+ .returns(Promise.resolve({revert_changes: [
+ {change_id: 12345},
+ ]}));
+ navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToSearchQuery');
+ });
+
+ test('revert submission single change', done => {
+ element._send('POST', {message: 'Revert submission'},
+ '/revert_submission', false, cleanup).then(res => {
+ element._handleResponse({__key: 'revert_submission'}, {}).
+ then(() => {
+ assert.isTrue(navigateToSearchQueryStub.called);
done();
});
- });
- });
});
});
});
- suite('revert single change', () => {
+ suite('multiple changes revert', () => {
+ let showActionDialogStub;
+ let navigateToSearchQueryStub;
setup(() => {
- element.change = {
- submission_id: '199',
- current_revision: '2000',
- };
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([
- {change_id: '12345678901234', topic: 'T', subject: 'random'},
- ]));
+ getResponseObjectStub
+ .returns(Promise.resolve({revert_changes: [
+ {change_id: 12345, topic: 'T'},
+ {change_id: 23456, topic: 'T'},
+ ]}));
+ showActionDialogStub = sandbox.stub(element, '_showActionDialog');
+ navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToSearchQuery');
});
- test('submit fails if message is not edited', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- const confirmRevertDialog = element.$.confirmRevertDialog;
- MockInteractions.tap(revertButton);
- const fireStub = sandbox.stub(confirmRevertDialog, 'fire');
- flush(() => {
- const confirmButton = element.$.confirmRevertDialog.shadowRoot
- .querySelector('gr-dialog')
- .shadowRoot.querySelector('#confirm');
- MockInteractions.tap(confirmButton);
- flush(() => {
- assert.isTrue(confirmRevertDialog._showErrorMessage);
- assert.isFalse(fireStub.called);
+ test('revert submission multiple change', done => {
+ element._send('POST', {message: 'Revert submission'},
+ '/revert_submission', false, cleanup).then(res => {
+ element._handleResponse({__key: 'revert_submission'}, {}).then(
+ () => {
+ assert.isFalse(showActionDialogStub.called);
+ assert.isTrue(navigateToSearchQueryStub.calledWith(
+ 'topic: T'));
+ done();
+ });
+ });
+ });
+ });
+
+ test('revision action', done => {
+ element
+ ._send('DELETE', payload, '/endpoint', true, cleanup)
+ .then(() => {
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.calledOnce);
+ assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
+ 12, payload));
done();
});
- });
- });
+ });
+ });
- test('confirm revert dialog shows no radio button', done => {
- const revertButton = element.shadowRoot
- .querySelector('gr-button[data-action-key="revert"]');
- MockInteractions.tap(revertButton);
- flush(() => {
- const confirmRevertDialog = element.$.confirmRevertDialog;
- const radioInputs = confirmRevertDialog.shadowRoot
- .querySelectorAll('input[name="revertOptions"]');
- assert.equal(radioInputs.length, 0);
- const msg = 'Revert "random commit message"\n\n'
- + 'This reverts commit 2000.\n\nReason '
- + 'for revert: <INSERT REASONING HERE>\n';
- assert.equal(confirmRevertDialog._message, msg);
- const editedMsg = msg + 'hello';
- confirmRevertDialog._message += 'hello';
- const confirmButton = element.$.confirmRevertDialog.shadowRoot
- .querySelector('gr-dialog')
- .shadowRoot.querySelector('#confirm');
- MockInteractions.tap(confirmButton);
- flush(() => {
- assert.equal(fireActionStub.getCall(0).args[0], '/revert');
- assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
- assert.equal(fireActionStub.getCall(0).args[3].message,
- editedMsg);
- done();
+ suite('failure modes', () => {
+ test('non-latest', () => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: false}));
+ const sendStub = sandbox.stub(element.$.restAPI,
+ 'executeChangeAction');
+
+ return element._send('DELETE', payload, '/endpoint', true, cleanup)
+ .then(() => {
+ assert.isTrue(onShowAlert.calledOnce);
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.calledOnce);
+ assert.isFalse(sendStub.called);
});
- });
- });
- });
- });
-
- suite('mark change private', () => {
- setup(() => {
- const privateAction = {
- __key: 'private',
- __type: 'change',
- __primary: false,
- method: 'POST',
- label: 'Mark private',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- private: privateAction,
- };
-
- element.change.is_private = false;
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- return element.reload();
});
- test('make sure the mark private change button is not outside of the ' +
- 'overflow menu', done => {
- flush(() => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="private"]'));
- done();
- });
- });
-
- test('private change', done => {
- flush(() => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private-change"]'));
- element.setActionOverflow('change', 'private', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="private"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private-change"]'));
- done();
- });
- });
- });
-
- suite('unmark private change', () => {
- setup(() => {
- const unmarkPrivateAction = {
- __key: 'private.delete',
- __type: 'change',
- __primary: false,
- method: 'POST',
- label: 'Unmark private',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- 'private.delete': unmarkPrivateAction,
- };
-
- element.change.is_private = true;
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- return element.reload();
- });
-
- test('make sure the unmark private change button is not outside of the ' +
- 'overflow menu', done => {
- flush(() => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="private.delete"]'));
- done();
- });
- });
-
- test('unmark the private change', done => {
- flush(() => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private.delete-change"]')
- );
- element.setActionOverflow('change', 'private.delete', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="private.delete"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="private.delete-change"]')
- );
- done();
- });
- });
- });
-
- suite('delete change', () => {
- let fireActionStub;
- let deleteAction;
-
- setup(() => {
- fireActionStub = sandbox.stub(element, '_fireAction');
- element.change = {
- current_revision: 'abc1234',
- };
- deleteAction = {
- method: 'DELETE',
- label: 'Delete Change',
- title: 'Delete change X_X',
- enabled: true,
- };
- element.actions = {
- '/': deleteAction,
- };
- });
-
- test('does not delete on action', () => {
- element._handleDeleteTap();
- assert.isFalse(fireActionStub.called);
- });
-
- test('shows confirm dialog', () => {
- element._handleDeleteTap();
- assert.isFalse(element.shadowRoot
- .querySelector('#confirmDeleteDialog').hidden);
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('#confirmDeleteDialog')
- .shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
- assert.isTrue(fireActionStub.calledWith('/', deleteAction, false));
- });
-
- test('hides delete confirm on cancel', () => {
- element._handleDeleteTap();
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('#confirmDeleteDialog')
- .shadowRoot
- .querySelector('gr-button:not([primary])'));
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('#confirmDeleteDialog').hidden);
- assert.isFalse(fireActionStub.called);
- });
- });
-
- suite('ignore change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const IgnoreAction = {
- __key: 'ignore',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Ignore',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- ignore: IgnoreAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('make sure the ignore button is not outside of the overflow menu',
- () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="ignore"]'));
- });
-
- test('ignoring change', () => {
- assert.isOk(element.$.moreActions.shadowRoot
- .querySelector('span[data-id="ignore-change"]'));
- element.setActionOverflow('change', 'ignore', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="ignore"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="ignore-change"]'));
- });
- });
-
- suite('unignore change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const UnignoreAction = {
- __key: 'unignore',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Unignore',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- unignore: UnignoreAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('unignore button is not outside of the overflow menu', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="unignore"]'));
- });
-
- test('unignoring change', () => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unignore-change"]'));
- element.setActionOverflow('change', 'unignore', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="unignore"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unignore-change"]'));
- });
- });
-
- suite('reviewed change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const ReviewedAction = {
- __key: 'reviewed',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Mark reviewed',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- reviewed: ReviewedAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('make sure the reviewed button is not outside of the overflow menu',
- () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="reviewed"]'));
- });
-
- test('reviewing change', () => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="reviewed-change"]'));
- element.setActionOverflow('change', 'reviewed', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="reviewed"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="reviewed-change"]'));
- });
- });
-
- suite('unreviewed change', () => {
- setup(done => {
- sandbox.stub(element, '_fireAction');
-
- const UnreviewedAction = {
- __key: 'unreviewed',
- __type: 'change',
- __primary: false,
- method: 'PUT',
- label: 'Mark unreviewed',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- unreviewed: UnreviewedAction,
- };
-
- element.changeNum = '2';
- element.latestPatchNum = '2';
-
- element.reload().then(() => { flush(done); });
- });
-
- test('unreviewed button not outside of the overflow menu', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="unreviewed"]'));
- });
-
- test('unreviewed change', () => {
- assert.isOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unreviewed-change"]'));
- element.setActionOverflow('change', 'unreviewed', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="unreviewed"]'));
- assert.isNotOk(
- element.$.moreActions.shadowRoot
- .querySelector('span[data-id="unreviewed-change"]'));
- });
- });
-
- suite('quick approve', () => {
- setup(() => {
- element.change = {
- current_revision: 'abc1234',
- };
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {
- values: {
- '-1': '',
- ' 0': '',
- '+1': '',
- },
- },
- },
- permitted_labels: {
- foo: ['-1', ' 0', '+1'],
- },
- };
- flushAsynchronousOperations();
- });
-
- test('added when can approve', () => {
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNotNull(approveButton);
- });
-
- test('hide quick approve', () => {
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNotNull(approveButton);
- assert.isFalse(element._hideQuickApproveAction);
-
- // Assert approve button gets removed from list of buttons.
- element.hideQuickApproveAction();
- flushAsynchronousOperations();
- const approveButtonUpdated =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButtonUpdated);
- assert.isTrue(element._hideQuickApproveAction);
- });
-
- test('is first in list of secondary actions', () => {
- const approveButton = element.$.secondaryActions
- .querySelector('gr-button');
- assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
- });
-
- test('not added when already approved', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {
- approved: {},
- values: {},
- },
- },
- permitted_labels: {
- foo: [' 0', '+1'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('not added when label not permitted', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {values: {}},
- },
- permitted_labels: {
- bar: [],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('approves when tapped', () => {
- const fireActionStub = sandbox.stub(element, '_fireAction');
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']'));
- flushAsynchronousOperations();
- assert.isTrue(fireActionStub.called);
- assert.isTrue(fireActionStub.calledWith('/review'));
- const payload = fireActionStub.lastCall.args[3];
- assert.deepEqual(payload.labels, {foo: '+1'});
- });
-
- test('not added when multiple labels are required', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {values: {}},
- bar: {values: {}},
- },
- permitted_labels: {
- foo: [' 0', '+1'],
- bar: [' 0', '+1', '+2'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('button label for missing approval', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- foo: {
- values: {
- ' 0': '',
- '+1': '',
- },
- },
- bar: {approved: {}, values: {}},
- },
- permitted_labels: {
- foo: [' 0', '+1'],
- bar: [' 0', '+1', '+2'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.equal(approveButton.getAttribute('data-label'), 'foo+1');
- });
-
- test('no quick approve if score is not maximal for a label', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- bar: {
- value: 1,
- values: {
- ' 0': '',
- '+1': '',
- '+2': '',
- },
- },
- },
- permitted_labels: {
- bar: [' 0', '+1'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.isNull(approveButton);
- });
-
- test('approving label with a non-max score', () => {
- element.change = {
- current_revision: 'abc1234',
- labels: {
- bar: {
- value: 1,
- values: {
- ' 0': '',
- '+1': '',
- '+2': '',
- },
- },
- },
- permitted_labels: {
- bar: [' 0', '+1', '+2'],
- },
- };
- flushAsynchronousOperations();
- const approveButton =
- element.shadowRoot
- .querySelector('gr-button[data-action-key=\'review\']');
- assert.equal(approveButton.getAttribute('data-label'), 'bar+2');
- });
- });
-
- test('adds download revision action', () => {
- const handler = sandbox.stub();
- element.addEventListener('download-tap', handler);
- assert.ok(element.revisionActions.download);
- element._handleDownloadTap();
- flushAsynchronousOperations();
-
- assert.isTrue(handler.called);
- });
-
- test('changing changeNum or patchNum does not reload', () => {
- const reloadStub = sandbox.stub(element, 'reload');
- element.changeNum = 123;
- assert.isFalse(reloadStub.called);
- element.latestPatchNum = 456;
- assert.isFalse(reloadStub.called);
- });
-
- test('_toSentenceCase', () => {
- assert.equal(element._toSentenceCase('blah blah'), 'Blah blah');
- assert.equal(element._toSentenceCase('BLAH BLAH'), 'Blah blah');
- assert.equal(element._toSentenceCase('b'), 'B');
- assert.equal(element._toSentenceCase(''), '');
- assert.equal(element._toSentenceCase('!@#$%^&*()'), '!@#$%^&*()');
- });
-
- suite('setActionOverflow', () => {
- test('move action from overflow', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="cherrypick"]'));
- assert.strictEqual(
- element.$.moreActions.items[0].id, 'cherrypick-revision');
- element.setActionOverflow('revision', 'cherrypick', false);
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="cherrypick"]'));
- assert.notEqual(
- element.$.moreActions.items[0].id, 'cherrypick-revision');
- });
-
- test('move action to overflow', () => {
- assert.isOk(element.shadowRoot
- .querySelector('[data-action-key="submit"]'));
- element.setActionOverflow('revision', 'submit', true);
- flushAsynchronousOperations();
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="submit"]'));
- assert.strictEqual(
- element.$.moreActions.items[3].id, 'submit-revision');
- });
-
- suite('_waitForChangeReachable', () => {
- setup(() => {
- sandbox.stub(element, 'async', fn => fn());
- });
-
- const makeGetChange = numTries => () => {
- if (numTries === 1) {
- return Promise.resolve({_number: 123});
- } else {
- numTries--;
- return Promise.resolve(undefined);
- }
- };
-
- test('succeed', () => {
- sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5));
- return element._waitForChangeReachable(123).then(success => {
- assert.isTrue(success);
- });
- });
-
- test('fail', () => {
- sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6));
- return element._waitForChangeReachable(123).then(success => {
- assert.isFalse(success);
- });
- });
- });
- });
-
- suite('_send', () => {
- let cleanup;
- let payload;
- let onShowError;
- let onShowAlert;
- let getResponseObjectStub;
-
- setup(() => {
- cleanup = sinon.stub();
- element.changeNum = 42;
- element.latestPatchNum = 12;
- payload = {foo: 'bar'};
-
- onShowError = sinon.stub();
- element.addEventListener('show-error', onShowError);
- onShowAlert = sinon.stub();
- element.addEventListener('show-alert', onShowAlert);
- });
-
- suite('happy path', () => {
- let sendStub;
- setup(() => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
- sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction')
- .returns(Promise.resolve({}));
- getResponseObjectStub = sandbox.stub(element.$.restAPI,
- 'getResponseObject');
- sandbox.stub(Gerrit.Nav,
- 'navigateToChange').returns(Promise.resolve(true));
- });
-
- test('change action', done => {
- element
- ._send('DELETE', payload, '/endpoint', false, cleanup)
- .then(() => {
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.calledOnce);
- assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
- null, payload));
- done();
- });
- });
-
- suite('show revert submission dialog', () => {
- setup(() => {
- element.change.submission_id = '199';
- element.change.current_revision = '2000';
- sandbox.stub(element.$.restAPI, 'getChanges')
- .returns(Promise.resolve([
- {change_id: '12345678901234', topic: 'T', subject: 'random'},
- {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
- ]));
- });
-
- test('revert submission shows submissionId', done => {
- const expectedMsg = 'Revert submission 199' + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>' + '\n' +
- 'Reverted Changes:' + '\n' +
- '1234567890: random' + '\n' +
- '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' +
- '\n';
- const modifiedMsg = expectedMsg + 'abcd';
- sandbox.stub(element.$.confirmRevertSubmissionDialog,
- '_modifyRevertSubmissionMsg').returns(modifiedMsg);
- element.showRevertSubmissionDialog();
- flush(() => {
- const msg = element.$.confirmRevertSubmissionDialog.message;
- assert.equal(msg, modifiedMsg);
- done();
+ test('send fails', () => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ const sendStub = sandbox.stub(element.$.restAPI,
+ 'executeChangeAction',
+ (num, method, patchNum, endpoint, payload, onErr) => {
+ onErr();
+ return Promise.resolve(null);
});
- });
- });
+ const handleErrorStub = sandbox.stub(element, '_handleResponseError');
- suite('single changes revert', () => {
- let navigateToSearchQueryStub;
- setup(() => {
- getResponseObjectStub
- .returns(Promise.resolve({revert_changes: [
- {change_id: 12345},
- ]}));
- navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
- 'navigateToSearchQuery');
- });
-
- test('revert submission single change', done => {
- element._send('POST', {message: 'Revert submission'},
- '/revert_submission', false, cleanup).then(res => {
- element._handleResponse({__key: 'revert_submission'}, {}).
- then(() => {
- assert.isTrue(navigateToSearchQueryStub.called);
- done();
- });
+ return element._send('DELETE', payload, '/endpoint', true, cleanup)
+ .then(() => {
+ assert.isFalse(onShowError.called);
+ assert.isTrue(cleanup.called);
+ assert.isTrue(sendStub.calledOnce);
+ assert.isTrue(handleErrorStub.called);
});
- });
- });
-
- suite('multiple changes revert', () => {
- let showActionDialogStub;
- let navigateToSearchQueryStub;
- setup(() => {
- getResponseObjectStub
- .returns(Promise.resolve({revert_changes: [
- {change_id: 12345, topic: 'T'},
- {change_id: 23456, topic: 'T'},
- ]}));
- showActionDialogStub = sandbox.stub(element, '_showActionDialog');
- navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav,
- 'navigateToSearchQuery');
- });
-
- test('revert submission multiple change', done => {
- element._send('POST', {message: 'Revert submission'},
- '/revert_submission', false, cleanup).then(res => {
- element._handleResponse({__key: 'revert_submission'}, {}).then(
- () => {
- assert.isFalse(showActionDialogStub.called);
- assert.isTrue(navigateToSearchQueryStub.calledWith(
- 'topic: T'));
- done();
- });
- });
- });
- });
-
- test('revision action', done => {
- element
- ._send('DELETE', payload, '/endpoint', true, cleanup)
- .then(() => {
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.calledOnce);
- assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint',
- 12, payload));
- done();
- });
- });
});
-
- suite('failure modes', () => {
- test('non-latest', () => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: false}));
- const sendStub = sandbox.stub(element.$.restAPI,
- 'executeChangeAction');
-
- return element._send('DELETE', payload, '/endpoint', true, cleanup)
- .then(() => {
- assert.isTrue(onShowAlert.calledOnce);
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.calledOnce);
- assert.isFalse(sendStub.called);
- });
- });
-
- test('send fails', () => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
- const sendStub = sandbox.stub(element.$.restAPI,
- 'executeChangeAction',
- (num, method, patchNum, endpoint, payload, onErr) => {
- onErr();
- return Promise.resolve(null);
- });
- const handleErrorStub = sandbox.stub(element, '_handleResponseError');
-
- return element._send('DELETE', payload, '/endpoint', true, cleanup)
- .then(() => {
- assert.isFalse(onShowError.called);
- assert.isTrue(cleanup.called);
- assert.isTrue(sendStub.calledOnce);
- assert.isTrue(handleErrorStub.called);
- });
- });
- });
- });
-
- test('_handleAction reports', () => {
- sandbox.stub(element, '_fireAction');
- const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
- element._handleAction('type', 'key');
- assert.isTrue(reportStub.called);
- assert.equal(reportStub.lastCall.args[0], 'type-key');
});
});
- suite('getChangeRevisionActions returns only some actions', () => {
- let element;
- let sandbox;
- let changeRevisionActions;
-
- setup(() => {
- stub('gr-rest-api-interface', {
- getChangeRevisionActions() {
- return Promise.resolve(changeRevisionActions);
- },
- send(method, url, payload) {
- return Promise.reject(new Error('error'));
- },
- getProjectConfig() { return Promise.resolve({}); },
- });
-
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
-
- element = fixture('basic');
- // getChangeRevisionActions is not called without
- // set the following properies
- element.change = {};
- element.changeNum = '42';
- element.latestPatchNum = '2';
-
- sandbox.stub(element.$.confirmCherrypick.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
- sandbox.stub(element.$.confirmMove.$.restAPI,
- 'getRepoBranches').returns(Promise.resolve([]));
- return element.reload();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('confirmSubmitDialog and confirmRebase properties are changed', () => {
- changeRevisionActions = {};
- element.reload();
- assert.strictEqual(element.$.confirmSubmitDialog.action, null);
- assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
- });
-
- test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
- const currentRevisionActions = {
- cherrypick: {
- enabled: true,
- label: 'Cherry Pick',
- method: 'POST',
- title: 'cherrypick',
- },
- };
- element._parentIsCurrent = undefined;
- element._updateRebaseAction(currentRevisionActions);
- assert.isTrue(element._parentIsCurrent);
- });
-
- test('_updateRebaseAction', () => {
- const currentRevisionActions = {
- cherrypick: {
- enabled: true,
- label: 'Cherry Pick',
- method: 'POST',
- title: 'cherrypick',
- },
- rebase: {
- enabled: true,
- label: 'Rebase',
- method: 'POST',
- title: 'Rebase onto tip of branch or parent change',
- },
- };
- element._parentIsCurrent = undefined;
-
- // Rebase enabled should always end up true.
- // When rebase is enabled initially, rebaseOnCurrent should be set to
- // true.
- assert.equal(element._updateRebaseAction(currentRevisionActions),
- currentRevisionActions);
-
- assert.isTrue(currentRevisionActions.rebase.enabled);
- assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent);
- assert.isFalse(element._parentIsCurrent);
-
- delete currentRevisionActions.rebase.enabled;
-
- // When rebase is not enabled initially, rebaseOnCurrent should be set to
- // false.
- assert.equal(element._updateRebaseAction(currentRevisionActions),
- currentRevisionActions);
-
- assert.isTrue(currentRevisionActions.rebase.enabled);
- assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent);
- assert.isTrue(element._parentIsCurrent);
- });
+ test('_handleAction reports', () => {
+ sandbox.stub(element, '_fireAction');
+ const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
+ element._handleAction('type', 'key');
+ assert.isTrue(reportStub.called);
+ assert.equal(reportStub.lastCall.args[0], 'type-key');
});
});
+
+ suite('getChangeRevisionActions returns only some actions', () => {
+ let element;
+ let sandbox;
+ let changeRevisionActions;
+
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getChangeRevisionActions() {
+ return Promise.resolve(changeRevisionActions);
+ },
+ send(method, url, payload) {
+ return Promise.reject(new Error('error'));
+ },
+ getProjectConfig() { return Promise.resolve({}); },
+ });
+
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+
+ element = fixture('basic');
+ // getChangeRevisionActions is not called without
+ // set the following properies
+ element.change = {};
+ element.changeNum = '42';
+ element.latestPatchNum = '2';
+
+ sandbox.stub(element.$.confirmCherrypick.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
+ sandbox.stub(element.$.confirmMove.$.restAPI,
+ 'getRepoBranches').returns(Promise.resolve([]));
+ return element.reload();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('confirmSubmitDialog and confirmRebase properties are changed', () => {
+ changeRevisionActions = {};
+ element.reload();
+ assert.strictEqual(element.$.confirmSubmitDialog.action, null);
+ assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
+ });
+
+ test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
+ const currentRevisionActions = {
+ cherrypick: {
+ enabled: true,
+ label: 'Cherry Pick',
+ method: 'POST',
+ title: 'cherrypick',
+ },
+ };
+ element._parentIsCurrent = undefined;
+ element._updateRebaseAction(currentRevisionActions);
+ assert.isTrue(element._parentIsCurrent);
+ });
+
+ test('_updateRebaseAction', () => {
+ const currentRevisionActions = {
+ cherrypick: {
+ enabled: true,
+ label: 'Cherry Pick',
+ method: 'POST',
+ title: 'cherrypick',
+ },
+ rebase: {
+ enabled: true,
+ label: 'Rebase',
+ method: 'POST',
+ title: 'Rebase onto tip of branch or parent change',
+ },
+ };
+ element._parentIsCurrent = undefined;
+
+ // Rebase enabled should always end up true.
+ // When rebase is enabled initially, rebaseOnCurrent should be set to
+ // true.
+ assert.equal(element._updateRebaseAction(currentRevisionActions),
+ currentRevisionActions);
+
+ assert.isTrue(currentRevisionActions.rebase.enabled);
+ assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent);
+ assert.isFalse(element._parentIsCurrent);
+
+ delete currentRevisionActions.rebase.enabled;
+
+ // When rebase is not enabled initially, rebaseOnCurrent should be set to
+ // false.
+ assert.equal(element._updateRebaseAction(currentRevisionActions),
+ currentRevisionActions);
+
+ assert.isTrue(currentRevisionActions.rebase.enabled);
+ assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent);
+ assert.isTrue(element._parentIsCurrent);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 3269dc0..3ee7219 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</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="../../plugins/gr-plugin-host/gr-plugin-host.html">
-<link rel="import" href="gr-change-metadata.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="../../plugins/gr-plugin-host/gr-plugin-host.js"></script>
+<script type="module" src="./gr-change-metadata.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../plugins/gr-plugin-host/gr-plugin-host.js';
+import './gr-change-metadata.js';
+void(0);
+</script>
<test-fixture id="element">
<template>
@@ -42,139 +48,143 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-metadata integration tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../plugins/gr-plugin-host/gr-plugin-host.js';
+import './gr-change-metadata.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-change-metadata integration tests', () => {
+ let sandbox;
+ let element;
- const sectionSelectors = [
- 'section.assignee',
- 'section.strategy',
- 'section.topic',
- ];
+ const sectionSelectors = [
+ 'section.assignee',
+ 'section.strategy',
+ 'section.topic',
+ ];
- const labels = {
- CI: {
- all: [
- {value: 1, name: 'user 2', _account_id: 1},
- {value: 2, name: 'user '},
- ],
- values: {
- ' 0': 'Don\'t submit as-is',
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
+ const labels = {
+ CI: {
+ all: [
+ {value: 1, name: 'user 2', _account_id: 1},
+ {value: 2, name: 'user '},
+ ],
+ values: {
+ ' 0': 'Don\'t submit as-is',
+ '+1': 'No score',
+ '+2': 'Looks good to me',
},
- };
+ },
+ };
- const getStyle = function(selector, name) {
- return window.getComputedStyle(
- Polymer.dom(element.root).querySelector(selector))[name];
- };
+ const getStyle = function(selector, name) {
+ return window.getComputedStyle(
+ dom(element.root).querySelector(selector))[name];
+ };
- function createElement() {
- const element = fixture('element');
- element.change = {labels, status: 'NEW'};
- element.revision = {};
- return element;
- }
+ function createElement() {
+ const element = fixture('element');
+ element.change = {labels, status: 'NEW'};
+ element.revision = {};
+ return element;
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- deleteVote() { return Promise.resolve({ok: true}); },
- });
- });
-
- teardown(() => {
- sandbox.restore();
- Gerrit._testOnly_resetPlugins();
- });
-
- suite('by default', () => {
- setup(done => {
- element = createElement();
- flush(done);
- });
-
- for (const sectionSelector of sectionSelectors) {
- test(sectionSelector + ' does not have display: none', () => {
- assert.notEqual(getStyle(sectionSelector, 'display'), 'none');
- });
- }
- });
-
- suite('with plugin style', () => {
- setup(done => {
- Gerrit._testOnly_resetPlugins();
- const pluginHost = fixture('plugin-host');
- pluginHost.config = {
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html?' + Math.random(),
- window.location.href).toString(),
- ],
- },
- };
- element = createElement();
- const importSpy = sandbox.spy(element.$.externalStyle, '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues).then(() => {
- flush(done);
- });
- });
- });
-
- for (const sectionSelector of sectionSelectors) {
- test(sectionSelector + ' may have display: none', () => {
- assert.equal(getStyle(sectionSelector, 'display'), 'none');
- });
- }
- });
-
- suite('label updates', () => {
- let plugin;
-
- setup(() => {
- Gerrit.install(p => plugin = p, '0.1',
- new URL('test/plugin.html?' + Math.random(),
- window.location.href).toString());
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
- Gerrit._loadPlugins([]);
- element = createElement();
- });
-
- test('labels changed callback', done => {
- let callCount = 0;
- const labelChangeSpy = sandbox.spy(arg => {
- callCount++;
- if (callCount === 1) {
- assert.deepEqual(arg, labels);
- assert.equal(arg.CI.all.length, 2);
- element.set(['change', 'labels'], {
- CI: {
- all: [
- {value: 1, name: 'user 2', _account_id: 1},
- ],
- values: {
- ' 0': 'Don\'t submit as-is',
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
- },
- });
- } else if (callCount === 2) {
- assert.equal(arg.CI.all.length, 1);
- done();
- }
- });
-
- plugin.changeMetadata().onLabelsChanged(labelChangeSpy);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ deleteVote() { return Promise.resolve({ok: true}); },
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ suite('by default', () => {
+ setup(done => {
+ element = createElement();
+ flush(done);
+ });
+
+ for (const sectionSelector of sectionSelectors) {
+ test(sectionSelector + ' does not have display: none', () => {
+ assert.notEqual(getStyle(sectionSelector, 'display'), 'none');
+ });
+ }
+ });
+
+ suite('with plugin style', () => {
+ setup(done => {
+ Gerrit._testOnly_resetPlugins();
+ const pluginHost = fixture('plugin-host');
+ pluginHost.config = {
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString(),
+ ],
+ },
+ };
+ element = createElement();
+ const importSpy = sandbox.spy(element.$.externalStyle, '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues).then(() => {
+ flush(done);
+ });
+ });
+ });
+
+ for (const sectionSelector of sectionSelectors) {
+ test(sectionSelector + ' may have display: none', () => {
+ assert.equal(getStyle(sectionSelector, 'display'), 'none');
+ });
+ }
+ });
+
+ suite('label updates', () => {
+ let plugin;
+
+ setup(() => {
+ Gerrit.install(p => plugin = p, '0.1',
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString());
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._loadPlugins([]);
+ element = createElement();
+ });
+
+ test('labels changed callback', done => {
+ let callCount = 0;
+ const labelChangeSpy = sandbox.spy(arg => {
+ callCount++;
+ if (callCount === 1) {
+ assert.deepEqual(arg, labels);
+ assert.equal(arg.CI.all.length, 2);
+ element.set(['change', 'labels'], {
+ CI: {
+ all: [
+ {value: 1, name: 'user 2', _account_id: 1},
+ ],
+ values: {
+ ' 0': 'Don\'t submit as-is',
+ '+1': 'No score',
+ '+2': 'Looks good to me',
+ },
+ },
+ });
+ } else if (callCount === 2) {
+ assert.equal(arg.CI.all.length, 1);
+ done();
+ }
+ });
+
+ plugin.changeMetadata().onLabelsChanged(labelChangeSpy);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index ea11514..2e9cdf9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -14,493 +14,524 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-change-metadata-shared-styles.js';
+import '../../../styles/gr-change-view-integration-shared-styles.js';
+import '../../../styles/gr-voting-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../plugins/gr-external-style/gr-external-style.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-editable-label/gr-editable-label.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-limited-text/gr-limited-text.js';
+import '../../shared/gr-linked-chip/gr-linked-chip.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-change-requirements/gr-change-requirements.js';
+import '../gr-commit-info/gr-commit-info.js';
+import '../gr-reviewer-list/gr-reviewer-list.js';
+import '../../shared/gr-account-list/gr-account-list.js';
+import '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.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-change-metadata_html.js';
- const SubmitTypeLabel = {
- FAST_FORWARD_ONLY: 'Fast Forward Only',
- MERGE_IF_NECESSARY: 'Merge if Necessary',
- REBASE_IF_NECESSARY: 'Rebase if Necessary',
- MERGE_ALWAYS: 'Always Merge',
- REBASE_ALWAYS: 'Rebase Always',
- CHERRY_PICK: 'Cherry Pick',
- };
+const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
- const NOT_CURRENT_MESSAGE = 'Not current - rebase possible';
+const SubmitTypeLabel = {
+ FAST_FORWARD_ONLY: 'Fast Forward Only',
+ MERGE_IF_NECESSARY: 'Merge if Necessary',
+ REBASE_IF_NECESSARY: 'Rebase if Necessary',
+ MERGE_ALWAYS: 'Always Merge',
+ REBASE_ALWAYS: 'Rebase Always',
+ CHERRY_PICK: 'Cherry Pick',
+};
+const NOT_CURRENT_MESSAGE = 'Not current - rebase possible';
+
+/**
+ * @enum {string}
+ */
+const CertificateStatus = {
/**
- * @enum {string}
+ * This certificate status is bad.
*/
- const CertificateStatus = {
- /**
- * This certificate status is bad.
- */
- BAD: 'BAD',
- /**
- * This certificate status is OK.
- */
- OK: 'OK',
- /**
- * This certificate status is TRUSTED.
- */
- TRUSTED: 'TRUSTED',
- };
-
+ BAD: 'BAD',
/**
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * This certificate status is OK.
*/
- class GrChangeMetadata extends Polymer.mixinBehaviors( [
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-metadata'; }
- /**
- * Fired when the change topic is changed.
- *
- * @event topic-changed
- */
+ OK: 'OK',
+ /**
+ * This certificate status is TRUSTED.
+ */
+ TRUSTED: 'TRUSTED',
+};
- static get properties() {
- return {
+/**
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeMetadata extends mixinBehaviors( [
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-metadata'; }
+ /**
+ * Fired when the change topic is changed.
+ *
+ * @event topic-changed
+ */
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: Object,
+ labels: {
+ type: Object,
+ notify: true,
+ },
+ account: Object,
/** @type {?} */
- change: Object,
- labels: {
- type: Object,
- notify: true,
+ revision: Object,
+ commitInfo: Object,
+ _mutable: {
+ type: Boolean,
+ computed: '_computeIsMutable(account)',
+ },
+ /** @type {?} */
+ serverConfig: Object,
+ parentIsCurrent: Boolean,
+ _notCurrentMessage: {
+ type: String,
+ value: NOT_CURRENT_MESSAGE,
+ readOnly: true,
+ },
+ _topicReadOnly: {
+ type: Boolean,
+ computed: '_computeTopicReadOnly(_mutable, change)',
+ },
+ _hashtagReadOnly: {
+ type: Boolean,
+ computed: '_computeHashtagReadOnly(_mutable, change)',
+ },
+ /**
+ * @type {Gerrit.PushCertificateValidation}
+ */
+ _pushCertificateValidation: {
+ type: Object,
+ computed: '_computePushCertificateValidation(serverConfig, change)',
+ },
+ _showRequirements: {
+ type: Boolean,
+ computed: '_computeShowRequirements(change)',
+ },
+
+ _assignee: Array,
+ _isWip: {
+ type: Boolean,
+ computed: '_computeIsWip(change)',
+ },
+ _newHashtag: String,
+
+ _settingTopic: {
+ type: Boolean,
+ value: false,
+ },
+
+ _currentParents: {
+ type: Array,
+ computed: '_computeParents(change)',
+ },
+
+ /** @type {?} */
+ _CHANGE_ROLE: {
+ type: Object,
+ readOnly: true,
+ value: {
+ OWNER: 'owner',
+ UPLOADER: 'uploader',
+ AUTHOR: 'author',
+ COMMITTER: 'committer',
},
- account: Object,
- /** @type {?} */
- revision: Object,
- commitInfo: Object,
- _mutable: {
- type: Boolean,
- computed: '_computeIsMutable(account)',
- },
- /** @type {?} */
- serverConfig: Object,
- parentIsCurrent: Boolean,
- _notCurrentMessage: {
- type: String,
- value: NOT_CURRENT_MESSAGE,
- readOnly: true,
- },
- _topicReadOnly: {
- type: Boolean,
- computed: '_computeTopicReadOnly(_mutable, change)',
- },
- _hashtagReadOnly: {
- type: Boolean,
- computed: '_computeHashtagReadOnly(_mutable, change)',
- },
- /**
- * @type {Gerrit.PushCertificateValidation}
- */
- _pushCertificateValidation: {
- type: Object,
- computed: '_computePushCertificateValidation(serverConfig, change)',
- },
- _showRequirements: {
- type: Boolean,
- computed: '_computeShowRequirements(change)',
- },
+ },
+ };
+ }
- _assignee: Array,
- _isWip: {
- type: Boolean,
- computed: '_computeIsWip(change)',
- },
- _newHashtag: String,
+ static get observers() {
+ return [
+ '_changeChanged(change)',
+ '_labelsChanged(change.labels)',
+ '_assigneeChanged(_assignee.*)',
+ ];
+ }
- _settingTopic: {
- type: Boolean,
- value: false,
- },
+ _labelsChanged(labels) {
+ this.labels = Object.assign({}, labels) || null;
+ }
- _currentParents: {
- type: Array,
- computed: '_computeParents(change)',
- },
+ _changeChanged(change) {
+ this._assignee = change.assignee ? [change.assignee] : [];
+ }
- /** @type {?} */
- _CHANGE_ROLE: {
- type: Object,
- readOnly: true,
- value: {
- OWNER: 'owner',
- UPLOADER: 'uploader',
- AUTHOR: 'author',
- COMMITTER: 'committer',
- },
- },
- };
- }
-
- static get observers() {
- return [
- '_changeChanged(change)',
- '_labelsChanged(change.labels)',
- '_assigneeChanged(_assignee.*)',
- ];
- }
-
- _labelsChanged(labels) {
- this.labels = Object.assign({}, labels) || null;
- }
-
- _changeChanged(change) {
- this._assignee = change.assignee ? [change.assignee] : [];
- }
-
- _assigneeChanged(assigneeRecord) {
- if (!this.change) { return; }
- const assignee = assigneeRecord.base;
- if (assignee.length) {
- const acct = assignee[0];
- if (this.change.assignee &&
- acct._account_id === this.change.assignee._account_id) { return; }
- this.set(['change', 'assignee'], acct);
- this.$.restAPI.setAssignee(this.change._number, acct._account_id);
- } else {
- if (!this.change.assignee) { return; }
- this.set(['change', 'assignee'], undefined);
- this.$.restAPI.deleteAssignee(this.change._number);
- }
- }
-
- _computeHideStrategy(change) {
- return !this.changeIsOpen(change);
- }
-
- /**
- * @param {Object} commitInfo
- * @return {?Array} If array is empty, returns null instead so
- * an existential check can be used to hide or show the webLinks
- * section.
- */
- _computeWebLinks(commitInfo, serverConfig) {
- if (!commitInfo) { return null; }
- const weblinks = Gerrit.Nav.getChangeWeblinks(
- this.change ? this.change.repo : '',
- commitInfo.commit,
- {
- weblinks: commitInfo.web_links,
- config: serverConfig,
- });
- return weblinks.length ? weblinks : null;
- }
-
- _computeStrategy(change) {
- return SubmitTypeLabel[change.submit_type];
- }
-
- _computeLabelNames(labels) {
- return Object.keys(labels).sort();
- }
-
- _handleTopicChanged(e, topic) {
- const lastTopic = this.change.topic;
- if (!topic.length) { topic = null; }
- this._settingTopic = true;
- this.$.restAPI.setChangeTopic(this.change._number, topic)
- .then(newTopic => {
- this._settingTopic = false;
- this.set(['change', 'topic'], newTopic);
- if (newTopic !== lastTopic) {
- this.dispatchEvent(new CustomEvent(
- 'topic-changed', {bubbles: true, composed: true}));
- }
- });
- }
-
- _showAddTopic(changeRecord, settingTopic) {
- const hasTopic = !!changeRecord &&
- !!changeRecord.base && !!changeRecord.base.topic;
- return !hasTopic && !settingTopic;
- }
-
- _showTopicChip(changeRecord, settingTopic) {
- const hasTopic = !!changeRecord &&
- !!changeRecord.base && !!changeRecord.base.topic;
- return hasTopic && !settingTopic;
- }
-
- _showCherryPickOf(changeRecord) {
- const hasCherryPickOf = !!changeRecord &&
- !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change &&
- !!changeRecord.base.cherry_pick_of_patch_set;
- return hasCherryPickOf;
- }
-
- _handleHashtagChanged(e) {
- const lastHashtag = this.change.hashtag;
- if (!this._newHashtag.length) { return; }
- const newHashtag = this._newHashtag;
- this._newHashtag = '';
- this.$.restAPI.setChangeHashtag(
- this.change._number, {add: [newHashtag]}).then(newHashtag => {
- this.set(['change', 'hashtags'], newHashtag);
- if (newHashtag !== lastHashtag) {
- this.dispatchEvent(
- new CustomEvent('hashtag-changed', {
- bubbles: true, composed: true}));
- }
- });
- }
-
- _computeTopicReadOnly(mutable, change) {
- return !mutable ||
- !change ||
- !change.actions ||
- !change.actions.topic ||
- !change.actions.topic.enabled;
- }
-
- _computeHashtagReadOnly(mutable, change) {
- return !mutable ||
- !change ||
- !change.actions ||
- !change.actions.hashtags ||
- !change.actions.hashtags.enabled;
- }
-
- _computeAssigneeReadOnly(mutable, change) {
- return !mutable ||
- !change ||
- !change.actions ||
- !change.actions.assignee ||
- !change.actions.assignee.enabled;
- }
-
- _computeTopicPlaceholder(_topicReadOnly) {
- // Action items in Material Design are uppercase -- placeholder label text
- // is sentence case.
- return _topicReadOnly ? 'No topic' : 'ADD TOPIC';
- }
-
- _computeHashtagPlaceholder(_hashtagReadOnly) {
- return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE;
- }
-
- _computeShowRequirements(change) {
- if (change.status !== this.ChangeStatus.NEW) {
- // TODO(maximeg) change this to display the stored
- // requirements, once it is implemented server-side.
- return false;
- }
- const hasRequirements = !!change.requirements &&
- Object.keys(change.requirements).length > 0;
- const hasLabels = !!change.labels &&
- Object.keys(change.labels).length > 0;
- return hasRequirements || hasLabels || !!change.work_in_progress;
- }
-
- /**
- * @return {?Gerrit.PushCertificateValidation} object representing data for
- * the push validation.
- */
- _computePushCertificateValidation(serverConfig, change) {
- if (!change || !serverConfig || !serverConfig.receive ||
- !serverConfig.receive.enable_signed_push) {
- return null;
- }
- const rev = change.revisions[change.current_revision];
- if (!rev.push_certificate || !rev.push_certificate.key) {
- return {
- class: 'help',
- icon: 'gr-icons:help',
- message: 'This patch set was created without a push certificate',
- };
- }
-
- const key = rev.push_certificate.key;
- switch (key.status) {
- case CertificateStatus.BAD:
- return {
- class: 'invalid',
- icon: 'gr-icons:close',
- message: this._problems('Push certificate is invalid', key),
- };
- case CertificateStatus.OK:
- return {
- class: 'notTrusted',
- icon: 'gr-icons:info',
- message: this._problems(
- 'Push certificate is valid, but key is not trusted', key),
- };
- case CertificateStatus.TRUSTED:
- return {
- class: 'trusted',
- icon: 'gr-icons:check',
- message: this._problems(
- 'Push certificate is valid and key is trusted', key),
- };
- default:
- throw new Error(`unknown certificate status: ${key.status}`);
- }
- }
-
- _problems(msg, key) {
- if (!key || !key.problems || key.problems.length === 0) {
- return msg;
- }
-
- return [msg + ':'].concat(key.problems).join('\n');
- }
-
- _computeShowRepoBranchTogether(repo, branch) {
- return !!repo && !!branch && repo.length + branch.length < 40;
- }
-
- _computeProjectUrl(project) {
- return Gerrit.Nav.getUrlForProjectChanges(project);
- }
-
- _computeBranchUrl(project, branch) {
- if (!this.change || !this.change.status) return '';
- return Gerrit.Nav.getUrlForBranch(branch, project,
- this.change.status == this.ChangeStatus.NEW ? 'open' :
- this.change.status.toLowerCase());
- }
-
- _computeCherryPickOfUrl(change, patchset, project) {
- return Gerrit.Nav.getUrlForChangeById(change, project, patchset);
- }
-
- _computeTopicUrl(topic) {
- return Gerrit.Nav.getUrlForTopic(topic);
- }
-
- _computeHashtagUrl(hashtag) {
- return Gerrit.Nav.getUrlForHashtag(hashtag);
- }
-
- _handleTopicRemoved(e) {
- const target = Polymer.dom(e).rootTarget;
- target.disabled = true;
- this.$.restAPI.setChangeTopic(this.change._number, null)
- .then(() => {
- target.disabled = false;
- this.set(['change', 'topic'], '');
- this.dispatchEvent(
- new CustomEvent('topic-changed',
- {bubbles: true, composed: true}));
- })
- .catch(err => {
- target.disabled = false;
- return;
- });
- }
-
- _handleHashtagRemoved(e) {
- e.preventDefault();
- const target = Polymer.dom(e).rootTarget;
- target.disabled = true;
- this.$.restAPI.setChangeHashtag(this.change._number,
- {remove: [target.text]})
- .then(newHashtag => {
- target.disabled = false;
- this.set(['change', 'hashtags'], newHashtag);
- })
- .catch(err => {
- target.disabled = false;
- return;
- });
- }
-
- _computeIsWip(change) {
- return !!change.work_in_progress;
- }
-
- _computeShowRoleClass(change, role) {
- return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay';
- }
-
- /**
- * Get the user with the specified role on the change. Returns null if the
- * user with that role is the same as the owner.
- *
- * @param {!Object} change
- * @param {string} role One of the values from _CHANGE_ROLE
- * @return {Object|null} either an accound or null.
- */
- _getNonOwnerRole(change, role) {
- if (!change || !change.current_revision ||
- !change.revisions[change.current_revision]) {
- return null;
- }
-
- const rev = change.revisions[change.current_revision];
- if (!rev) { return null; }
-
- if (role === this._CHANGE_ROLE.UPLOADER &&
- rev.uploader &&
- change.owner._account_id !== rev.uploader._account_id) {
- return rev.uploader;
- }
-
- if (role === this._CHANGE_ROLE.AUTHOR &&
- rev.commit && rev.commit.author &&
- change.owner.email !== rev.commit.author.email) {
- return rev.commit.author;
- }
-
- if (role === this._CHANGE_ROLE.COMMITTER &&
- rev.commit && rev.commit.committer &&
- change.owner.email !== rev.commit.committer.email) {
- return rev.commit.committer;
- }
-
- return null;
- }
-
- _computeParents(change) {
- if (!change || !change.current_revision ||
- !change.revisions[change.current_revision] ||
- !change.revisions[change.current_revision].commit) {
- return undefined;
- }
- return change.revisions[change.current_revision].commit.parents;
- }
-
- _computeParentsLabel(parents) {
- return parents && parents.length > 1 ? 'Parents' : 'Parent';
- }
-
- _computeParentListClass(parents, parentIsCurrent) {
- // Undefined check for polymer 2
- if (parents === undefined || parentIsCurrent === undefined) {
- return '';
- }
-
- return [
- 'parentList',
- parents && parents.length > 1 ? 'merge' : 'nonMerge',
- parentIsCurrent ? 'current' : 'notCurrent',
- ].join(' ');
- }
-
- _computeIsMutable(account) {
- return !!Object.keys(account).length;
- }
-
- editTopic() {
- if (this._topicReadOnly || this.change.topic) { return; }
- // Cannot use `this.$.ID` syntax because the element exists inside of a
- // dom-if.
- this.shadowRoot.querySelector('.topicEditableLabel').open();
- }
-
- _getReviewerSuggestionsProvider(change) {
- const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
- change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
- provider.init();
- return provider;
+ _assigneeChanged(assigneeRecord) {
+ if (!this.change) { return; }
+ const assignee = assigneeRecord.base;
+ if (assignee.length) {
+ const acct = assignee[0];
+ if (this.change.assignee &&
+ acct._account_id === this.change.assignee._account_id) { return; }
+ this.set(['change', 'assignee'], acct);
+ this.$.restAPI.setAssignee(this.change._number, acct._account_id);
+ } else {
+ if (!this.change.assignee) { return; }
+ this.set(['change', 'assignee'], undefined);
+ this.$.restAPI.deleteAssignee(this.change._number);
}
}
- customElements.define(GrChangeMetadata.is, GrChangeMetadata);
-})();
+ _computeHideStrategy(change) {
+ return !this.changeIsOpen(change);
+ }
+
+ /**
+ * @param {Object} commitInfo
+ * @return {?Array} If array is empty, returns null instead so
+ * an existential check can be used to hide or show the webLinks
+ * section.
+ */
+ _computeWebLinks(commitInfo, serverConfig) {
+ if (!commitInfo) { return null; }
+ const weblinks = Gerrit.Nav.getChangeWeblinks(
+ this.change ? this.change.repo : '',
+ commitInfo.commit,
+ {
+ weblinks: commitInfo.web_links,
+ config: serverConfig,
+ });
+ return weblinks.length ? weblinks : null;
+ }
+
+ _computeStrategy(change) {
+ return SubmitTypeLabel[change.submit_type];
+ }
+
+ _computeLabelNames(labels) {
+ return Object.keys(labels).sort();
+ }
+
+ _handleTopicChanged(e, topic) {
+ const lastTopic = this.change.topic;
+ if (!topic.length) { topic = null; }
+ this._settingTopic = true;
+ this.$.restAPI.setChangeTopic(this.change._number, topic)
+ .then(newTopic => {
+ this._settingTopic = false;
+ this.set(['change', 'topic'], newTopic);
+ if (newTopic !== lastTopic) {
+ this.dispatchEvent(new CustomEvent(
+ 'topic-changed', {bubbles: true, composed: true}));
+ }
+ });
+ }
+
+ _showAddTopic(changeRecord, settingTopic) {
+ const hasTopic = !!changeRecord &&
+ !!changeRecord.base && !!changeRecord.base.topic;
+ return !hasTopic && !settingTopic;
+ }
+
+ _showTopicChip(changeRecord, settingTopic) {
+ const hasTopic = !!changeRecord &&
+ !!changeRecord.base && !!changeRecord.base.topic;
+ return hasTopic && !settingTopic;
+ }
+
+ _showCherryPickOf(changeRecord) {
+ const hasCherryPickOf = !!changeRecord &&
+ !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change &&
+ !!changeRecord.base.cherry_pick_of_patch_set;
+ return hasCherryPickOf;
+ }
+
+ _handleHashtagChanged(e) {
+ const lastHashtag = this.change.hashtag;
+ if (!this._newHashtag.length) { return; }
+ const newHashtag = this._newHashtag;
+ this._newHashtag = '';
+ this.$.restAPI.setChangeHashtag(
+ this.change._number, {add: [newHashtag]}).then(newHashtag => {
+ this.set(['change', 'hashtags'], newHashtag);
+ if (newHashtag !== lastHashtag) {
+ this.dispatchEvent(
+ new CustomEvent('hashtag-changed', {
+ bubbles: true, composed: true}));
+ }
+ });
+ }
+
+ _computeTopicReadOnly(mutable, change) {
+ return !mutable ||
+ !change ||
+ !change.actions ||
+ !change.actions.topic ||
+ !change.actions.topic.enabled;
+ }
+
+ _computeHashtagReadOnly(mutable, change) {
+ return !mutable ||
+ !change ||
+ !change.actions ||
+ !change.actions.hashtags ||
+ !change.actions.hashtags.enabled;
+ }
+
+ _computeAssigneeReadOnly(mutable, change) {
+ return !mutable ||
+ !change ||
+ !change.actions ||
+ !change.actions.assignee ||
+ !change.actions.assignee.enabled;
+ }
+
+ _computeTopicPlaceholder(_topicReadOnly) {
+ // Action items in Material Design are uppercase -- placeholder label text
+ // is sentence case.
+ return _topicReadOnly ? 'No topic' : 'ADD TOPIC';
+ }
+
+ _computeHashtagPlaceholder(_hashtagReadOnly) {
+ return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE;
+ }
+
+ _computeShowRequirements(change) {
+ if (change.status !== this.ChangeStatus.NEW) {
+ // TODO(maximeg) change this to display the stored
+ // requirements, once it is implemented server-side.
+ return false;
+ }
+ const hasRequirements = !!change.requirements &&
+ Object.keys(change.requirements).length > 0;
+ const hasLabels = !!change.labels &&
+ Object.keys(change.labels).length > 0;
+ return hasRequirements || hasLabels || !!change.work_in_progress;
+ }
+
+ /**
+ * @return {?Gerrit.PushCertificateValidation} object representing data for
+ * the push validation.
+ */
+ _computePushCertificateValidation(serverConfig, change) {
+ if (!change || !serverConfig || !serverConfig.receive ||
+ !serverConfig.receive.enable_signed_push) {
+ return null;
+ }
+ const rev = change.revisions[change.current_revision];
+ if (!rev.push_certificate || !rev.push_certificate.key) {
+ return {
+ class: 'help',
+ icon: 'gr-icons:help',
+ message: 'This patch set was created without a push certificate',
+ };
+ }
+
+ const key = rev.push_certificate.key;
+ switch (key.status) {
+ case CertificateStatus.BAD:
+ return {
+ class: 'invalid',
+ icon: 'gr-icons:close',
+ message: this._problems('Push certificate is invalid', key),
+ };
+ case CertificateStatus.OK:
+ return {
+ class: 'notTrusted',
+ icon: 'gr-icons:info',
+ message: this._problems(
+ 'Push certificate is valid, but key is not trusted', key),
+ };
+ case CertificateStatus.TRUSTED:
+ return {
+ class: 'trusted',
+ icon: 'gr-icons:check',
+ message: this._problems(
+ 'Push certificate is valid and key is trusted', key),
+ };
+ default:
+ throw new Error(`unknown certificate status: ${key.status}`);
+ }
+ }
+
+ _problems(msg, key) {
+ if (!key || !key.problems || key.problems.length === 0) {
+ return msg;
+ }
+
+ return [msg + ':'].concat(key.problems).join('\n');
+ }
+
+ _computeShowRepoBranchTogether(repo, branch) {
+ return !!repo && !!branch && repo.length + branch.length < 40;
+ }
+
+ _computeProjectUrl(project) {
+ return Gerrit.Nav.getUrlForProjectChanges(project);
+ }
+
+ _computeBranchUrl(project, branch) {
+ if (!this.change || !this.change.status) return '';
+ return Gerrit.Nav.getUrlForBranch(branch, project,
+ this.change.status == this.ChangeStatus.NEW ? 'open' :
+ this.change.status.toLowerCase());
+ }
+
+ _computeCherryPickOfUrl(change, patchset, project) {
+ return Gerrit.Nav.getUrlForChangeById(change, project, patchset);
+ }
+
+ _computeTopicUrl(topic) {
+ return Gerrit.Nav.getUrlForTopic(topic);
+ }
+
+ _computeHashtagUrl(hashtag) {
+ return Gerrit.Nav.getUrlForHashtag(hashtag);
+ }
+
+ _handleTopicRemoved(e) {
+ const target = dom(e).rootTarget;
+ target.disabled = true;
+ this.$.restAPI.setChangeTopic(this.change._number, null)
+ .then(() => {
+ target.disabled = false;
+ this.set(['change', 'topic'], '');
+ this.dispatchEvent(
+ new CustomEvent('topic-changed',
+ {bubbles: true, composed: true}));
+ })
+ .catch(err => {
+ target.disabled = false;
+ return;
+ });
+ }
+
+ _handleHashtagRemoved(e) {
+ e.preventDefault();
+ const target = dom(e).rootTarget;
+ target.disabled = true;
+ this.$.restAPI.setChangeHashtag(this.change._number,
+ {remove: [target.text]})
+ .then(newHashtag => {
+ target.disabled = false;
+ this.set(['change', 'hashtags'], newHashtag);
+ })
+ .catch(err => {
+ target.disabled = false;
+ return;
+ });
+ }
+
+ _computeIsWip(change) {
+ return !!change.work_in_progress;
+ }
+
+ _computeShowRoleClass(change, role) {
+ return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay';
+ }
+
+ /**
+ * Get the user with the specified role on the change. Returns null if the
+ * user with that role is the same as the owner.
+ *
+ * @param {!Object} change
+ * @param {string} role One of the values from _CHANGE_ROLE
+ * @return {Object|null} either an accound or null.
+ */
+ _getNonOwnerRole(change, role) {
+ if (!change || !change.current_revision ||
+ !change.revisions[change.current_revision]) {
+ return null;
+ }
+
+ const rev = change.revisions[change.current_revision];
+ if (!rev) { return null; }
+
+ if (role === this._CHANGE_ROLE.UPLOADER &&
+ rev.uploader &&
+ change.owner._account_id !== rev.uploader._account_id) {
+ return rev.uploader;
+ }
+
+ if (role === this._CHANGE_ROLE.AUTHOR &&
+ rev.commit && rev.commit.author &&
+ change.owner.email !== rev.commit.author.email) {
+ return rev.commit.author;
+ }
+
+ if (role === this._CHANGE_ROLE.COMMITTER &&
+ rev.commit && rev.commit.committer &&
+ change.owner.email !== rev.commit.committer.email) {
+ return rev.commit.committer;
+ }
+
+ return null;
+ }
+
+ _computeParents(change) {
+ if (!change || !change.current_revision ||
+ !change.revisions[change.current_revision] ||
+ !change.revisions[change.current_revision].commit) {
+ return undefined;
+ }
+ return change.revisions[change.current_revision].commit.parents;
+ }
+
+ _computeParentsLabel(parents) {
+ return parents && parents.length > 1 ? 'Parents' : 'Parent';
+ }
+
+ _computeParentListClass(parents, parentIsCurrent) {
+ // Undefined check for polymer 2
+ if (parents === undefined || parentIsCurrent === undefined) {
+ return '';
+ }
+
+ return [
+ 'parentList',
+ parents && parents.length > 1 ? 'merge' : 'nonMerge',
+ parentIsCurrent ? 'current' : 'notCurrent',
+ ].join(' ');
+ }
+
+ _computeIsMutable(account) {
+ return !!Object.keys(account).length;
+ }
+
+ editTopic() {
+ if (this._topicReadOnly || this.change.topic) { return; }
+ // Cannot use `this.$.ID` syntax because the element exists inside of a
+ // dom-if.
+ this.shadowRoot.querySelector('.topicEditableLabel').open();
+ }
+
+ _getReviewerSuggestionsProvider(change) {
+ const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+ change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
+ provider.init();
+ return provider;
+ }
+}
+
+customElements.define(GrChangeMetadata.is, GrChangeMetadata);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
index ad3b621..786a118 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
@@ -1,48 +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/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-change-metadata-shared-styles.html">
-<link rel="import" href="../../../styles/gr-change-view-integration-shared-styles.html">
-<link rel="import" href="../../../styles/gr-voting-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../plugins/gr-external-style/gr-external-style.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-change-requirements/gr-change-requirements.html">
-<link rel="import" href="../gr-commit-info/gr-commit-info.html">
-<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
-<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
-<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
-
-<dom-module id="gr-change-metadata">
- <template>
+export const htmlTemplate = html`
<style include="gr-change-metadata-shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -128,9 +102,7 @@
<section>
<span class="title">Updated</span>
<span class="value">
- <gr-date-formatter
- has-tooltip
- date-str="[[change.updated]]"></gr-date-formatter>
+ <gr-date-formatter has-tooltip="" date-str="[[change.updated]]"></gr-date-formatter>
</span>
</section>
<section>
@@ -138,92 +110,65 @@
<span class="value">
<gr-account-link account="[[change.owner]]"></gr-account-link>
<template is="dom-if" if="[[_pushCertificateValidation]]">
- <gr-tooltip-content
- has-tooltip
- title$="[[_pushCertificateValidation.message]]">
- <iron-icon
- class$="icon [[_pushCertificateValidation.class]]"
- icon="[[_pushCertificateValidation.icon]]">
+ <gr-tooltip-content has-tooltip="" title\$="[[_pushCertificateValidation.message]]">
+ <iron-icon class\$="icon [[_pushCertificateValidation.class]]" icon="[[_pushCertificateValidation.icon]]">
</iron-icon>
</gr-tooltip-content>
</template>
</span>
</section>
- <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.UPLOADER)]]">
+ <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.UPLOADER)]]">
<span class="title">Uploader</span>
<span class="value">
- <gr-account-link
- account="[[_getNonOwnerRole(change, _CHANGE_ROLE.UPLOADER)]]"
- ></gr-account-link>
+ <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.UPLOADER)]]"></gr-account-link>
</span>
</section>
- <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.AUTHOR)]]">
+ <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.AUTHOR)]]">
<span class="title">Author</span>
<span class="value">
- <gr-account-link
- account="[[_getNonOwnerRole(change, _CHANGE_ROLE.AUTHOR)]]"
- ></gr-account-link>
+ <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.AUTHOR)]]"></gr-account-link>
</span>
</section>
- <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.COMMITTER)]]">
+ <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.COMMITTER)]]">
<span class="title">Committer</span>
<span class="value">
- <gr-account-link
- account="[[_getNonOwnerRole(change, _CHANGE_ROLE.COMMITTER)]]"
- ></gr-account-link>
+ <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.COMMITTER)]]"></gr-account-link>
</span>
</section>
<section class="assignee">
<span class="title">Assignee</span>
<span class="value">
- <gr-account-list
- id="assigneeValue"
- placeholder="Set assignee..."
- max-count="1"
- skip-suggest-on-empty
- accounts="{{_assignee}}"
- readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
- suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
+ <gr-account-list id="assigneeValue" placeholder="Set assignee..." max-count="1" skip-suggest-on-empty="" accounts="{{_assignee}}" readonly="[[_computeAssigneeReadOnly(_mutable, change)]]" suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
</gr-account-list>
</span>
</section>
<section>
<span class="title">Reviewers</span>
<span class="value">
- <gr-reviewer-list
- change="{{change}}"
- mutable="[[_mutable]]"
- reviewers-only
- max-reviewers-displayed="3"></gr-reviewer-list>
+ <gr-reviewer-list change="{{change}}" mutable="[[_mutable]]" reviewers-only="" max-reviewers-displayed="3"></gr-reviewer-list>
</span>
</section>
<section>
<span class="title">CC</span>
<span class="value">
- <gr-reviewer-list
- change="{{change}}"
- mutable="[[_mutable]]"
- ccs-only
- max-reviewers-displayed="3"></gr-reviewer-list>
+ <gr-reviewer-list change="{{change}}" mutable="[[_mutable]]" ccs-only="" max-reviewers-displayed="3"></gr-reviewer-list>
</span>
</section>
- <template is="dom-if"
- if="[[_computeShowRepoBranchTogether(change.project, change.branch)]]">
+ <template is="dom-if" if="[[_computeShowRepoBranchTogether(change.project, change.branch)]]">
<section>
<span class="title">Repo / Branch</span>
<span class="value">
- <a href$="[[_computeProjectUrl(change.project)]]">[[change.project]]</a>
+ <a href\$="[[_computeProjectUrl(change.project)]]">[[change.project]]</a>
/
- <a href$="[[_computeBranchUrl(change.project, change.branch)]]">[[change.branch]]</a>
+ <a href\$="[[_computeBranchUrl(change.project, change.branch)]]">[[change.branch]]</a>
</span>
</section>
</template>
- <template is="dom-if"
- if="[[!_computeShowRepoBranchTogether(change.project, change.branch)]]">
+ <template is="dom-if" if="[[!_computeShowRepoBranchTogether(change.project, change.branch)]]">
<section>
<span class="title">Repo</span>
<span class="value">
- <a href$="[[_computeProjectUrl(change.project)]]">
+ <a href\$="[[_computeProjectUrl(change.project)]]">
<gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text>
</a>
</span>
@@ -231,7 +176,7 @@
<section>
<span class="title">Branch</span>
<span class="value">
- <a href$="[[_computeBranchUrl(change.project, change.branch)]]">
+ <a href\$="[[_computeBranchUrl(change.project, change.branch)]]">
<gr-limited-text limit="40" text="[[change.branch]]"></gr-limited-text>
</a>
</span>
@@ -240,18 +185,11 @@
<section>
<span class="title">[[_computeParentsLabel(_currentParents)]]</span>
<span class="value">
- <ol class$="[[_computeParentListClass(_currentParents, parentIsCurrent)]]">
+ <ol class\$="[[_computeParentListClass(_currentParents, parentIsCurrent)]]">
<template is="dom-repeat" items="[[_currentParents]]" as="parent">
<li>
- <gr-commit-info
- change="[[change]]"
- commit-info="[[parent]]"
- server-config="[[serverConfig]]"></gr-commit-info>
- <gr-tooltip-content
- id="parentNotCurrentMessage"
- has-tooltip
- show-icon
- title$="[[_notCurrentMessage]]"></gr-tooltip-content>
+ <gr-commit-info change="[[change]]" commit-info="[[parent]]" server-config="[[serverConfig]]"></gr-commit-info>
+ <gr-tooltip-content id="parentNotCurrentMessage" has-tooltip="" show-icon="" title\$="[[_notCurrentMessage]]"></gr-tooltip-content>
</li>
</template>
</ol>
@@ -260,27 +198,11 @@
<section class="topic">
<span class="title">Topic</span>
<span class="value">
- <template
- is="dom-if"
- if="[[_showTopicChip(change.*, _settingTopic)]]">
- <gr-linked-chip
- text="[[change.topic]]"
- limit="40"
- href="[[_computeTopicUrl(change.topic)]]"
- removable="[[!_topicReadOnly]]"
- on-remove="_handleTopicRemoved"></gr-linked-chip>
+ <template is="dom-if" if="[[_showTopicChip(change.*, _settingTopic)]]">
+ <gr-linked-chip text="[[change.topic]]" limit="40" href="[[_computeTopicUrl(change.topic)]]" removable="[[!_topicReadOnly]]" on-remove="_handleTopicRemoved"></gr-linked-chip>
</template>
- <template
- is="dom-if"
- if="[[_showAddTopic(change.*, _settingTopic)]]">
- <gr-editable-label
- class="topicEditableLabel"
- label-text="Add a topic"
- value="[[change.topic]]"
- max-length="1024"
- placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]"
- read-only="[[_topicReadOnly]]"
- on-changed="_handleTopicChanged"></gr-editable-label>
+ <template is="dom-if" if="[[_showAddTopic(change.*, _settingTopic)]]">
+ <gr-editable-label class="topicEditableLabel" label-text="Add a topic" value="[[change.topic]]" max-length="1024" placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" read-only="[[_topicReadOnly]]" on-changed="_handleTopicChanged"></gr-editable-label>
</template>
</span>
</section>
@@ -288,16 +210,14 @@
<section>
<span class="title">Cherry pick of</span>
<span class="value">
- <a href$="[[_computeCherryPickOfUrl(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]">
- <gr-limited-text
- text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]"
- limit="40">
+ <a href\$="[[_computeCherryPickOfUrl(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]">
+ <gr-limited-text text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]" limit="40">
</gr-limited-text>
</a>
</span>
</section>
</template>
- <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden>
+ <section class="strategy" hidden\$="[[_computeHideStrategy(change)]]" hidden="">
<span class="title">Strategy</span>
<span class="value">[[_computeStrategy(change)]]</span>
</section>
@@ -305,36 +225,21 @@
<span class="title">Hashtags</span>
<span class="value">
<template is="dom-repeat" items="[[change.hashtags]]">
- <gr-linked-chip
- class="hashtagChip"
- text="[[item]]"
- href="[[_computeHashtagUrl(item)]]"
- removable="[[!_hashtagReadOnly]]"
- on-remove="_handleHashtagRemoved">
+ <gr-linked-chip class="hashtagChip" text="[[item]]" href="[[_computeHashtagUrl(item)]]" removable="[[!_hashtagReadOnly]]" on-remove="_handleHashtagRemoved">
</gr-linked-chip>
</template>
<template is="dom-if" if="[[!_hashtagReadOnly]]">
- <gr-editable-label
- uppercase
- label-text="Add a hashtag"
- value="{{_newHashtag}}"
- placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]"
- read-only="[[_hashtagReadOnly]]"
- on-changed="_handleHashtagChanged"></gr-editable-label>
+ <gr-editable-label uppercase="" label-text="Add a hashtag" value="{{_newHashtag}}" placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]" read-only="[[_hashtagReadOnly]]" on-changed="_handleHashtagChanged"></gr-editable-label>
</template>
</span>
</section>
<div class="separatedSection">
- <gr-change-requirements
- change="{{change}}"
- account="[[account]]"
- mutable="[[_mutable]]"></gr-change-requirements>
+ <gr-change-requirements change="{{change}}" account="[[account]]" mutable="[[_mutable]]"></gr-change-requirements>
</div>
- <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo, serverConfig)]]">
+ <section id="webLinks" hidden\$="[[!_computeWebLinks(commitInfo, serverConfig)]]">
<span class="title">Links</span>
<span class="value">
- <template is="dom-repeat"
- items="[[_computeWebLinks(commitInfo, serverConfig)]]" as="link">
+ <template is="dom-repeat" items="[[_computeWebLinks(commitInfo, serverConfig)]]" as="link">
<a href="[[link.url]]" class="webLink" rel="noopener" target="_blank">
[[link.name]]
</a>
@@ -348,6 +253,4 @@
</gr-endpoint-decorator>
</gr-external-style>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-change-metadata.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 055f3f0..5fe53f8d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</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="../../core/gr-router/gr-router.html">
-<link rel="import" href="gr-change-metadata.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="../../core/gr-router/gr-router.js"></script>
+<script type="module" src="./gr-change-metadata.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../core/gr-router/gr-router.js';
+import './gr-change-metadata.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,733 +42,736 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-metadata tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../core/gr-router/gr-router.js';
+import './gr-change-metadata.js';
+suite('gr-change-metadata tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('computed fields', () => {
+ assert.isFalse(element._computeHideStrategy({status: 'NEW'}));
+ assert.isTrue(element._computeHideStrategy({status: 'MERGED'}));
+ assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'}));
+ assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}),
+ 'Cherry Pick');
+ assert.equal(element._computeStrategy({submit_type: 'REBASE_ALWAYS'}),
+ 'Rebase Always');
+ });
+
+ test('computed fields requirements', () => {
+ assert.isFalse(element._computeShowRequirements({status: 'MERGED'}));
+ assert.isFalse(element._computeShowRequirements({status: 'ABANDONED'}));
+
+ // No labels and no requirements: submit status is useless
+ assert.isFalse(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ }));
+
+ // Work in Progress: submit status should be present
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ work_in_progress: true,
+ }));
+
+ // We have at least one reason to display Submit Status
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: false,
+ },
+ },
+ requirements: [],
+ }));
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ }));
+ });
+
+ test('show strategy for open change', () => {
+ element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}};
+ flushAsynchronousOperations();
+ const strategy = element.shadowRoot
+ .querySelector('.strategy');
+ assert.ok(strategy);
+ assert.isFalse(strategy.hasAttribute('hidden'));
+ assert.equal(strategy.children[1].innerHTML, 'Cherry Pick');
+ });
+
+ test('hide strategy for closed change', () => {
+ element.change = {status: 'MERGED', labels: {}};
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('.strategy').hasAttribute('hidden'));
+ });
+
+ test('weblinks use Gerrit.Nav interface', () => {
+ const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
+ .returns([{name: 'stubb', url: '#s'}]);
+ element.commitInfo = {};
+ element.serverConfig = {};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(weblinksStub.called);
+ assert.isFalse(webLinks.hasAttribute('hidden'));
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
+ });
+
+ test('weblinks hidden when no weblinks', () => {
+ element.commitInfo = {};
+ element.serverConfig = {};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(webLinks.hasAttribute('hidden'));
+ });
+
+ test('weblinks hidden when only gitiles weblink', () => {
+ element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]};
+ element.serverConfig = {};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(webLinks.hasAttribute('hidden'));
+ assert.equal(element._computeWebLinks(element.commitInfo), null);
+ });
+
+ test('weblinks hidden when sole weblink is set as primary', () => {
+ const browser = 'browser';
+ element.commitInfo = {web_links: [{name: browser, url: '#'}]};
+ element.serverConfig = {
+ gerrit: {
+ primary_weblink_name: browser,
+ },
+ };
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isTrue(webLinks.hasAttribute('hidden'));
+ });
+
+ test('weblinks are visible when other weblinks', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.commitInfo = {web_links: [{name: 'test', url: '#'}]};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isFalse(webLinks.hasAttribute('hidden'));
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
+ // With two non-gitiles weblinks, there are two returned.
+ element.commitInfo = {
+ web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]};
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 2);
+ });
+
+ test('weblinks are visible when gitiles and other weblinks', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.commitInfo = {
+ web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]};
+ flushAsynchronousOperations();
+ const webLinks = element.$.webLinks;
+ assert.isFalse(webLinks.hasAttribute('hidden'));
+ // Only the non-gitiles weblink is returned.
+ assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
+ });
+
+ suite('_getNonOwnerRole', () => {
+ let change;
setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-endpoint-decorator', {
- _import: sandbox.stub().returns(Promise.resolve()),
- });
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- });
-
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('computed fields', () => {
- assert.isFalse(element._computeHideStrategy({status: 'NEW'}));
- assert.isTrue(element._computeHideStrategy({status: 'MERGED'}));
- assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'}));
- assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}),
- 'Cherry Pick');
- assert.equal(element._computeStrategy({submit_type: 'REBASE_ALWAYS'}),
- 'Rebase Always');
- });
-
- test('computed fields requirements', () => {
- assert.isFalse(element._computeShowRequirements({status: 'MERGED'}));
- assert.isFalse(element._computeShowRequirements({status: 'ABANDONED'}));
-
- // No labels and no requirements: submit status is useless
- assert.isFalse(element._computeShowRequirements({
- status: 'NEW',
- labels: {},
- }));
-
- // Work in Progress: submit status should be present
- assert.isTrue(element._computeShowRequirements({
- status: 'NEW',
- labels: {},
- work_in_progress: true,
- }));
-
- // We have at least one reason to display Submit Status
- assert.isTrue(element._computeShowRequirements({
- status: 'NEW',
- labels: {
- Verified: {
- approved: false,
- },
- },
- requirements: [],
- }));
- assert.isTrue(element._computeShowRequirements({
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- }));
- });
-
- test('show strategy for open change', () => {
- element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}};
- flushAsynchronousOperations();
- const strategy = element.shadowRoot
- .querySelector('.strategy');
- assert.ok(strategy);
- assert.isFalse(strategy.hasAttribute('hidden'));
- assert.equal(strategy.children[1].innerHTML, 'Cherry Pick');
- });
-
- test('hide strategy for closed change', () => {
- element.change = {status: 'MERGED', labels: {}};
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('.strategy').hasAttribute('hidden'));
- });
-
- test('weblinks use Gerrit.Nav interface', () => {
- const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
- .returns([{name: 'stubb', url: '#s'}]);
- element.commitInfo = {};
- element.serverConfig = {};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(weblinksStub.called);
- assert.isFalse(webLinks.hasAttribute('hidden'));
- assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
- });
-
- test('weblinks hidden when no weblinks', () => {
- element.commitInfo = {};
- element.serverConfig = {};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(webLinks.hasAttribute('hidden'));
- });
-
- test('weblinks hidden when only gitiles weblink', () => {
- element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]};
- element.serverConfig = {};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(webLinks.hasAttribute('hidden'));
- assert.equal(element._computeWebLinks(element.commitInfo), null);
- });
-
- test('weblinks hidden when sole weblink is set as primary', () => {
- const browser = 'browser';
- element.commitInfo = {web_links: [{name: browser, url: '#'}]};
- element.serverConfig = {
- gerrit: {
- primary_weblink_name: browser,
- },
- };
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isTrue(webLinks.hasAttribute('hidden'));
- });
-
- test('weblinks are visible when other weblinks', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.commitInfo = {web_links: [{name: 'test', url: '#'}]};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isFalse(webLinks.hasAttribute('hidden'));
- assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
- // With two non-gitiles weblinks, there are two returned.
- element.commitInfo = {
- web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]};
- assert.equal(element._computeWebLinks(element.commitInfo).length, 2);
- });
-
- test('weblinks are visible when gitiles and other weblinks', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.commitInfo = {
- web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]};
- flushAsynchronousOperations();
- const webLinks = element.$.webLinks;
- assert.isFalse(webLinks.hasAttribute('hidden'));
- // Only the non-gitiles weblink is returned.
- assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
- });
-
- suite('_getNonOwnerRole', () => {
- let change;
-
- setup(() => {
- change = {
- owner: {
- email: 'abc@def',
- _account_id: 1019328,
- },
- revisions: {
- rev1: {
- _number: 1,
- uploader: {
- email: 'ghi@def',
- _account_id: 1011123,
- },
- commit: {
- author: {email: 'jkl@def'},
- committer: {email: 'ghi@def'},
- },
- },
- },
- current_revision: 'rev1',
- };
- });
-
- suite('role=uploader', () => {
- test('_getNonOwnerRole for uploader', () => {
- assert.deepEqual(
- element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER),
- {email: 'ghi@def', _account_id: 1011123});
- });
-
- test('_getNonOwnerRole that it does not return uploader', () => {
- // Set the uploader email to be the same as the owner.
- change.revisions.rev1.uploader._account_id = 1019328;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.UPLOADER));
- });
-
- test('_getNonOwnerRole null for uploader with no current rev', () => {
- delete change.current_revision;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.UPLOADER));
- });
-
- test('_computeShowRoleClass show uploader', () => {
- assert.equal(element._computeShowRoleClass(
- change, element._CHANGE_ROLE.UPLOADER), '');
- });
-
- test('_computeShowRoleClass hide uploader', () => {
- // Set the uploader email to be the same as the owner.
- change.revisions.rev1.uploader._account_id = 1019328;
- assert.equal(element._computeShowRoleClass(change,
- element._CHANGE_ROLE.UPLOADER), 'hideDisplay');
- });
- });
-
- suite('role=committer', () => {
- test('_getNonOwnerRole for committer', () => {
- assert.deepEqual(
- element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER),
- {email: 'ghi@def'});
- });
-
- test('_getNonOwnerRole that it does not return committer', () => {
- // Set the committer email to be the same as the owner.
- change.revisions.rev1.commit.committer.email = 'abc@def';
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
-
- test('_getNonOwnerRole null for committer with no current rev', () => {
- delete change.current_revision;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
-
- test('_getNonOwnerRole null for committer with no commit', () => {
- delete change.revisions.rev1.commit;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
-
- test('_getNonOwnerRole null for committer with no committer', () => {
- delete change.revisions.rev1.commit.committer;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.COMMITTER));
- });
- });
-
- suite('role=author', () => {
- test('_getNonOwnerRole for author', () => {
- assert.deepEqual(
- element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR),
- {email: 'jkl@def'});
- });
-
- test('_getNonOwnerRole that it does not return author', () => {
- // Set the author email to be the same as the owner.
- change.revisions.rev1.commit.author.email = 'abc@def';
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
-
- test('_getNonOwnerRole null for author with no current rev', () => {
- delete change.current_revision;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
-
- test('_getNonOwnerRole null for author with no commit', () => {
- delete change.revisions.rev1.commit;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
-
- test('_getNonOwnerRole null for author with no author', () => {
- delete change.revisions.rev1.commit.author;
- assert.isNull(element._getNonOwnerRole(change,
- element._CHANGE_ROLE.AUTHOR));
- });
- });
- });
-
- test('Push Certificate Validation test BAD', () => {
- const serverConfig = {
- receive: {
- enable_signed_push: true,
- },
- };
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ change = {
owner: {
+ email: 'abc@def',
_account_id: 1019328,
},
revisions: {
rev1: {
_number: 1,
- push_certificate: {
- key: {
- status: 'BAD',
- problems: [
- 'No public keys found for key ID E5E20E52',
- ],
- },
+ uploader: {
+ email: 'ghi@def',
+ _account_id: 1011123,
+ },
+ commit: {
+ author: {email: 'jkl@def'},
+ committer: {email: 'ghi@def'},
},
},
},
current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
};
- const result =
- element._computePushCertificateValidation(serverConfig, change);
- assert.equal(result.message,
- 'Push certificate is invalid:\n' +
- 'No public keys found for key ID E5E20E52');
- assert.equal(result.icon, 'gr-icons:close');
- assert.equal(result.class, 'invalid');
});
- test('Push Certificate Validation test TRUSTED', () => {
- const serverConfig = {
- receive: {
- enable_signed_push: true,
- },
- };
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- owner: {
- _account_id: 1019328,
- },
- revisions: {
- rev1: {
- _number: 1,
- push_certificate: {
- key: {
- status: 'TRUSTED',
- },
- },
- },
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
- };
- const result =
- element._computePushCertificateValidation(serverConfig, change);
- assert.equal(result.message,
- 'Push certificate is valid and key is trusted');
- assert.equal(result.icon, 'gr-icons:check');
- assert.equal(result.class, 'trusted');
- });
-
- test('Push Certificate Validation is missing test', () => {
- const serverConfig = {
- receive: {
- enable_signed_push: true,
- },
- };
- const change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- owner: {
- _account_id: 1019328,
- },
- revisions: {
- rev1: {
- _number: 1,
- },
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- mergeable: true,
- };
- const result =
- element._computePushCertificateValidation(serverConfig, change);
- assert.equal(result.message,
- 'This patch set was created without a push certificate');
- assert.equal(result.icon, 'gr-icons:help');
- assert.equal(result.class, 'help');
- });
-
- test('_computeParents', () => {
- const parents = [{commit: '123', subject: 'abc'}];
- assert.isUndefined(element._computeParents(
- {revisions: {456: {commit: {parents}}}}));
- assert.isUndefined(element._computeParents(
- {current_revision: '789', revisions: {456: {commit: {parents}}}}));
- assert.equal(element._computeParents(
- {current_revision: '456', revisions: {456: {commit: {parents}}}}),
- parents);
- });
-
- test('_computeParentsLabel', () => {
- const parent = {commit: 'abc123', subject: 'My parent commit'};
- assert.equal(element._computeParentsLabel([parent]), 'Parent');
- assert.equal(element._computeParentsLabel([parent, parent]),
- 'Parents');
- });
-
- test('_computeParentListClass', () => {
- const parent = {commit: 'abc123', subject: 'My parent commit'};
- assert.equal(element._computeParentListClass([parent], true),
- 'parentList nonMerge current');
- assert.equal(element._computeParentListClass([parent], false),
- 'parentList nonMerge notCurrent');
- assert.equal(element._computeParentListClass([parent, parent], false),
- 'parentList merge notCurrent');
- assert.equal(element._computeParentListClass([parent, parent], true),
- 'parentList merge current');
- });
-
- test('_showAddTopic', () => {
- assert.isTrue(element._showAddTopic(null, false));
- assert.isTrue(element._showAddTopic({base: {topic: null}}, false));
- assert.isFalse(element._showAddTopic({base: {topic: null}}, true));
- assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, true));
- assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, false));
- });
-
- test('_showTopicChip', () => {
- assert.isFalse(element._showTopicChip(null, false));
- assert.isFalse(element._showTopicChip({base: {topic: null}}, false));
- assert.isFalse(element._showTopicChip({base: {topic: null}}, true));
- assert.isFalse(element._showTopicChip({base: {topic: 'foo'}}, true));
- assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false));
- });
-
- test('_showCherryPickOf', () => {
- assert.isFalse(element._showCherryPickOf(null));
- assert.isFalse(element._showCherryPickOf({
- base: {
- cherry_pick_of_change: null,
- cherry_pick_of_patch_set: null,
- },
- }));
- assert.isTrue(element._showCherryPickOf({
- base: {
- cherry_pick_of_change: 123,
- cherry_pick_of_patch_set: 1,
- },
- }));
- });
-
- suite('Topic removal', () => {
- let change;
- setup(() => {
- change = {
- _number: 'the number',
- actions: {
- topic: {enabled: false},
- },
- change_id: 'the id',
- topic: 'the topic',
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- },
- },
- removable_reviewers: [],
- };
+ suite('role=uploader', () => {
+ test('_getNonOwnerRole for uploader', () => {
+ assert.deepEqual(
+ element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER),
+ {email: 'ghi@def', _account_id: 1011123});
});
- test('_computeTopicReadOnly', () => {
- let mutable = false;
- assert.isTrue(element._computeTopicReadOnly(mutable, change));
- mutable = true;
- assert.isTrue(element._computeTopicReadOnly(mutable, change));
- change.actions.topic.enabled = true;
- assert.isFalse(element._computeTopicReadOnly(mutable, change));
- mutable = false;
- assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ test('_getNonOwnerRole that it does not return uploader', () => {
+ // Set the uploader email to be the same as the owner.
+ change.revisions.rev1.uploader._account_id = 1019328;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.UPLOADER));
});
- test('topic read only hides delete button', () => {
- element.account = {};
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isTrue(button.hasAttribute('hidden'));
+ test('_getNonOwnerRole null for uploader with no current rev', () => {
+ delete change.current_revision;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.UPLOADER));
});
- test('topic not read only does not hide delete button', () => {
- element.account = {test: true};
- change.actions.topic.enabled = true;
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isFalse(button.hasAttribute('hidden'));
+ test('_computeShowRoleClass show uploader', () => {
+ assert.equal(element._computeShowRoleClass(
+ change, element._CHANGE_ROLE.UPLOADER), '');
+ });
+
+ test('_computeShowRoleClass hide uploader', () => {
+ // Set the uploader email to be the same as the owner.
+ change.revisions.rev1.uploader._account_id = 1019328;
+ assert.equal(element._computeShowRoleClass(change,
+ element._CHANGE_ROLE.UPLOADER), 'hideDisplay');
});
});
- suite('Hashtag removal', () => {
- let change;
- setup(() => {
- change = {
- _number: 'the number',
- actions: {
- hashtags: {enabled: false},
- },
- change_id: 'the id',
- hashtags: ['test-hashtag'],
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- },
- },
- removable_reviewers: [],
- };
+ suite('role=committer', () => {
+ test('_getNonOwnerRole for committer', () => {
+ assert.deepEqual(
+ element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER),
+ {email: 'ghi@def'});
});
- test('_computeHashtagReadOnly', () => {
- flushAsynchronousOperations();
- let mutable = false;
- assert.isTrue(element._computeHashtagReadOnly(mutable, change));
- mutable = true;
- assert.isTrue(element._computeHashtagReadOnly(mutable, change));
- change.actions.hashtags.enabled = true;
- assert.isFalse(element._computeHashtagReadOnly(mutable, change));
- mutable = false;
- assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ test('_getNonOwnerRole that it does not return committer', () => {
+ // Set the committer email to be the same as the owner.
+ change.revisions.rev1.commit.committer.email = 'abc@def';
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
});
- test('hashtag read only hides delete button', () => {
- flushAsynchronousOperations();
- element.account = {};
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isTrue(button.hasAttribute('hidden'));
+ test('_getNonOwnerRole null for committer with no current rev', () => {
+ delete change.current_revision;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
});
- test('hashtag not read only does not hide delete button', () => {
- flushAsynchronousOperations();
- element.account = {test: true};
- change.actions.hashtags.enabled = true;
- element.change = change;
- flushAsynchronousOperations();
- const button = element.shadowRoot
- .querySelector('gr-linked-chip').shadowRoot
- .querySelector('gr-button');
- assert.isFalse(button.hasAttribute('hidden'));
+ test('_getNonOwnerRole null for committer with no commit', () => {
+ delete change.revisions.rev1.commit;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
+ });
+
+ test('_getNonOwnerRole null for committer with no committer', () => {
+ delete change.revisions.rev1.commit.committer;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.COMMITTER));
});
});
- suite('remove reviewer votes', () => {
- setup(() => {
- sandbox.stub(element, '_computeTopicReadOnly').returns(true);
- element.change = {
- _number: 42,
- change_id: 'the id',
- actions: [],
- topic: 'the topic',
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- },
- },
- removable_reviewers: [],
- };
- flushAsynchronousOperations();
+ suite('role=author', () => {
+ test('_getNonOwnerRole for author', () => {
+ assert.deepEqual(
+ element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR),
+ {email: 'jkl@def'});
});
- suite('assignee field', () => {
- const dummyAccount = {
- _account_id: 1,
- name: 'bojack',
- };
- const change = {
- actions: {
- assignee: {enabled: false},
- },
- assignee: dummyAccount,
- };
- let deleteStub;
- let setStub;
-
- setup(() => {
- deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee');
- setStub = sandbox.stub(element.$.restAPI, 'setAssignee');
- });
-
- test('changing change recomputes _assignee', () => {
- assert.isFalse(!!element._assignee.length);
- const change = element.change;
- change.assignee = dummyAccount;
- element._changeChanged(change);
- assert.deepEqual(element._assignee[0], dummyAccount);
- });
-
- test('modifying _assignee calls API', () => {
- assert.isFalse(!!element._assignee.length);
- element.set('_assignee', [dummyAccount]);
- assert.isTrue(setStub.calledOnce);
- assert.deepEqual(element.change.assignee, dummyAccount);
- element.set('_assignee', [dummyAccount]);
- assert.isTrue(setStub.calledOnce);
- element.set('_assignee', []);
- assert.isTrue(deleteStub.calledOnce);
- assert.equal(element.change.assignee, undefined);
- element.set('_assignee', []);
- assert.isTrue(deleteStub.calledOnce);
- });
-
- test('_computeAssigneeReadOnly', () => {
- let mutable = false;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- mutable = true;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- change.actions.assignee.enabled = true;
- assert.isFalse(element._computeAssigneeReadOnly(mutable, change));
- mutable = false;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- });
+ test('_getNonOwnerRole that it does not return author', () => {
+ // Set the author email to be the same as the owner.
+ change.revisions.rev1.commit.author.email = 'abc@def';
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
- test('changing topic', () => {
- const newTopic = 'the new topic';
- sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
- Promise.resolve(newTopic));
- element._handleTopicChanged({}, newTopic);
- const topicChangedSpy = sandbox.spy();
- element.addEventListener('topic-changed', topicChangedSpy);
- assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
- 42, newTopic));
- return element.$.restAPI.setChangeTopic.lastCall.returnValue
- .then(() => {
- assert.equal(element.change.topic, newTopic);
- assert.isTrue(topicChangedSpy.called);
- });
+ test('_getNonOwnerRole null for author with no current rev', () => {
+ delete change.current_revision;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
- test('topic removal', () => {
- sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
- Promise.resolve());
- const chip = element.shadowRoot
- .querySelector('gr-linked-chip');
- const remove = chip.$.remove;
- const topicChangedSpy = sandbox.spy();
- element.addEventListener('topic-changed', topicChangedSpy);
- MockInteractions.tap(remove);
- assert.isTrue(chip.disabled);
- assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
- 42, null));
- return element.$.restAPI.setChangeTopic.lastCall.returnValue
- .then(() => {
- assert.isFalse(chip.disabled);
- assert.equal(element.change.topic, '');
- assert.isTrue(topicChangedSpy.called);
- });
+ test('_getNonOwnerRole null for author with no commit', () => {
+ delete change.revisions.rev1.commit;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
- test('changing hashtag', () => {
- flushAsynchronousOperations();
- element._newHashtag = 'new hashtag';
- const newHashtag = ['new hashtag'];
- sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns(
- Promise.resolve(newHashtag));
- element._handleHashtagChanged({}, 'new hashtag');
- assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith(
- 42, {add: ['new hashtag']}));
- return element.$.restAPI.setChangeHashtag.lastCall.returnValue
- .then(() => {
- assert.equal(element.change.hashtags, newHashtag);
- });
- });
- });
-
- test('editTopic', () => {
- element.account = {test: true};
- element.change = {actions: {topic: {enabled: true}}};
- flushAsynchronousOperations();
-
- const label = element.shadowRoot
- .querySelector('.topicEditableLabel');
- assert.ok(label);
- sandbox.stub(label, 'open');
- element.editTopic();
- flushAsynchronousOperations();
-
- assert.isTrue(label.open.called);
- });
-
- suite('plugin endpoints', () => {
- test('endpoint params', done => {
- element.change = {labels: {}};
- element.revision = {};
- let hookEl;
- let plugin;
- Gerrit.install(
- p => {
- plugin = p;
- plugin.hook('change-metadata-item').getLastAttached()
- .then(el => hookEl = el);
- },
- '0.1',
- 'http://some/plugins/url.html');
- Gerrit._loadPlugins([]);
- flush(() => {
- assert.strictEqual(hookEl.plugin, plugin);
- assert.strictEqual(hookEl.change, element.change);
- assert.strictEqual(hookEl.revision, element.revision);
- done();
- });
+ test('_getNonOwnerRole null for author with no author', () => {
+ delete change.revisions.rev1.commit.author;
+ assert.isNull(element._getNonOwnerRole(change,
+ element._CHANGE_ROLE.AUTHOR));
});
});
});
+
+ test('Push Certificate Validation test BAD', () => {
+ const serverConfig = {
+ receive: {
+ enable_signed_push: true,
+ },
+ };
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {
+ _account_id: 1019328,
+ },
+ revisions: {
+ rev1: {
+ _number: 1,
+ push_certificate: {
+ key: {
+ status: 'BAD',
+ problems: [
+ 'No public keys found for key ID E5E20E52',
+ ],
+ },
+ },
+ },
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ const result =
+ element._computePushCertificateValidation(serverConfig, change);
+ assert.equal(result.message,
+ 'Push certificate is invalid:\n' +
+ 'No public keys found for key ID E5E20E52');
+ assert.equal(result.icon, 'gr-icons:close');
+ assert.equal(result.class, 'invalid');
+ });
+
+ test('Push Certificate Validation test TRUSTED', () => {
+ const serverConfig = {
+ receive: {
+ enable_signed_push: true,
+ },
+ };
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {
+ _account_id: 1019328,
+ },
+ revisions: {
+ rev1: {
+ _number: 1,
+ push_certificate: {
+ key: {
+ status: 'TRUSTED',
+ },
+ },
+ },
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ const result =
+ element._computePushCertificateValidation(serverConfig, change);
+ assert.equal(result.message,
+ 'Push certificate is valid and key is trusted');
+ assert.equal(result.icon, 'gr-icons:check');
+ assert.equal(result.class, 'trusted');
+ });
+
+ test('Push Certificate Validation is missing test', () => {
+ const serverConfig = {
+ receive: {
+ enable_signed_push: true,
+ },
+ };
+ const change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {
+ _account_id: 1019328,
+ },
+ revisions: {
+ rev1: {
+ _number: 1,
+ },
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ mergeable: true,
+ };
+ const result =
+ element._computePushCertificateValidation(serverConfig, change);
+ assert.equal(result.message,
+ 'This patch set was created without a push certificate');
+ assert.equal(result.icon, 'gr-icons:help');
+ assert.equal(result.class, 'help');
+ });
+
+ test('_computeParents', () => {
+ const parents = [{commit: '123', subject: 'abc'}];
+ assert.isUndefined(element._computeParents(
+ {revisions: {456: {commit: {parents}}}}));
+ assert.isUndefined(element._computeParents(
+ {current_revision: '789', revisions: {456: {commit: {parents}}}}));
+ assert.equal(element._computeParents(
+ {current_revision: '456', revisions: {456: {commit: {parents}}}}),
+ parents);
+ });
+
+ test('_computeParentsLabel', () => {
+ const parent = {commit: 'abc123', subject: 'My parent commit'};
+ assert.equal(element._computeParentsLabel([parent]), 'Parent');
+ assert.equal(element._computeParentsLabel([parent, parent]),
+ 'Parents');
+ });
+
+ test('_computeParentListClass', () => {
+ const parent = {commit: 'abc123', subject: 'My parent commit'};
+ assert.equal(element._computeParentListClass([parent], true),
+ 'parentList nonMerge current');
+ assert.equal(element._computeParentListClass([parent], false),
+ 'parentList nonMerge notCurrent');
+ assert.equal(element._computeParentListClass([parent, parent], false),
+ 'parentList merge notCurrent');
+ assert.equal(element._computeParentListClass([parent, parent], true),
+ 'parentList merge current');
+ });
+
+ test('_showAddTopic', () => {
+ assert.isTrue(element._showAddTopic(null, false));
+ assert.isTrue(element._showAddTopic({base: {topic: null}}, false));
+ assert.isFalse(element._showAddTopic({base: {topic: null}}, true));
+ assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, true));
+ assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, false));
+ });
+
+ test('_showTopicChip', () => {
+ assert.isFalse(element._showTopicChip(null, false));
+ assert.isFalse(element._showTopicChip({base: {topic: null}}, false));
+ assert.isFalse(element._showTopicChip({base: {topic: null}}, true));
+ assert.isFalse(element._showTopicChip({base: {topic: 'foo'}}, true));
+ assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false));
+ });
+
+ test('_showCherryPickOf', () => {
+ assert.isFalse(element._showCherryPickOf(null));
+ assert.isFalse(element._showCherryPickOf({
+ base: {
+ cherry_pick_of_change: null,
+ cherry_pick_of_patch_set: null,
+ },
+ }));
+ assert.isTrue(element._showCherryPickOf({
+ base: {
+ cherry_pick_of_change: 123,
+ cherry_pick_of_patch_set: 1,
+ },
+ }));
+ });
+
+ suite('Topic removal', () => {
+ let change;
+ setup(() => {
+ change = {
+ _number: 'the number',
+ actions: {
+ topic: {enabled: false},
+ },
+ change_id: 'the id',
+ topic: 'the topic',
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ },
+ },
+ removable_reviewers: [],
+ };
+ });
+
+ test('_computeTopicReadOnly', () => {
+ let mutable = false;
+ assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ mutable = true;
+ assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ change.actions.topic.enabled = true;
+ assert.isFalse(element._computeTopicReadOnly(mutable, change));
+ mutable = false;
+ assert.isTrue(element._computeTopicReadOnly(mutable, change));
+ });
+
+ test('topic read only hides delete button', () => {
+ element.account = {};
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isTrue(button.hasAttribute('hidden'));
+ });
+
+ test('topic not read only does not hide delete button', () => {
+ element.account = {test: true};
+ change.actions.topic.enabled = true;
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isFalse(button.hasAttribute('hidden'));
+ });
+ });
+
+ suite('Hashtag removal', () => {
+ let change;
+ setup(() => {
+ change = {
+ _number: 'the number',
+ actions: {
+ hashtags: {enabled: false},
+ },
+ change_id: 'the id',
+ hashtags: ['test-hashtag'],
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ },
+ },
+ removable_reviewers: [],
+ };
+ });
+
+ test('_computeHashtagReadOnly', () => {
+ flushAsynchronousOperations();
+ let mutable = false;
+ assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ mutable = true;
+ assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ change.actions.hashtags.enabled = true;
+ assert.isFalse(element._computeHashtagReadOnly(mutable, change));
+ mutable = false;
+ assert.isTrue(element._computeHashtagReadOnly(mutable, change));
+ });
+
+ test('hashtag read only hides delete button', () => {
+ flushAsynchronousOperations();
+ element.account = {};
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isTrue(button.hasAttribute('hidden'));
+ });
+
+ test('hashtag not read only does not hide delete button', () => {
+ flushAsynchronousOperations();
+ element.account = {test: true};
+ change.actions.hashtags.enabled = true;
+ element.change = change;
+ flushAsynchronousOperations();
+ const button = element.shadowRoot
+ .querySelector('gr-linked-chip').shadowRoot
+ .querySelector('gr-button');
+ assert.isFalse(button.hasAttribute('hidden'));
+ });
+ });
+
+ suite('remove reviewer votes', () => {
+ setup(() => {
+ sandbox.stub(element, '_computeTopicReadOnly').returns(true);
+ element.change = {
+ _number: 42,
+ change_id: 'the id',
+ actions: [],
+ topic: 'the topic',
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ },
+ },
+ removable_reviewers: [],
+ };
+ flushAsynchronousOperations();
+ });
+
+ suite('assignee field', () => {
+ const dummyAccount = {
+ _account_id: 1,
+ name: 'bojack',
+ };
+ const change = {
+ actions: {
+ assignee: {enabled: false},
+ },
+ assignee: dummyAccount,
+ };
+ let deleteStub;
+ let setStub;
+
+ setup(() => {
+ deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee');
+ setStub = sandbox.stub(element.$.restAPI, 'setAssignee');
+ });
+
+ test('changing change recomputes _assignee', () => {
+ assert.isFalse(!!element._assignee.length);
+ const change = element.change;
+ change.assignee = dummyAccount;
+ element._changeChanged(change);
+ assert.deepEqual(element._assignee[0], dummyAccount);
+ });
+
+ test('modifying _assignee calls API', () => {
+ assert.isFalse(!!element._assignee.length);
+ element.set('_assignee', [dummyAccount]);
+ assert.isTrue(setStub.calledOnce);
+ assert.deepEqual(element.change.assignee, dummyAccount);
+ element.set('_assignee', [dummyAccount]);
+ assert.isTrue(setStub.calledOnce);
+ element.set('_assignee', []);
+ assert.isTrue(deleteStub.calledOnce);
+ assert.equal(element.change.assignee, undefined);
+ element.set('_assignee', []);
+ assert.isTrue(deleteStub.calledOnce);
+ });
+
+ test('_computeAssigneeReadOnly', () => {
+ let mutable = false;
+ assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
+ mutable = true;
+ assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
+ change.actions.assignee.enabled = true;
+ assert.isFalse(element._computeAssigneeReadOnly(mutable, change));
+ mutable = false;
+ assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
+ });
+ });
+
+ test('changing topic', () => {
+ const newTopic = 'the new topic';
+ sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+ Promise.resolve(newTopic));
+ element._handleTopicChanged({}, newTopic);
+ const topicChangedSpy = sandbox.spy();
+ element.addEventListener('topic-changed', topicChangedSpy);
+ assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
+ 42, newTopic));
+ return element.$.restAPI.setChangeTopic.lastCall.returnValue
+ .then(() => {
+ assert.equal(element.change.topic, newTopic);
+ assert.isTrue(topicChangedSpy.called);
+ });
+ });
+
+ test('topic removal', () => {
+ sandbox.stub(element.$.restAPI, 'setChangeTopic').returns(
+ Promise.resolve());
+ const chip = element.shadowRoot
+ .querySelector('gr-linked-chip');
+ const remove = chip.$.remove;
+ const topicChangedSpy = sandbox.spy();
+ element.addEventListener('topic-changed', topicChangedSpy);
+ MockInteractions.tap(remove);
+ assert.isTrue(chip.disabled);
+ assert.isTrue(element.$.restAPI.setChangeTopic.calledWith(
+ 42, null));
+ return element.$.restAPI.setChangeTopic.lastCall.returnValue
+ .then(() => {
+ assert.isFalse(chip.disabled);
+ assert.equal(element.change.topic, '');
+ assert.isTrue(topicChangedSpy.called);
+ });
+ });
+
+ test('changing hashtag', () => {
+ flushAsynchronousOperations();
+ element._newHashtag = 'new hashtag';
+ const newHashtag = ['new hashtag'];
+ sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns(
+ Promise.resolve(newHashtag));
+ element._handleHashtagChanged({}, 'new hashtag');
+ assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith(
+ 42, {add: ['new hashtag']}));
+ return element.$.restAPI.setChangeHashtag.lastCall.returnValue
+ .then(() => {
+ assert.equal(element.change.hashtags, newHashtag);
+ });
+ });
+ });
+
+ test('editTopic', () => {
+ element.account = {test: true};
+ element.change = {actions: {topic: {enabled: true}}};
+ flushAsynchronousOperations();
+
+ const label = element.shadowRoot
+ .querySelector('.topicEditableLabel');
+ assert.ok(label);
+ sandbox.stub(label, 'open');
+ element.editTopic();
+ flushAsynchronousOperations();
+
+ assert.isTrue(label.open.called);
+ });
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element.change = {labels: {}};
+ element.revision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-metadata-item').getLastAttached()
+ .then(el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ Gerrit._loadPlugins([]);
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element.change);
+ assert.strictEqual(hookEl.revision, element.revision);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index a413c6f..9dd5acf 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -14,147 +14,160 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
- */
- class GrChangeRequirements extends Polymer.mixinBehaviors( [
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-requirements'; }
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-label/gr-label.js';
+import '../../shared/gr-label-info/gr-label-info.js';
+import '../../shared/gr-limited-text/gr-limited-text.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-change-requirements_html.js';
- static get properties() {
- return {
- /** @type {?} */
- change: Object,
- account: Object,
- mutable: Boolean,
- _requirements: {
- type: Array,
- computed: '_computeRequirements(change)',
- },
- _requiredLabels: {
- type: Array,
- value: () => [],
- },
- _optionalLabels: {
- type: Array,
- value: () => [],
- },
- _showWip: {
- type: Boolean,
- computed: '_computeShowWip(change)',
- },
- _showOptionalLabels: {
- type: Boolean,
- value: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeRequirements extends mixinBehaviors( [
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_computeLabels(change.labels.*)',
- ];
- }
+ static get is() { return 'gr-change-requirements'; }
- _computeShowWip(change) {
- return change.work_in_progress;
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: Object,
+ account: Object,
+ mutable: Boolean,
+ _requirements: {
+ type: Array,
+ computed: '_computeRequirements(change)',
+ },
+ _requiredLabels: {
+ type: Array,
+ value: () => [],
+ },
+ _optionalLabels: {
+ type: Array,
+ value: () => [],
+ },
+ _showWip: {
+ type: Boolean,
+ computed: '_computeShowWip(change)',
+ },
+ _showOptionalLabels: {
+ type: Boolean,
+ value: true,
+ },
+ };
+ }
- _computeRequirements(change) {
- const _requirements = [];
+ static get observers() {
+ return [
+ '_computeLabels(change.labels.*)',
+ ];
+ }
- if (change.requirements) {
- for (const requirement of change.requirements) {
- requirement.satisfied = requirement.status === 'OK';
- requirement.style =
- this._computeRequirementClass(requirement.satisfied);
- _requirements.push(requirement);
- }
- }
- if (change.work_in_progress) {
- _requirements.push({
- fallback_text: 'Work-in-progress',
- tooltip: 'Change must not be in \'Work in Progress\' state.',
- });
- }
+ _computeShowWip(change) {
+ return change.work_in_progress;
+ }
- return _requirements;
- }
+ _computeRequirements(change) {
+ const _requirements = [];
- _computeRequirementClass(requirementStatus) {
- return requirementStatus ? 'approved' : '';
- }
-
- _computeRequirementIcon(requirementStatus) {
- return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass';
- }
-
- _computeLabels(labelsRecord) {
- const labels = labelsRecord.base;
- this._optionalLabels = [];
- this._requiredLabels = [];
-
- for (const label in labels) {
- if (!labels.hasOwnProperty(label)) { continue; }
-
- const labelInfo = labels[label];
- const icon = this._computeLabelIcon(labelInfo);
- const style = this._computeLabelClass(labelInfo);
- const path = labelInfo.optional ? '_optionalLabels' : '_requiredLabels';
-
- this.push(path, {label, icon, style, labelInfo});
+ if (change.requirements) {
+ for (const requirement of change.requirements) {
+ requirement.satisfied = requirement.status === 'OK';
+ requirement.style =
+ this._computeRequirementClass(requirement.satisfied);
+ _requirements.push(requirement);
}
}
-
- /**
- * @param {Object} labelInfo
- * @return {string} The icon name, or undefined if no icon should
- * be used.
- */
- _computeLabelIcon(labelInfo) {
- if (labelInfo.approved) { return 'gr-icons:check'; }
- if (labelInfo.rejected) { return 'gr-icons:close'; }
- return 'gr-icons:hourglass';
+ if (change.work_in_progress) {
+ _requirements.push({
+ fallback_text: 'Work-in-progress',
+ tooltip: 'Change must not be in \'Work in Progress\' state.',
+ });
}
- /**
- * @param {Object} labelInfo
- */
- _computeLabelClass(labelInfo) {
- if (labelInfo.approved) { return 'approved'; }
- if (labelInfo.rejected) { return 'rejected'; }
- return '';
- }
+ return _requirements;
+ }
- _computeShowOptional(optionalFieldsRecord) {
- return optionalFieldsRecord.base.length ? '' : 'hidden';
- }
+ _computeRequirementClass(requirementStatus) {
+ return requirementStatus ? 'approved' : '';
+ }
- _computeLabelValue(value) {
- return (value > 0 ? '+' : '') + value;
- }
+ _computeRequirementIcon(requirementStatus) {
+ return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass';
+ }
- _computeShowHideIcon(showOptionalLabels) {
- return showOptionalLabels ?
- 'gr-icons:expand-less' :
- 'gr-icons:expand-more';
- }
+ _computeLabels(labelsRecord) {
+ const labels = labelsRecord.base;
+ this._optionalLabels = [];
+ this._requiredLabels = [];
- _computeSectionClass(show) {
- return show ? '' : 'hidden';
- }
+ for (const label in labels) {
+ if (!labels.hasOwnProperty(label)) { continue; }
- _handleShowHide(e) {
- this._showOptionalLabels = !this._showOptionalLabels;
+ const labelInfo = labels[label];
+ const icon = this._computeLabelIcon(labelInfo);
+ const style = this._computeLabelClass(labelInfo);
+ const path = labelInfo.optional ? '_optionalLabels' : '_requiredLabels';
+
+ this.push(path, {label, icon, style, labelInfo});
}
}
- customElements.define(GrChangeRequirements.is, GrChangeRequirements);
-})();
+ /**
+ * @param {Object} labelInfo
+ * @return {string} The icon name, or undefined if no icon should
+ * be used.
+ */
+ _computeLabelIcon(labelInfo) {
+ if (labelInfo.approved) { return 'gr-icons:check'; }
+ if (labelInfo.rejected) { return 'gr-icons:close'; }
+ return 'gr-icons:hourglass';
+ }
+
+ /**
+ * @param {Object} labelInfo
+ */
+ _computeLabelClass(labelInfo) {
+ if (labelInfo.approved) { return 'approved'; }
+ if (labelInfo.rejected) { return 'rejected'; }
+ return '';
+ }
+
+ _computeShowOptional(optionalFieldsRecord) {
+ return optionalFieldsRecord.base.length ? '' : 'hidden';
+ }
+
+ _computeLabelValue(value) {
+ return (value > 0 ? '+' : '') + value;
+ }
+
+ _computeShowHideIcon(showOptionalLabels) {
+ return showOptionalLabels ?
+ 'gr-icons:expand-less' :
+ 'gr-icons:expand-more';
+ }
+
+ _computeSectionClass(show) {
+ return show ? '' : 'hidden';
+ }
+
+ _handleShowHide(e) {
+ this._showOptionalLabels = !this._showOptionalLabels;
+ }
+}
+
+customElements.define(GrChangeRequirements.is, GrChangeRequirements);
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
index 372ae50..311cfe4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
@@ -1,31 +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="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-label/gr-label.html">
-<link rel="import" href="../../shared/gr-label-info/gr-label-info.html">
-<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
-
-<dom-module id="gr-change-requirements">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: table;
@@ -91,59 +82,43 @@
height: var(--spacing-m);
}
</style>
- <template
- is="dom-repeat"
- items="[[_requirements]]">
+ <template is="dom-repeat" items="[[_requirements]]">
<section>
<div class="title requirement">
- <span class$="status [[item.style]]">
+ <span class\$="status [[item.style]]">
<iron-icon class="icon" icon="[[_computeRequirementIcon(item.satisfied)]]"></iron-icon>
</span>
<gr-limited-text class="name" limit="40" text="[[item.fallback_text]]"></gr-limited-text>
</div>
</section>
</template>
- <template
- is="dom-repeat"
- items="[[_requiredLabels]]">
+ <template is="dom-repeat" items="[[_requiredLabels]]">
<section>
<div class="title">
- <span class$="status [[item.style]]">
+ <span class\$="status [[item.style]]">
<iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
</span>
<gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text>
</div>
<div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.label]]"
- label-info="[[item.labelInfo]]"></gr-label-info>
+ <gr-label-info change="{{change}}" account="[[account]]" mutable="[[mutable]]" label="[[item.label]]" label-info="[[item.labelInfo]]"></gr-label-info>
</div>
</section>
</template>
<section class="spacer"></section>
- <section class$="spacer [[_computeShowOptional(_optionalLabels.*)]]"></section>
- <section
- show-bottom-border$="[[_showOptionalLabels]]"
- on-click="_handleShowHide"
- class$="showHide [[_computeShowOptional(_optionalLabels.*)]]">
+ <section class\$="spacer [[_computeShowOptional(_optionalLabels.*)]]"></section>
+ <section show-bottom-border\$="[[_showOptionalLabels]]" on-click="_handleShowHide" class\$="showHide [[_computeShowOptional(_optionalLabels.*)]]">
<div class="title">Other labels</div>
<div class="value">
- <iron-icon
- id="showHide"
- icon="[[_computeShowHideIcon(_showOptionalLabels)]]">
+ <iron-icon id="showHide" icon="[[_computeShowHideIcon(_showOptionalLabels)]]">
</iron-icon>
- </label>
+
</div>
</section>
- <template
- is="dom-repeat"
- items="[[_optionalLabels]]">
- <section class$="optional [[_computeSectionClass(_showOptionalLabels)]]">
+ <template is="dom-repeat" items="[[_optionalLabels]]">
+ <section class\$="optional [[_computeSectionClass(_showOptionalLabels)]]">
<div class="title">
- <span class$="status [[item.style]]">
+ <span class\$="status [[item.style]]">
<template is="dom-if" if="[[item.icon]]">
<iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
</template>
@@ -154,16 +129,9 @@
<gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text>
</div>
<div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.label]]"
- label-info="[[item.labelInfo]]"></gr-label-info>
+ <gr-label-info change="{{change}}" account="[[account]]" mutable="[[mutable]]" label="[[item.label]]" label-info="[[item.labelInfo]]"></gr-label-info>
</div>
</section>
</template>
- <section class$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"></section>
- </template>
- <script src="gr-change-requirements.js"></script>
-</dom-module>
+ <section class\$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"></section>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index 10466db..2883f20 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_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-change-requirements</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-change-requirements.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-change-requirements.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-requirements.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,204 +40,206 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-metadata tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-requirements.js';
+suite('gr-change-metadata tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('requirements computed fields', () => {
- assert.isTrue(element._computeShowWip({work_in_progress: true}));
- assert.isFalse(element._computeShowWip({work_in_progress: false}));
-
- assert.equal(element._computeRequirementClass(true), 'approved');
- assert.equal(element._computeRequirementClass(false), '');
-
- assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
- assert.equal(element._computeRequirementIcon(false),
- 'gr-icons:hourglass');
- });
-
- test('label computed fields', () => {
- assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
- assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
- assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass');
-
- assert.equal(element._computeLabelClass({approved: []}), 'approved');
- assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
- assert.equal(element._computeLabelClass({}), '');
- assert.equal(element._computeLabelClass({value: 0}), '');
-
- assert.equal(element._computeLabelValue(1), '+1');
- assert.equal(element._computeLabelValue(-1), '-1');
- assert.equal(element._computeLabelValue(0), '0');
- });
-
- test('_computeLabels', () => {
- assert.equal(element._optionalLabels.length, 0);
- assert.equal(element._requiredLabels.length, 0);
- element._computeLabels({base: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- value: 1,
- },
- opt_test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- optional: true,
- },
- }});
- assert.equal(element._optionalLabels.length, 1);
- assert.equal(element._requiredLabels.length, 1);
-
- assert.equal(element._optionalLabels[0].label, 'opt_test');
- assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass');
- assert.equal(element._optionalLabels[0].style, '');
- assert.ok(element._optionalLabels[0].labelInfo);
- });
-
- test('optional show/hide', () => {
- element._optionalLabels = [{label: 'test'}];
- flushAsynchronousOperations();
-
- assert.ok(element.shadowRoot
- .querySelector('section.optional'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('.showHide'));
- flushAsynchronousOperations();
-
- assert.isFalse(element._showOptionalLabels);
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('section.optional')));
- });
-
- test('properly converts satisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: [],
- },
- },
- requirements: [],
- };
- flushAsynchronousOperations();
-
- assert.ok(element.shadowRoot
- .querySelector('.approved'));
- assert.ok(element.shadowRoot
- .querySelector('.name'));
- assert.equal(element.shadowRoot
- .querySelector('.name').text, 'Verified');
- });
-
- test('properly converts unsatisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: false,
- },
- },
- };
- flushAsynchronousOperations();
-
- const name = element.shadowRoot
- .querySelector('.name');
- assert.ok(name);
- assert.isFalse(name.hasAttribute('hidden'));
- assert.equal(name.text, 'Verified');
- });
-
- test('properly displays Work In Progress', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [],
- work_in_progress: true,
- };
- flushAsynchronousOperations();
-
- const changeIsWip = element.shadowRoot
- .querySelector('.title');
- assert.ok(changeIsWip);
- });
-
- test('properly displays a satisfied requirement', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.isFalse(requirement.hasAttribute('hidden'));
- assert.ok(requirement.querySelector('.approved'));
- assert.equal(requirement.querySelector('.name').text,
- 'Resolve all comments');
- });
-
- test('satisfied class is applied with OK', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.ok(requirement.querySelector('.approved'));
- });
-
- test('satisfied class is not applied with NOT_READY', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'NOT_READY',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
-
- test('satisfied class is not applied with RULE_ERROR', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'RULE_ERROR',
- }],
- };
- flushAsynchronousOperations();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('requirements computed fields', () => {
+ assert.isTrue(element._computeShowWip({work_in_progress: true}));
+ assert.isFalse(element._computeShowWip({work_in_progress: false}));
+
+ assert.equal(element._computeRequirementClass(true), 'approved');
+ assert.equal(element._computeRequirementClass(false), '');
+
+ assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
+ assert.equal(element._computeRequirementIcon(false),
+ 'gr-icons:hourglass');
+ });
+
+ test('label computed fields', () => {
+ assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
+ assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
+ assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass');
+
+ assert.equal(element._computeLabelClass({approved: []}), 'approved');
+ assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
+ assert.equal(element._computeLabelClass({}), '');
+ assert.equal(element._computeLabelClass({value: 0}), '');
+
+ assert.equal(element._computeLabelValue(1), '+1');
+ assert.equal(element._computeLabelValue(-1), '-1');
+ assert.equal(element._computeLabelValue(0), '0');
+ });
+
+ test('_computeLabels', () => {
+ assert.equal(element._optionalLabels.length, 0);
+ assert.equal(element._requiredLabels.length, 0);
+ element._computeLabels({base: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ value: 1,
+ },
+ opt_test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ optional: true,
+ },
+ }});
+ assert.equal(element._optionalLabels.length, 1);
+ assert.equal(element._requiredLabels.length, 1);
+
+ assert.equal(element._optionalLabels[0].label, 'opt_test');
+ assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass');
+ assert.equal(element._optionalLabels[0].style, '');
+ assert.ok(element._optionalLabels[0].labelInfo);
+ });
+
+ test('optional show/hide', () => {
+ element._optionalLabels = [{label: 'test'}];
+ flushAsynchronousOperations();
+
+ assert.ok(element.shadowRoot
+ .querySelector('section.optional'));
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.showHide'));
+ flushAsynchronousOperations();
+
+ assert.isFalse(element._showOptionalLabels);
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('section.optional')));
+ });
+
+ test('properly converts satisfied labels', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: [],
+ },
+ },
+ requirements: [],
+ };
+ flushAsynchronousOperations();
+
+ assert.ok(element.shadowRoot
+ .querySelector('.approved'));
+ assert.ok(element.shadowRoot
+ .querySelector('.name'));
+ assert.equal(element.shadowRoot
+ .querySelector('.name').text, 'Verified');
+ });
+
+ test('properly converts unsatisfied labels', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: false,
+ },
+ },
+ };
+ flushAsynchronousOperations();
+
+ const name = element.shadowRoot
+ .querySelector('.name');
+ assert.ok(name);
+ assert.isFalse(name.hasAttribute('hidden'));
+ assert.equal(name.text, 'Verified');
+ });
+
+ test('properly displays Work In Progress', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [],
+ work_in_progress: true,
+ };
+ flushAsynchronousOperations();
+
+ const changeIsWip = element.shadowRoot
+ .querySelector('.title');
+ assert.ok(changeIsWip);
+ });
+
+ test('properly displays a satisfied requirement', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.isFalse(requirement.hasAttribute('hidden'));
+ assert.ok(requirement.querySelector('.approved'));
+ assert.equal(requirement.querySelector('.name').text,
+ 'Resolve all comments');
+ });
+
+ test('satisfied class is applied with OK', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.ok(requirement.querySelector('.approved'));
+ });
+
+ test('satisfied class is not applied with NOT_READY', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'NOT_READY',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.strictEqual(requirement.querySelector('.approved'), null);
+ });
+
+ test('satisfied class is not applied with RULE_ERROR', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'RULE_ERROR',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const requirement = element.shadowRoot
+ .querySelector('.requirement');
+ assert.ok(requirement);
+ assert.strictEqual(requirement.querySelector('.approved'), null);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 58505ff..7a80d34 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.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,2064 +14,2111 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const CHANGE_ID_ERROR = {
- MISMATCH: 'mismatch',
- MISSING: 'missing',
- };
- const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/paper-tabs/paper-tabs.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../edit/gr-edit-constants.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-account-link/gr-account-link.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-change-star/gr-change-star.js';
+import '../../shared/gr-change-status/gr-change-status.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-editable-content/gr-editable-content.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-linked-text/gr-linked-text.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../shared/revision-info/revision-info.js';
+import '../gr-change-actions/gr-change-actions.js';
+import '../gr-change-metadata/gr-change-metadata.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../gr-commit-info/gr-commit-info.js';
+import '../gr-download-dialog/gr-download-dialog.js';
+import '../gr-file-list-header/gr-file-list-header.js';
+import '../gr-file-list/gr-file-list.js';
+import '../gr-included-in-dialog/gr-included-in-dialog.js';
+import '../gr-messages-list/gr-messages-list.js';
+import '../gr-related-changes-list/gr-related-changes-list.js';
+import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js';
+import '../gr-reply-dialog/gr-reply-dialog.js';
+import '../gr-thread-list/gr-thread-list.js';
+import '../gr-upload-help-dialog/gr-upload-help-dialog.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {beforeNextRender} from '@polymer/polymer/lib/utils/render-status.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-change-view_html.js';
- const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
- const DEFAULT_NUM_FILES_SHOWN = 200;
+const CHANGE_ID_ERROR = {
+ MISMATCH: 'mismatch',
+ MISSING: 'missing',
+};
+const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm;
- const REVIEWERS_REGEX = /^(R|CC)=/gm;
- const MIN_CHECK_INTERVAL_SECS = 0;
+const MIN_LINES_FOR_COMMIT_COLLAPSE = 30;
+const DEFAULT_NUM_FILES_SHOWN = 200;
- // These are the same as the breakpoint set in CSS. Make sure both are changed
- // together.
- const BREAKPOINT_RELATED_SMALL = '50em';
- const BREAKPOINT_RELATED_MED = '75em';
+const REVIEWERS_REGEX = /^(R|CC)=/gm;
+const MIN_CHECK_INTERVAL_SECS = 0;
- // In the event that the related changes medium width calculation is too close
- // to zero, provide some height.
- const MINIMUM_RELATED_MAX_HEIGHT = 100;
+// These are the same as the breakpoint set in CSS. Make sure both are changed
+// together.
+const BREAKPOINT_RELATED_SMALL = '50em';
+const BREAKPOINT_RELATED_MED = '75em';
- const SMALL_RELATED_HEIGHT = 400;
+// In the event that the related changes medium width calculation is too close
+// to zero, provide some height.
+const MINIMUM_RELATED_MAX_HEIGHT = 100;
- const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
+const SMALL_RELATED_HEIGHT = 400;
- const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
+const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
- const MSG_PREFIX = '#message-';
+const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
- const ReloadToastMessage = {
- NEWER_REVISION: 'A newer patch set has been uploaded',
- RESTORED: 'This change has been restored',
- ABANDONED: 'This change has been abandoned',
- MERGED: 'This change has been merged',
- NEW_MESSAGE: 'There are new messages on this change',
- };
+const MSG_PREFIX = '#message-';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const ReloadToastMessage = {
+ NEWER_REVISION: 'A newer patch set has been uploaded',
+ RESTORED: 'This change has been restored',
+ ABANDONED: 'This change has been abandoned',
+ MERGED: 'This change has been merged',
+ NEW_MESSAGE: 'There are new messages on this change',
+};
- const CommentTabs = {
- CHANGE_LOG: 0,
- COMMENT_THREADS: 1,
- ROBOT_COMMENTS: 2,
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded';
- const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded';
- const SEND_REPLY_TIMING_LABEL = 'SendReply';
- // Making the tab names more unique in case a plugin adds one with same name
- const FILES_TAB_NAME = '__gerrit_internal_files';
- const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
- const ROBOT_COMMENTS_LIMIT = 10;
+const CommentTabs = {
+ CHANGE_LOG: 0,
+ COMMENT_THREADS: 1,
+ ROBOT_COMMENTS: 2,
+};
+
+const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded';
+const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded';
+const SEND_REPLY_TIMING_LABEL = 'SendReply';
+// Making the tab names more unique in case a plugin adds one with same name
+const FILES_TAB_NAME = '__gerrit_internal_files';
+const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
+const ROBOT_COMMENTS_LIMIT = 10;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrChangeView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-change-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired if an error occurs when fetching the change data.
+ *
+ * @event page-error
*/
- class GrChangeView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-view'; }
+
+ /**
+ * Fired if being logged in is required.
+ *
+ * @event show-auth-required
+ */
+
+ static get properties() {
+ return {
/**
- * Fired when the title of the page should change.
- *
- * @event title-change
+ * URL params passed from the router.
*/
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+ /** @type {?} */
+ viewState: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ observer: '_viewStateChanged',
+ },
+ backPage: String,
+ hasParent: Boolean,
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ disableEdit: {
+ type: Boolean,
+ value: false,
+ },
+ disableDiffPrefs: {
+ type: Boolean,
+ value: false,
+ },
+ _diffPrefsDisabled: {
+ type: Boolean,
+ computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+ },
+ _commentThreads: Array,
+ // TODO(taoalpha): Consider replacing diffDrafts
+ // with _draftCommentThreads everywhere, currently only
+ // replaced in reply-dialoig
+ _draftCommentThreads: {
+ type: Array,
+ },
+ _robotCommentThreads: {
+ type: Array,
+ computed: '_computeRobotCommentThreads(_commentThreads,'
+ + ' _currentRobotCommentsPatchSet, _showAllRobotComments)',
+ },
+ /** @type {?} */
+ _serverConfig: {
+ type: Object,
+ observer: '_startUpdateCheckTimer',
+ },
+ _diffPrefs: Object,
+ _numFilesShown: {
+ type: Number,
+ value: DEFAULT_NUM_FILES_SHOWN,
+ observer: '_numFilesShownChanged',
+ },
+ _account: {
+ type: Object,
+ value: {},
+ },
+ _prefs: Object,
+ /** @type {?} */
+ _changeComments: Object,
+ _canStartReview: {
+ type: Boolean,
+ computed: '_computeCanStartReview(_change)',
+ },
+ _comments: Object,
+ /** @type {?} */
+ _change: {
+ type: Object,
+ observer: '_changeChanged',
+ },
+ _revisionInfo: {
+ type: Object,
+ computed: '_getRevisionInfo(_change)',
+ },
+ /** @type {?} */
+ _commitInfo: Object,
+ _currentRevision: {
+ type: Object,
+ computed: '_computeCurrentRevision(_change.current_revision, ' +
+ '_change.revisions)',
+ observer: '_handleCurrentRevisionUpdate',
+ },
+ _files: Object,
+ _changeNum: String,
+ _diffDrafts: {
+ type: Object,
+ value() { return {}; },
+ },
+ _editingCommitMessage: {
+ type: Boolean,
+ value: false,
+ },
+ _hideEditCommitMessage: {
+ type: Boolean,
+ computed: '_computeHideEditCommitMessage(_loggedIn, ' +
+ '_editingCommitMessage, _change, _editMode, _commitCollapsed, ' +
+ '_commitCollapsible)',
+ },
+ _diffAgainst: String,
+ /** @type {?string} */
+ _latestCommitMessage: {
+ type: String,
+ value: '',
+ },
+ _commentTabs: {
+ type: Object,
+ value: CommentTabs,
+ },
+ _lineHeight: Number,
+ _changeIdCommitMessageError: {
+ type: String,
+ computed:
+ '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)',
+ },
+ /** @type {?} */
+ _patchRange: {
+ type: Object,
+ },
+ _filesExpanded: String,
+ _basePatchNum: String,
+ _selectedRevision: Object,
+ _currentRevisionActions: Object,
+ _allPatchSets: {
+ type: Array,
+ computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: Boolean,
+ /** @type {?} */
+ _projectConfig: Object,
+ _rebaseOnCurrent: Boolean,
+ _replyButtonLabel: {
+ type: String,
+ value: 'Reply',
+ computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)',
+ },
+ _selectedPatchSet: String,
+ _shownFileCount: Number,
+ _initialLoadComplete: {
+ type: Boolean,
+ value: false,
+ },
+ _replyDisabled: {
+ type: Boolean,
+ value: true,
+ computed: '_computeReplyDisabled(_serverConfig)',
+ },
+ _changeStatus: {
+ type: String,
+ computed: 'changeStatusString(_change)',
+ },
+ _changeStatuses: {
+ type: String,
+ computed:
+ '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)',
+ },
+ /** If false, then the "Show more" button was used to expand. */
+ _commitCollapsed: {
+ type: Boolean,
+ value: true,
+ },
+ /** Is the "Show more/less" button visible? */
+ _commitCollapsible: {
+ type: Boolean,
+ computed: '_computeCommitCollapsible(_latestCommitMessage)',
+ },
+ _relatedChangesCollapsed: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {?number} */
+ _updateCheckTimerHandle: Number,
+ _editMode: {
+ type: Boolean,
+ computed: '_computeEditMode(_patchRange.*, params.*)',
+ },
+ _showRelatedToggle: {
+ type: Boolean,
+ value: false,
+ observer: '_updateToggleContainerClass',
+ },
+ _parentIsCurrent: Boolean,
+ _submitEnabled: {
+ type: Boolean,
+ computed: '_isSubmitEnabled(_currentRevisionActions)',
+ },
- /**
- * Fired if an error occurs when fetching the change data.
- *
- * @event page-error
- */
+ /** @type {?} */
+ _mergeable: {
+ type: Boolean,
+ value: undefined,
+ },
+ _currentView: {
+ type: Number,
+ value: CommentTabs.CHANGE_LOG,
+ },
+ _showFileTabContent: {
+ type: Boolean,
+ value: true,
+ },
+ /** @type {Array<string>} */
+ _dynamicTabHeaderEndpoints: {
+ type: Array,
+ },
+ /** @type {Array<string>} */
+ _dynamicTabContentEndpoints: {
+ type: Array,
+ },
+ // The dynamic content of the plugin added tab
+ _selectedTabPluginEndpoint: {
+ type: String,
+ },
+ // The dynamic heading of the plugin added tab
+ _selectedTabPluginHeader: {
+ type: String,
+ },
+ _robotCommentsPatchSetDropdownItems: {
+ type: Array,
+ value() { return []; },
+ computed: '_computeRobotCommentsPatchSetDropdownItems(_change, ' +
+ '_commentThreads)',
+ },
+ _currentRobotCommentsPatchSet: {
+ type: Number,
+ },
+ _files_tab_name: {
+ type: String,
+ value: FILES_TAB_NAME,
+ },
+ _findings_tab_name: {
+ type: String,
+ value: FINDINGS_TAB_NAME,
+ },
+ _currentTabName: {
+ type: String,
+ value: FILES_TAB_NAME,
+ },
+ _showAllRobotComments: {
+ type: Boolean,
+ value: false,
+ },
+ _showRobotCommentsButton: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /**
- * Fired if being logged in is required.
- *
- * @event show-auth-required
- */
+ static get observers() {
+ return [
+ '_labelsChanged(_change.labels.*)',
+ '_paramsAndChangeChanged(params, _change)',
+ '_patchNumChanged(_patchRange.patchNum)',
+ ];
+ }
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- /** @type {?} */
- viewState: {
- type: Object,
- notify: true,
- value() { return {}; },
- observer: '_viewStateChanged',
- },
- backPage: String,
- hasParent: Boolean,
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- disableEdit: {
- type: Boolean,
- value: false,
- },
- disableDiffPrefs: {
- type: Boolean,
- value: false,
- },
- _diffPrefsDisabled: {
- type: Boolean,
- computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
- },
- _commentThreads: Array,
- // TODO(taoalpha): Consider replacing diffDrafts
- // with _draftCommentThreads everywhere, currently only
- // replaced in reply-dialoig
- _draftCommentThreads: {
- type: Array,
- },
- _robotCommentThreads: {
- type: Array,
- computed: '_computeRobotCommentThreads(_commentThreads,'
- + ' _currentRobotCommentsPatchSet, _showAllRobotComments)',
- },
- /** @type {?} */
- _serverConfig: {
- type: Object,
- observer: '_startUpdateCheckTimer',
- },
- _diffPrefs: Object,
- _numFilesShown: {
- type: Number,
- value: DEFAULT_NUM_FILES_SHOWN,
- observer: '_numFilesShownChanged',
- },
- _account: {
- type: Object,
- value: {},
- },
- _prefs: Object,
- /** @type {?} */
- _changeComments: Object,
- _canStartReview: {
- type: Boolean,
- computed: '_computeCanStartReview(_change)',
- },
- _comments: Object,
- /** @type {?} */
- _change: {
- type: Object,
- observer: '_changeChanged',
- },
- _revisionInfo: {
- type: Object,
- computed: '_getRevisionInfo(_change)',
- },
- /** @type {?} */
- _commitInfo: Object,
- _currentRevision: {
- type: Object,
- computed: '_computeCurrentRevision(_change.current_revision, ' +
- '_change.revisions)',
- observer: '_handleCurrentRevisionUpdate',
- },
- _files: Object,
- _changeNum: String,
- _diffDrafts: {
- type: Object,
- value() { return {}; },
- },
- _editingCommitMessage: {
- type: Boolean,
- value: false,
- },
- _hideEditCommitMessage: {
- type: Boolean,
- computed: '_computeHideEditCommitMessage(_loggedIn, ' +
- '_editingCommitMessage, _change, _editMode, _commitCollapsed, ' +
- '_commitCollapsible)',
- },
- _diffAgainst: String,
- /** @type {?string} */
- _latestCommitMessage: {
- type: String,
- value: '',
- },
- _commentTabs: {
- type: Object,
- value: CommentTabs,
- },
- _lineHeight: Number,
- _changeIdCommitMessageError: {
- type: String,
- computed:
- '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)',
- },
- /** @type {?} */
- _patchRange: {
- type: Object,
- },
- _filesExpanded: String,
- _basePatchNum: String,
- _selectedRevision: Object,
- _currentRevisionActions: Object,
- _allPatchSets: {
- type: Array,
- computed: 'computeAllPatchSets(_change, _change.revisions.*)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _loading: Boolean,
- /** @type {?} */
- _projectConfig: Object,
- _rebaseOnCurrent: Boolean,
- _replyButtonLabel: {
- type: String,
- value: 'Reply',
- computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)',
- },
- _selectedPatchSet: String,
- _shownFileCount: Number,
- _initialLoadComplete: {
- type: Boolean,
- value: false,
- },
- _replyDisabled: {
- type: Boolean,
- value: true,
- computed: '_computeReplyDisabled(_serverConfig)',
- },
- _changeStatus: {
- type: String,
- computed: 'changeStatusString(_change)',
- },
- _changeStatuses: {
- type: String,
- computed:
- '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)',
- },
- /** If false, then the "Show more" button was used to expand. */
- _commitCollapsed: {
- type: Boolean,
- value: true,
- },
- /** Is the "Show more/less" button visible? */
- _commitCollapsible: {
- type: Boolean,
- computed: '_computeCommitCollapsible(_latestCommitMessage)',
- },
- _relatedChangesCollapsed: {
- type: Boolean,
- value: true,
- },
- /** @type {?number} */
- _updateCheckTimerHandle: Number,
- _editMode: {
- type: Boolean,
- computed: '_computeEditMode(_patchRange.*, params.*)',
- },
- _showRelatedToggle: {
- type: Boolean,
- value: false,
- observer: '_updateToggleContainerClass',
- },
- _parentIsCurrent: Boolean,
- _submitEnabled: {
- type: Boolean,
- computed: '_isSubmitEnabled(_currentRevisionActions)',
- },
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
+ [this.Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding
+ [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
+ [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
+ [this.Shortcut.OPEN_DOWNLOAD_DIALOG]:
+ '_handleOpenDownloadDialogShortcut',
+ [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+ [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
+ [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
+ [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
+ [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
+ [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
+ [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic',
+ };
+ }
- /** @type {?} */
- _mergeable: {
- type: Boolean,
- value: undefined,
- },
- _currentView: {
- type: Number,
- value: CommentTabs.CHANGE_LOG,
- },
- _showFileTabContent: {
- type: Boolean,
- value: true,
- },
- /** @type {Array<string>} */
- _dynamicTabHeaderEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicTabContentEndpoints: {
- type: Array,
- },
- // The dynamic content of the plugin added tab
- _selectedTabPluginEndpoint: {
- type: String,
- },
- // The dynamic heading of the plugin added tab
- _selectedTabPluginHeader: {
- type: String,
- },
- _robotCommentsPatchSetDropdownItems: {
- type: Array,
- value() { return []; },
- computed: '_computeRobotCommentsPatchSetDropdownItems(_change, ' +
- '_commentThreads)',
- },
- _currentRobotCommentsPatchSet: {
- type: Number,
- },
- _files_tab_name: {
- type: String,
- value: FILES_TAB_NAME,
- },
- _findings_tab_name: {
- type: String,
- value: FINDINGS_TAB_NAME,
- },
- _currentTabName: {
- type: String,
- value: FILES_TAB_NAME,
- },
- _showAllRobotComments: {
- type: Boolean,
- value: false,
- },
- _showRobotCommentsButton: {
- type: Boolean,
- value: false,
- },
- };
- }
+ /** @override */
+ created() {
+ super.created();
- static get observers() {
- return [
- '_labelsChanged(_change.labels.*)',
- '_paramsAndChangeChanged(params, _change)',
- '_patchNumChanged(_patchRange.patchNum)',
- ];
- }
+ this.addEventListener('topic-changed',
+ () => this._handleTopicChanged());
- keyboardShortcuts() {
- return {
- [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
- [this.Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding
- [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
- [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
- [this.Shortcut.OPEN_DOWNLOAD_DIALOG]:
- '_handleOpenDownloadDialogShortcut',
- [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
- [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
- [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
- [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
- [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
- [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
- [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic',
- };
- }
+ this.addEventListener(
+ // When an overlay is opened in a mobile viewport, the overlay has a full
+ // screen view. When it has a full screen view, we do not want the
+ // background to be scrollable. This will eliminate background scroll by
+ // hiding most of the contents on the screen upon opening, and showing
+ // again upon closing.
+ 'fullscreen-overlay-opened',
+ () => this._handleHideBackgroundContent());
- /** @override */
- created() {
- super.created();
-
- this.addEventListener('topic-changed',
- () => this._handleTopicChanged());
-
- this.addEventListener(
- // When an overlay is opened in a mobile viewport, the overlay has a full
- // screen view. When it has a full screen view, we do not want the
- // background to be scrollable. This will eliminate background scroll by
- // hiding most of the contents on the screen upon opening, and showing
- // again upon closing.
- 'fullscreen-overlay-opened',
- () => this._handleHideBackgroundContent());
-
- this.addEventListener('fullscreen-overlay-closed',
- () => this._handleShowBackgroundContent());
-
- this.addEventListener('diff-comments-modified',
- () => this._handleReloadCommentThreads());
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getServerConfig().then(config => {
- this._serverConfig = config;
- });
-
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this.$.restAPI.getAccount().then(acct => {
- this._account = acct;
- });
- }
- this._setDiffViewMode();
- });
-
- Gerrit.awaitPluginsLoaded()
- .then(() => {
- this._dynamicTabHeaderEndpoints =
- Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header');
- this._dynamicTabContentEndpoints =
- Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content');
- if (this._dynamicTabContentEndpoints.length !==
- this._dynamicTabHeaderEndpoints.length) {
- console.warn('Different number of tab headers and tab content.');
- }
- })
- .then(() => this._setPrimaryTab());
-
- this.addEventListener('comment-save', this._handleCommentSave.bind(this));
- this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
- this.addEventListener('comment-discard',
- this._handleCommentDiscard.bind(this));
- this.addEventListener('change-message-deleted',
- () => this._reload());
- this.addEventListener('editable-content-save',
- this._handleCommitMessageSave.bind(this));
- this.addEventListener('editable-content-cancel',
- this._handleCommitMessageCancel.bind(this));
- this.addEventListener('open-fix-preview',
- this._onOpenFixPreview.bind(this));
- this.addEventListener('close-fix-preview',
- this._onCloseFixPreview.bind(this));
- this.listen(window, 'scroll', '_handleScroll');
- this.listen(document, 'visibilitychange', '_handleVisibilityChange');
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_handleScroll');
- this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
-
- if (this._updateCheckTimerHandle) {
- this._cancelUpdateCheckTimer();
- }
- }
-
- get messagesList() {
- return this.shadowRoot.querySelector('gr-messages-list');
- }
-
- get threadList() {
- return this.shadowRoot.querySelector('gr-thread-list');
- }
-
- /**
- * @param {boolean=} opt_reset
- */
- _setDiffViewMode(opt_reset) {
- if (!opt_reset && this.viewState.diffViewMode) { return; }
-
- return this._getPreferences()
- .then( prefs => {
- if (!this.viewState.diffMode) {
- this.set('viewState.diffMode', prefs.default_diff_view);
- }
- })
- .then(() => {
- if (!this.viewState.diffMode) {
- this.set('viewState.diffMode', 'SIDE_BY_SIDE');
- }
- });
- }
-
- _onOpenFixPreview(e) {
- this.$.applyFixDialog.open(e);
- }
-
- _onCloseFixPreview(e) {
- this._reload();
- }
-
- _handleToggleDiffMode(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED);
- } else {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE);
- }
- }
-
- _handleCommentTabChange() {
- this._currentView = this.$.commentTabs.selected;
- const type = Object.keys(CommentTabs).find(key => CommentTabs[key] ===
- this._currentView);
- this.$.reporting.reportInteraction('comment-tab-changed', {tabName:
- type});
- }
-
- _isSelectedView(currentView, view) {
- return currentView === view;
- }
-
- _findIfTabMatches(currentTab, tab) {
- return currentTab === tab;
- }
-
- _handleFileTabChange(e) {
- const selectedIndex = e.target.selected;
- const tabs = e.target.querySelectorAll('paper-tab');
- this._currentTabName = tabs[selectedIndex] &&
- tabs[selectedIndex].dataset.name;
- const source = e && e.type ? e.type : '';
- const pluginIndex = (this._dynamicTabHeaderEndpoints || []).indexOf(
- this._currentTabName);
- if (pluginIndex !== -1) {
- this._selectedTabPluginEndpoint = this._dynamicTabContentEndpoints[
- pluginIndex];
- this._selectedTabPluginHeader = this._dynamicTabHeaderEndpoints[
- pluginIndex];
- } else {
- this._selectedTabPluginEndpoint = '';
- this._selectedTabPluginHeader = '';
- }
- this.$.reporting.reportInteraction('tab-changed',
- {tabName: this._currentTabName, source});
- }
-
- _handleShowTab(e) {
- const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
- const tabs = primaryTabs.querySelectorAll('paper-tab');
- let idx = -1;
- tabs.forEach((tab, index) => {
- if (tab.dataset.name === e.detail.tab) idx = index;
- });
- if (idx === -1) {
- console.error(e.detail.tab + ' tab not found');
- return;
- }
- primaryTabs.selected = idx;
- primaryTabs.scrollIntoView();
- this.$.reporting.reportInteraction('show-tab', {tabName: e.detail.tab});
- }
-
- _handleEditCommitMessage(e) {
- this._editingCommitMessage = true;
- this.$.commitMessageEditor.focusTextarea();
- }
-
- _handleCommitMessageSave(e) {
- // Trim trailing whitespace from each line.
- const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
-
- this.$.jsAPI.handleCommitMessage(this._change, message);
-
- this.$.commitMessageEditor.disabled = true;
- this.$.restAPI.putChangeCommitMessage(
- this._changeNum, message).then(resp => {
- this.$.commitMessageEditor.disabled = false;
- if (!resp.ok) { return; }
-
- this._latestCommitMessage = this._prepareCommitMsgForLinkify(
- message);
- this._editingCommitMessage = false;
- this._reloadWindow();
- })
- .catch(err => {
- this.$.commitMessageEditor.disabled = false;
- });
- }
-
- _reloadWindow() {
- window.location.reload();
- }
-
- _handleCommitMessageCancel(e) {
- this._editingCommitMessage = false;
- }
-
- _computeChangeStatusChips(change, mergeable, submitEnabled) {
- // Polymer 2: check for undefined
- if ([
- change,
- mergeable,
- ].some(arg => arg === undefined)) {
- // To keep consistent with Polymer 1, we are returning undefined
- // if not all dependencies are defined
- return undefined;
- }
-
- // Show no chips until mergeability is loaded.
- if (mergeable === null) {
- return [];
- }
-
- const options = {
- includeDerived: true,
- mergeable: !!mergeable,
- submitEnabled: !!submitEnabled,
- };
- return this.changeStatuses(change, options);
- }
-
- _computeHideEditCommitMessage(
- loggedIn, editing, change, editMode, collapsed, collapsible) {
- if (!loggedIn || editing ||
- (change && change.status === this.ChangeStatus.MERGED) ||
- editMode ||
- (collapsed && collapsible)) {
- return true;
- }
-
- return false;
- }
-
- _robotCommentCountPerPatchSet(threads) {
- return threads.reduce((robotCommentCountMap, thread) => {
- const comments = thread.comments;
- const robotCommentsCount = comments.reduce((acc, comment) =>
- (comment.robot_id ? acc + 1 : acc), 0);
- robotCommentCountMap[comments[0].patch_set] =
- (robotCommentCountMap[comments[0].patch_set] || 0) +
- robotCommentsCount;
- return robotCommentCountMap;
- }, {});
- }
-
- _computeText(patch, commentThreads) {
- const commentCount = this._robotCommentCountPerPatchSet(commentThreads);
- const commentCnt = commentCount[patch._number] || 0;
- if (commentCnt === 0) return `Patchset ${patch._number}`;
- const findingsText = commentCnt === 1 ? 'finding' : 'findings';
- return `Patchset ${patch._number}`
- + ` (${commentCnt} ${findingsText})`;
- }
-
- _computeRobotCommentsPatchSetDropdownItems(change, commentThreads) {
- if (!change || !commentThreads || !change.revisions) return [];
-
- return Object.values(change.revisions)
- .filter(patch => patch._number !== 'edit')
- .map(patch => {
- return {
- text: this._computeText(patch, commentThreads),
- value: patch._number,
- };
- })
- .sort((a, b) => b.value - a.value);
- }
-
- _handleCurrentRevisionUpdate(currentRevision) {
- this._currentRobotCommentsPatchSet = currentRevision._number;
- }
-
- _handleRobotCommentPatchSetChanged(e) {
- const patchSet = parseInt(e.detail.value);
- if (patchSet === this._currentRobotCommentsPatchSet) return;
- this._currentRobotCommentsPatchSet = patchSet;
- }
-
- _computeShowText(showAllRobotComments) {
- return showAllRobotComments ? 'Show Less' : 'Show more';
- }
-
- _toggleShowRobotComments() {
- this._showAllRobotComments = !this._showAllRobotComments;
- }
-
- _computeRobotCommentThreads(commentThreads, currentRobotCommentsPatchSet,
- showAllRobotComments) {
- if (!commentThreads || !currentRobotCommentsPatchSet) return [];
- const threads = commentThreads.filter(thread => {
- const comments = thread.comments || [];
- return comments.length && comments[0].robot_id && (comments[0].patch_set
- === currentRobotCommentsPatchSet);
- });
- this._showRobotCommentsButton = threads.length > ROBOT_COMMENTS_LIMIT;
- return threads.slice(0, showAllRobotComments ? undefined :
- ROBOT_COMMENTS_LIMIT);
- }
-
- _handleReloadCommentThreads() {
- // Get any new drafts that have been saved in the diff view and show
- // in the comment thread view.
- this._reloadDrafts().then(() => {
- this._commentThreads = this._changeComments.getAllThreadsForChange()
- .map(c => Object.assign({}, c));
- Polymer.dom.flush();
- });
- }
-
- _handleReloadDiffComments(e) {
- // Keeps the file list counts updated.
- this._reloadDrafts().then(() => {
- // Get any new drafts that have been saved in the thread view and show
- // in the diff view.
- this.$.fileList.reloadCommentsForThreadWithRootId(e.detail.rootId,
- e.detail.path);
- Polymer.dom.flush();
- });
- }
-
- _computeTotalCommentCounts(unresolvedCount, changeComments) {
- if (!changeComments) return undefined;
- const draftCount = changeComments.computeDraftCount();
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
- const draftString = GrCountStringFormatter.computePluralString(
- draftCount, 'draft');
-
- return unresolvedString +
- // Add a comma and space if both unresolved and draft comments exist.
- (unresolvedString && draftString ? ', ' : '') +
- draftString;
- }
-
- _handleCommentSave(e) {
- const draft = e.detail.comment;
- if (!draft.__draft) { return; }
-
- draft.patch_set = draft.patch_set || this._patchRange.patchNum;
-
- // The use of path-based notification helpers (set, push) can’t be used
- // because the paths could contain dots in them. A new object must be
- // created to satisfy Polymer’s dirty checking.
- // https://github.com/Polymer/polymer/issues/3127
- const diffDrafts = Object.assign({}, this._diffDrafts);
- if (!diffDrafts[draft.path]) {
- diffDrafts[draft.path] = [draft];
- this._diffDrafts = diffDrafts;
- return;
- }
- for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
- if (this._diffDrafts[draft.path][i].id === draft.id) {
- diffDrafts[draft.path][i] = draft;
- this._diffDrafts = diffDrafts;
- return;
- }
- }
- diffDrafts[draft.path].push(draft);
- diffDrafts[draft.path].sort((c1, c2) =>
- // No line number means that it’s a file comment. Sort it above the
- // others.
- (c1.line || -1) - (c2.line || -1)
- );
- this._diffDrafts = diffDrafts;
- }
-
- _handleCommentDiscard(e) {
- const draft = e.detail.comment;
- if (!draft.__draft) { return; }
-
- if (!this._diffDrafts[draft.path]) {
- return;
- }
- let index = -1;
- for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
- if (this._diffDrafts[draft.path][i].id === draft.id) {
- index = i;
- break;
- }
- }
- if (index === -1) {
- // It may be a draft that hasn’t been added to _diffDrafts since it was
- // never saved.
- return;
- }
-
- draft.patch_set = draft.patch_set || this._patchRange.patchNum;
-
- // The use of path-based notification helpers (set, push) can’t be used
- // because the paths could contain dots in them. A new object must be
- // created to satisfy Polymer’s dirty checking.
- // https://github.com/Polymer/polymer/issues/3127
- const diffDrafts = Object.assign({}, this._diffDrafts);
- diffDrafts[draft.path].splice(index, 1);
- if (diffDrafts[draft.path].length === 0) {
- delete diffDrafts[draft.path];
- }
- this._diffDrafts = diffDrafts;
- }
-
- _handleReplyTap(e) {
- e.preventDefault();
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
- }
-
- _handleOpenDiffPrefs() {
- this.$.fileList.openDiffPrefs();
- }
-
- _handleOpenIncludedInDialog() {
- this.$.includedInDialog.loadData().then(() => {
- Polymer.dom.flush();
- this.$.includedInOverlay.refit();
- });
- this.$.includedInOverlay.open();
- }
-
- _handleIncludedInDialogClose(e) {
- this.$.includedInOverlay.close();
- }
-
- _handleOpenDownloadDialog() {
- this.$.downloadOverlay.open().then(() => {
- this.$.downloadOverlay
- .setFocusStops(this.$.downloadDialog.getFocusStops());
- this.$.downloadDialog.focus();
- });
- }
-
- _handleDownloadDialogClose(e) {
- this.$.downloadOverlay.close();
- }
-
- _handleOpenUploadHelpDialog(e) {
- this.$.uploadHelpOverlay.open();
- }
-
- _handleCloseUploadHelpDialog(e) {
- this.$.uploadHelpOverlay.close();
- }
-
- _handleMessageReply(e) {
- const msg = e.detail.message.message;
- const quoteStr = msg.split('\n').map(
- line => '> ' + line)
- .join('\n') + '\n\n';
- this.$.replyDialog.quote = quoteStr;
- this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
- }
-
- _handleHideBackgroundContent() {
- this.$.mainContent.classList.add('overlayOpen');
- }
-
- _handleShowBackgroundContent() {
- this.$.mainContent.classList.remove('overlayOpen');
- }
-
- _handleReplySent(e) {
- this.addEventListener('change-details-loaded',
- () => {
- this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
- }, {once: true});
- this.$.replyOverlay.close();
- this._reload();
- }
-
- _handleReplyCancel(e) {
- this.$.replyOverlay.close();
- }
-
- _handleReplyAutogrow(e) {
- // If the textarea resizes, we need to re-fit the overlay.
- this.debounce('reply-overlay-refit', () => {
- this.$.replyOverlay.refit();
- }, REPLY_REFIT_DEBOUNCE_INTERVAL_MS);
- }
-
- _handleShowReplyDialog(e) {
- let target = this.$.replyDialog.FocusTarget.REVIEWERS;
- if (e.detail.value && e.detail.value.ccsOnly) {
- target = this.$.replyDialog.FocusTarget.CCS;
- }
- this._openReplyDialog(target);
- }
-
- _handleScroll() {
- this.debounce('scroll', () => {
- this.viewState.scrollTop = document.body.scrollTop;
- }, 150);
- }
-
- _setShownFiles(e) {
- this._shownFileCount = e.detail.length;
- }
-
- _expandAllDiffs() {
- this.$.fileList.expandAllDiffs();
- }
-
- _collapseAllDiffs() {
- this.$.fileList.collapseAllDiffs();
- }
-
- _paramsChanged(value) {
- this._currentView = CommentTabs.CHANGE_LOG;
- this._setPrimaryTab();
- if (value.view !== Gerrit.Nav.View.CHANGE) {
- this._initialLoadComplete = false;
- return;
- }
-
- if (value.changeNum && value.project) {
- this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
- }
-
- const patchChanged = this._patchRange &&
- (value.patchNum !== undefined && value.basePatchNum !== undefined) &&
- (this._patchRange.patchNum !== value.patchNum ||
- this._patchRange.basePatchNum !== value.basePatchNum);
-
- if (this._changeNum !== value.changeNum) {
- this._initialLoadComplete = false;
- }
+ this.addEventListener('fullscreen-overlay-closed',
+ () => this._handleShowBackgroundContent());
- const patchRange = {
- patchNum: value.patchNum,
- basePatchNum: value.basePatchNum || 'PARENT',
- };
+ this.addEventListener('diff-comments-modified',
+ () => this._handleReloadCommentThreads());
+ }
- this.$.fileList.collapseAllDiffs();
- this._patchRange = patchRange;
+ /** @override */
+ attached() {
+ super.attached();
+ this._getServerConfig().then(config => {
+ this._serverConfig = config;
+ });
- // If the change has already been loaded and the parameter change is only
- // in the patch range, then don't do a full reload.
- if (this._initialLoadComplete && patchChanged) {
- if (patchRange.patchNum == null) {
- patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
- }
- this._reloadPatchNumDependentResources().then(() => {
- this._sendShowChangeEvent();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this.$.restAPI.getAccount().then(acct => {
+ this._account = acct;
});
- return;
}
+ this._setDiffViewMode();
+ });
- this._changeNum = value.changeNum;
- this.$.relatedChanges.clear();
-
- this._reload(true).then(() => {
- this._performPostLoadTasks();
- });
- }
-
- _sendShowChangeEvent() {
- this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
- change: this._change,
- patchNum: this._patchRange.patchNum,
- info: {mergeable: this._mergeable},
- });
- }
-
- _setPrimaryTab() {
- // Selected has to be set after the paper-tabs are visible, because
- // the selected underline depends on calculations made by the browser.
- // paper-tabs depends on iron-resizable-behavior, which only fires on
- // attached() without using RenderStatus.beforeNextRender. Not changing
- // this when migrating from Polymer 1 to 2 was probably an oversight by
- // the paper component maintainers.
- // https://polymer-library.polymer-project.org/2.0/docs/upgrade#attach-time-attached-connectedcallback
- // By calling _onTabSizingChanged() we are reaching into the private API
- // of paper-tabs, but we believe this workaround is acceptable for the
- // time being.
- Polymer.RenderStatus.beforeNextRender(this, () => {
- this.$.commentTabs.selected = 0;
- this.$.commentTabs._onTabSizingChanged();
- const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
- if (primaryTabs) {
- primaryTabs.selected = 0;
- primaryTabs._onTabSizingChanged();
- }
- });
- }
-
- _performPostLoadTasks() {
- this._maybeShowReplyDialog();
- this._maybeShowRevertDialog();
-
- this._sendShowChangeEvent();
-
- this.async(() => {
- if (this.viewState.scrollTop) {
- document.documentElement.scrollTop =
- document.body.scrollTop = this.viewState.scrollTop;
- } else {
- this._maybeScrollToMessage(window.location.hash);
- }
- this._initialLoadComplete = true;
- });
- }
-
- _paramsAndChangeChanged(value, change) {
- // Polymer 2: check for undefined
- if ([value, change].some(arg => arg === undefined)) {
- return;
- }
-
- // If the change number or patch range is different, then reset the
- // selected file index.
- const patchRangeState = this.viewState.patchRange;
- if (this.viewState.changeNum !== this._changeNum ||
- patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
- patchRangeState.patchNum !== this._patchRange.patchNum) {
- this._resetFileListViewState();
- }
- }
-
- _viewStateChanged(viewState) {
- this._numFilesShown = viewState.numFilesShown ?
- viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
- }
-
- _numFilesShownChanged(numFilesShown) {
- this.viewState.numFilesShown = numFilesShown;
- }
-
- _handleMessageAnchorTap(e) {
- const hash = MSG_PREFIX + e.detail.id;
- const url = Gerrit.Nav.getUrlForChange(this._change,
- this._patchRange.patchNum, this._patchRange.basePatchNum,
- this._editMode, hash);
- history.replaceState(null, '', url);
- }
-
- _maybeScrollToMessage(hash) {
- if (hash.startsWith(MSG_PREFIX)) {
- this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
- }
- }
-
- _getLocationSearch() {
- // Not inlining to make it easier to test.
- return window.location.search;
- }
-
- _getUrlParameter(param) {
- const pageURL = this._getLocationSearch().substring(1);
- const vars = pageURL.split('&');
- for (let i = 0; i < vars.length; i++) {
- const name = vars[i].split('=');
- if (name[0] == param) {
- return name[0];
- }
- }
- return null;
- }
-
- _maybeShowRevertDialog() {
- Gerrit.awaitPluginsLoaded()
- .then(this._getLoggedIn.bind(this))
- .then(loggedIn => {
- if (!loggedIn || !this._change ||
- this._change.status !== this.ChangeStatus.MERGED) {
- // Do not display dialog if not logged-in or the change is not
- // merged.
- return;
- }
- if (this._getUrlParameter('revert')) {
- this.$.actions.showRevertDialog();
- }
- });
- }
-
- _maybeShowReplyDialog() {
- this._getLoggedIn().then(loggedIn => {
- if (!loggedIn) { return; }
-
- if (this.viewState.showReplyDialog) {
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
- // TODO(kaspern@): Find a better signal for when to call center.
- this.async(() => { this.$.replyOverlay.center(); }, 100);
- this.async(() => { this.$.replyOverlay.center(); }, 1000);
- this.set('viewState.showReplyDialog', false);
- }
- });
- }
-
- _resetFileListViewState() {
- this.set('viewState.selectedFileIndex', 0);
- this.set('viewState.scrollTop', 0);
- if (!!this.viewState.changeNum &&
- this.viewState.changeNum !== this._changeNum) {
- // Reset the diff mode to null when navigating from one change to
- // another, so that the user's preference is restored.
- this._setDiffViewMode(true);
- this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN);
- }
- this.set('viewState.changeNum', this._changeNum);
- this.set('viewState.patchRange', this._patchRange);
- }
-
- _changeChanged(change) {
- if (!change || !this._patchRange || !this._allPatchSets) { return; }
-
- // We get the parent first so we keep the original value for basePatchNum
- // and not the updated value.
- const parent = this._getBasePatchNum(change, this._patchRange);
-
- this.set('_patchRange.patchNum', this._patchRange.patchNum ||
- this.computeLatestPatchNum(this._allPatchSets));
-
- this.set('_patchRange.basePatchNum', parent);
-
- const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
- this.fire('title-change', {title});
- }
-
- /**
- * Gets base patch number, if it is a parent try and decide from
- * preference whether to default to `auto merge`, `Parent 1` or `PARENT`.
- *
- * @param {Object} change
- * @param {Object} patchRange
- * @return {number|string}
- */
- _getBasePatchNum(change, patchRange) {
- if (patchRange.basePatchNum &&
- patchRange.basePatchNum !== 'PARENT') {
- return patchRange.basePatchNum;
- }
-
- const revisionInfo = this._getRevisionInfo(change);
- if (!revisionInfo) return 'PARENT';
-
- const parentCounts = revisionInfo.getParentCountMap();
- // check that there is at least 2 parents otherwise fall back to 1,
- // which means there is only one parent.
- const parentCount = parentCounts.hasOwnProperty(1) ?
- parentCounts[1] : 1;
-
- const preferFirst = this._prefs &&
- this._prefs.default_base_for_merges === 'FIRST_PARENT';
-
- if (parentCount > 1 && preferFirst && !patchRange.patchNum) {
- return -1;
- }
-
- return 'PARENT';
- }
-
- _computeChangeUrl(change) {
- return Gerrit.Nav.getUrlForChange(change);
- }
-
- _computeShowCommitInfo(changeStatus, current_revision) {
- return changeStatus === 'Merged' && current_revision;
- }
-
- _computeMergedCommitInfo(current_revision, revisions) {
- const rev = revisions[current_revision];
- if (!rev || !rev.commit) { return {}; }
- // CommitInfo.commit is optional. Set commit in all cases to avoid error
- // in <gr-commit-info>. @see Issue 5337
- if (!rev.commit.commit) { rev.commit.commit = current_revision; }
- return rev.commit;
- }
-
- _computeChangeIdClass(displayChangeId) {
- return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : '';
- }
-
- _computeTitleAttributeWarning(displayChangeId) {
- if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) {
- return 'Change-Id mismatch';
- } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) {
- return 'No Change-Id in commit message';
- }
- }
-
- _computeChangeIdCommitMessageError(commitMessage, change) {
- // Polymer 2: check for undefined
- if ([commitMessage, change].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; }
-
- // Find the last match in the commit message:
- let changeId;
- let changeIdArr;
-
- while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) {
- changeId = changeIdArr[1];
- }
-
- if (changeId) {
- // A change-id is detected in the commit message.
-
- if (changeId === change.change_id) {
- // The change-id found matches the real change-id.
- return null;
- }
- // The change-id found does not match the change-id.
- return CHANGE_ID_ERROR.MISMATCH;
- }
- // There is no change-id in the commit message.
- return CHANGE_ID_ERROR.MISSING;
- }
-
- _computeLabelNames(labels) {
- return Object.keys(labels).sort();
- }
-
- _computeLabelValues(labelName, labels) {
- const result = [];
- const t = labels[labelName];
- if (!t) { return result; }
- const approvals = t.all || [];
- for (const label of approvals) {
- if (label.value && label.value != labels[labelName].default_value) {
- let labelClassName;
- let labelValPrefix = '';
- if (label.value > 0) {
- labelValPrefix = '+';
- labelClassName = 'approved';
- } else if (label.value < 0) {
- labelClassName = 'notApproved';
+ Gerrit.awaitPluginsLoaded()
+ .then(() => {
+ this._dynamicTabHeaderEndpoints =
+ Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header');
+ this._dynamicTabContentEndpoints =
+ Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content');
+ if (this._dynamicTabContentEndpoints.length !==
+ this._dynamicTabHeaderEndpoints.length) {
+ console.warn('Different number of tab headers and tab content.');
}
- result.push({
- value: labelValPrefix + label.value,
- className: labelClassName,
- account: label,
- });
- }
- }
- return result;
- }
+ })
+ .then(() => this._setPrimaryTab());
- _computeReplyButtonLabel(changeRecord, canStartReview) {
- // Polymer 2: check for undefined
- if ([changeRecord, canStartReview].some(arg => arg === undefined)) {
- return 'Reply';
- }
+ this.addEventListener('comment-save', this._handleCommentSave.bind(this));
+ this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
+ this.addEventListener('comment-discard',
+ this._handleCommentDiscard.bind(this));
+ this.addEventListener('change-message-deleted',
+ () => this._reload());
+ this.addEventListener('editable-content-save',
+ this._handleCommitMessageSave.bind(this));
+ this.addEventListener('editable-content-cancel',
+ this._handleCommitMessageCancel.bind(this));
+ this.addEventListener('open-fix-preview',
+ this._onOpenFixPreview.bind(this));
+ this.addEventListener('close-fix-preview',
+ this._onCloseFixPreview.bind(this));
+ this.listen(window, 'scroll', '_handleScroll');
+ this.listen(document, 'visibilitychange', '_handleVisibilityChange');
+ }
- if (canStartReview) {
- return 'Start review';
- }
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_handleScroll');
+ this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
- const drafts = (changeRecord && changeRecord.base) || {};
- const draftCount = Object.keys(drafts)
- .reduce((count, file) => count + drafts[file].length, 0);
-
- let label = 'Reply';
- if (draftCount > 0) {
- label += ' (' + draftCount + ')';
- }
- return label;
- }
-
- _handleOpenReplyDialog(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) {
- return;
- }
- this._getLoggedIn().then(isLoggedIn => {
- if (!isLoggedIn) {
- this.fire('show-auth-required');
- return;
- }
-
- e.preventDefault();
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
- });
- }
-
- _handleOpenDownloadDialogShortcut(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.downloadOverlay.open();
- }
-
- _handleEditTopic(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.metadata.editTopic();
- }
-
- _handleRefreshChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- e.preventDefault();
- Gerrit.Nav.navigateToChange(this._change);
- }
-
- _handleToggleChangeStar(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.changeStar.toggleStar();
- }
-
- _handleUpToDashboard(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._determinePageBack();
- }
-
- _handleExpandAllMessages(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.messagesList.handleExpandCollapse(true);
- }
-
- _handleCollapseAllMessages(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.messagesList.handleExpandCollapse(false);
- }
-
- _handleOpenDiffPrefsShortcut(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- if (this._diffPrefsDisabled) { return; }
-
- e.preventDefault();
- this.$.fileList.openDiffPrefs();
- }
-
- _determinePageBack() {
- // Default backPage to root if user came to change view page
- // via an email link, etc.
- Gerrit.Nav.navigateToRelativeUrl(this.backPage ||
- Gerrit.Nav.getUrlForRoot());
- }
-
- _handleLabelRemoved(splices, path) {
- for (const splice of splices) {
- for (const removed of splice.removed) {
- const changePath = path.split('.');
- const labelPath = changePath.splice(0, changePath.length - 2);
- const labelDict = this.get(labelPath);
- if (labelDict.approved &&
- labelDict.approved._account_id === removed._account_id) {
- this._reload();
- return;
- }
- }
- }
- }
-
- _labelsChanged(changeRecord) {
- if (!changeRecord) { return; }
- if (changeRecord.value && changeRecord.value.indexSplices) {
- this._handleLabelRemoved(changeRecord.value.indexSplices,
- changeRecord.path);
- }
- this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
- change: this._change,
- });
- }
-
- /**
- * @param {string=} opt_section
- */
- _openReplyDialog(opt_section) {
- this.$.replyOverlay.open().finally(() => {
- // the following code should be executed no matter open succeed or not
- this._resetReplyOverlayFocusStops();
- this.$.replyDialog.open(opt_section);
- Polymer.dom.flush();
- this.$.replyOverlay.center();
- });
- }
-
- _handleReloadChange(e) {
- return this._reload().then(() => {
- // If the change was rebased or submitted, we need to reload the page
- // with the latest patch.
- const action = e.detail.action;
- if (action === 'rebase' || action === 'submit') {
- Gerrit.Nav.navigateToChange(this._change);
- }
- });
- }
-
- _handleGetChangeDetailError(response) {
- this.fire('page-error', {response});
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getServerConfig() {
- return this.$.restAPI.getConfig();
- }
-
- _getProjectConfig() {
- if (!this._change) return;
- return this.$.restAPI.getProjectConfig(this._change.project).then(
- config => {
- this._projectConfig = config;
- });
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- _prepareCommitMsgForLinkify(msg) {
- // TODO(wyatta) switch linkify sequence, see issue 5526.
- // This is a zero-with space. It is added to prevent the linkify library
- // from including R= or CC= as part of the email address.
- return msg.replace(REVIEWERS_REGEX, '$1=\u200B');
- }
-
- /**
- * Utility function to make the necessary modifications to a change in the
- * case an edit exists.
- *
- * @param {!Object} change
- * @param {?Object} edit
- */
- _processEdit(change, edit) {
- if (!edit) { return; }
- change.revisions[edit.commit.commit] = {
- _number: this.EDIT_NAME,
- basePatchNum: edit.base_patch_set_number,
- commit: edit.commit,
- fetch: edit.fetch,
- };
- // If the edit is based on the most recent patchset, load it by
- // default, unless another patch set to load was specified in the URL.
- if (!this._patchRange.patchNum &&
- change.current_revision === edit.base_revision) {
- change.current_revision = edit.commit.commit;
- this.set('_patchRange.patchNum', this.EDIT_NAME);
- // Because edits are fibbed as revisions and added to the revisions
- // array, and revision actions are always derived from the 'latest'
- // patch set, we must copy over actions from the patch set base.
- // Context: Issue 7243
- change.revisions[edit.commit.commit].actions =
- change.revisions[edit.base_revision].actions;
- }
- }
-
- _getChangeDetail() {
- const detailCompletes = this.$.restAPI.getChangeDetail(
- this._changeNum, this._handleGetChangeDetailError.bind(this));
- const editCompletes = this._getEdit();
- const prefCompletes = this._getPreferences();
-
- return Promise.all([detailCompletes, editCompletes, prefCompletes])
- .then(([change, edit, prefs]) => {
- this._prefs = prefs;
-
- if (!change) {
- return '';
- }
- this._processEdit(change, edit);
- // Issue 4190: Coalesce missing topics to null.
- if (!change.topic) { change.topic = null; }
- if (!change.reviewer_updates) {
- change.reviewer_updates = null;
- }
- const latestRevisionSha = this._getLatestRevisionSHA(change);
- const currentRevision = change.revisions[latestRevisionSha];
- if (currentRevision.commit && currentRevision.commit.message) {
- this._latestCommitMessage = this._prepareCommitMsgForLinkify(
- currentRevision.commit.message);
- } else {
- this._latestCommitMessage = null;
- }
-
- const lineHeight = getComputedStyle(this).lineHeight;
-
- // Slice returns a number as a string, convert to an int.
- this._lineHeight =
- parseInt(lineHeight.slice(0, lineHeight.length - 2), 10);
-
- this._change = change;
- if (!this._patchRange || !this._patchRange.patchNum ||
- this.patchNumEquals(this._patchRange.patchNum,
- currentRevision._number)) {
- // CommitInfo.commit is optional, and may need patching.
- if (!currentRevision.commit.commit) {
- currentRevision.commit.commit = latestRevisionSha;
- }
- this._commitInfo = currentRevision.commit;
- this._selectedRevision = currentRevision;
- // TODO: Fetch and process files.
- } else {
- this._selectedRevision =
- Object.values(this._change.revisions).find(
- revision => {
- // edit patchset is a special one
- const thePatchNum = this._patchRange.patchNum;
- if (thePatchNum === 'edit') {
- return revision._number === thePatchNum;
- }
- return revision._number === parseInt(thePatchNum, 10);
- });
- }
- });
- }
-
- _isSubmitEnabled(revisionActions) {
- return !!(revisionActions && revisionActions.submit &&
- revisionActions.submit.enabled);
- }
-
- _getEdit() {
- return this.$.restAPI.getChangeEdit(this._changeNum, true);
- }
-
- _getLatestCommitMessage() {
- return this.$.restAPI.getChangeCommitInfo(this._changeNum,
- this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => {
- if (!commitInfo) return Promise.resolve();
- this._latestCommitMessage =
- this._prepareCommitMsgForLinkify(commitInfo.message);
- });
- }
-
- _getLatestRevisionSHA(change) {
- if (change.current_revision) {
- return change.current_revision;
- }
- // current_revision may not be present in the case where the latest rev is
- // a draft and the user doesn’t have permission to view that rev.
- let latestRev = null;
- let latestPatchNum = -1;
- for (const rev in change.revisions) {
- if (!change.revisions.hasOwnProperty(rev)) { continue; }
-
- if (change.revisions[rev]._number > latestPatchNum) {
- latestRev = rev;
- latestPatchNum = change.revisions[rev]._number;
- }
- }
- return latestRev;
- }
-
- _getCommitInfo() {
- return this.$.restAPI.getChangeCommitInfo(
- this._changeNum, this._patchRange.patchNum).then(
- commitInfo => {
- this._commitInfo = commitInfo;
- });
- }
-
- _reloadDraftsWithCallback(e) {
- return this._reloadDrafts().then(() => e.detail.resolve());
- }
-
- /**
- * Fetches a new changeComment object, and data for all types of comments
- * (comments, robot comments, draft comments) is requested.
- */
- _reloadComments() {
- return this.$.commentAPI.loadAll(this._changeNum)
- .then(comments => this._recomputeComments(comments));
- }
-
- /**
- * Fetches a new changeComment object, but only updated data for drafts is
- * requested.
- *
- * TODO(taoalpha): clean up this and _reloadComments, as single comment
- * can be a thread so it does not make sense to only update drafts
- * without updating threads
- */
- _reloadDrafts() {
- return this.$.commentAPI.reloadDrafts(this._changeNum)
- .then(comments => this._recomputeComments(comments));
- }
-
- _recomputeComments(comments) {
- this._changeComments = comments;
- this._diffDrafts = Object.assign({}, this._changeComments.drafts);
- this._commentThreads = this._changeComments.getAllThreadsForChange()
- .map(c => Object.assign({}, c));
- this._draftCommentThreads = this._commentThreads
- .filter(c => c.comments[c.comments.length - 1].__draft);
- }
-
- /**
- * Reload the change.
- *
- * @param {boolean=} opt_isLocationChange Reloads the related changes
- * when true and ends reporting events that started on location change.
- * @return {Promise} A promise that resolves when the core data has loaded.
- * Some non-core data loading may still be in-flight when the core data
- * promise resolves.
- */
- _reload(opt_isLocationChange) {
- this._loading = true;
- this._relatedChangesCollapsed = true;
- this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
- this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
-
- // Array to house all promises related to data requests.
- const allDataPromises = [];
-
- // Resolves when the change detail and the edit patch set (if available)
- // are loaded.
- const detailCompletes = this._getChangeDetail();
- allDataPromises.push(detailCompletes);
-
- // Resolves when the loading flag is set to false, meaning that some
- // change content may start appearing.
- const loadingFlagSet = detailCompletes
- .then(() => {
- this._loading = false;
- this.dispatchEvent(new CustomEvent('change-details-loaded',
- {bubbles: true, composed: true}));
- })
- .then(() => {
- this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
- if (opt_isLocationChange) {
- this.$.reporting.changeDisplayed();
- }
- });
-
- // Resolves when the project config has loaded.
- const projectConfigLoaded = detailCompletes
- .then(() => this._getProjectConfig());
- allDataPromises.push(projectConfigLoaded);
-
- // Resolves when change comments have loaded (comments, drafts and robot
- // comments).
- const commentsLoaded = this._reloadComments();
- allDataPromises.push(commentsLoaded);
-
- let coreDataPromise;
-
- // If the patch number is specified
- if (this._patchRange && this._patchRange.patchNum) {
- // Because a specific patchset is specified, reload the resources that
- // are keyed by patch number or patch range.
- const patchResourcesLoaded = this._reloadPatchNumDependentResources();
- allDataPromises.push(patchResourcesLoaded);
-
- // Promise resolves when the change detail and patch dependent resources
- // have loaded.
- const detailAndPatchResourcesLoaded =
- Promise.all([patchResourcesLoaded, loadingFlagSet]);
-
- // Promise resolves when mergeability information has loaded.
- const mergeabilityLoaded = detailAndPatchResourcesLoaded
- .then(() => this._getMergeability());
- allDataPromises.push(mergeabilityLoaded);
-
- // Promise resovles when the change actions have loaded.
- const actionsLoaded = detailAndPatchResourcesLoaded
- .then(() => this.$.actions.reload());
- allDataPromises.push(actionsLoaded);
-
- // The core data is loaded when both mergeability and actions are known.
- coreDataPromise = Promise.all([mergeabilityLoaded, actionsLoaded]);
- } else {
- // Resolves when the file list has loaded.
- const fileListReload = loadingFlagSet
- .then(() => this.$.fileList.reload());
- allDataPromises.push(fileListReload);
-
- const latestCommitMessageLoaded = loadingFlagSet.then(() => {
- // If the latest commit message is known, there is nothing to do.
- if (this._latestCommitMessage) { return Promise.resolve(); }
- return this._getLatestCommitMessage();
- });
- allDataPromises.push(latestCommitMessageLoaded);
-
- // Promise resolves when mergeability information has loaded.
- const mergeabilityLoaded = loadingFlagSet
- .then(() => this._getMergeability());
- allDataPromises.push(mergeabilityLoaded);
-
- // Core data is loaded when mergeability has been loaded.
- coreDataPromise = mergeabilityLoaded;
- }
-
- if (opt_isLocationChange) {
- const relatedChangesLoaded = coreDataPromise
- .then(() => this.$.relatedChanges.reload());
- allDataPromises.push(relatedChangesLoaded);
- }
-
- Promise.all(allDataPromises).then(() => {
- this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
- if (opt_isLocationChange) {
- this.$.reporting.changeFullyLoaded();
- }
- });
-
- return coreDataPromise;
- }
-
- /**
- * Kicks off requests for resources that rely on the patch range
- * (`this._patchRange`) being defined.
- */
- _reloadPatchNumDependentResources() {
- return Promise.all([
- this._getCommitInfo(),
- this.$.fileList.reload(),
- ]);
- }
-
- _getMergeability() {
- if (!this._change) {
- this._mergeable = null;
- return Promise.resolve();
- }
- // If the change is closed, it is not mergeable. Note: already merged
- // changes are obviously not mergeable, but the mergeability API will not
- // answer for abandoned changes.
- if (this._change.status === this.ChangeStatus.MERGED ||
- this._change.status === this.ChangeStatus.ABANDONED) {
- this._mergeable = false;
- return Promise.resolve();
- }
-
- this._mergeable = null;
- return this.$.restAPI.getMergeable(this._changeNum).then(m => {
- this._mergeable = m.mergeable;
- });
- }
-
- _computeCanStartReview(change) {
- return !!(change.actions && change.actions.ready &&
- change.actions.ready.enabled);
- }
-
- _computeReplyDisabled() { return false; }
-
- _computeChangePermalinkAriaLabel(changeNum) {
- return 'Change ' + changeNum;
- }
-
- _computeCommitMessageCollapsed(collapsed, collapsible) {
- return collapsible && collapsed;
- }
-
- _computeRelatedChangesClass(collapsed) {
- return collapsed ? 'collapsed' : '';
- }
-
- _computeCollapseText(collapsed) {
- // Symbols are up and down triangles.
- return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
- }
-
- /**
- * Returns the text to be copied when
- * click the copy icon next to change subject
- *
- * @param {!Object} change
- */
- _computeCopyTextForTitle(change) {
- return `${change._number}: ${change.subject}` +
- ` | https://${location.host}${this._computeChangeUrl(change)}`;
- }
-
- _toggleCommitCollapsed() {
- this._commitCollapsed = !this._commitCollapsed;
- if (this._commitCollapsed) {
- window.scrollTo(0, 0);
- }
- }
-
- _toggleRelatedChangesCollapsed() {
- this._relatedChangesCollapsed = !this._relatedChangesCollapsed;
- if (this._relatedChangesCollapsed) {
- window.scrollTo(0, 0);
- }
- }
-
- _computeCommitCollapsible(commitMessage) {
- if (!commitMessage) { return false; }
- return commitMessage.split('\n').length >= MIN_LINES_FOR_COMMIT_COLLAPSE;
- }
-
- _getOffsetHeight(element) {
- return element.offsetHeight;
- }
-
- _getScrollHeight(element) {
- return element.scrollHeight;
- }
-
- /**
- * Get the line height of an element to the nearest integer.
- */
- _getLineHeight(element) {
- const lineHeightStr = getComputedStyle(element).lineHeight;
- return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2));
- }
-
- /**
- * New max height for the related changes section, shorter than the existing
- * change info height.
- */
- _updateRelatedChangeMaxHeight() {
- // Takes into account approximate height for the expand button and
- // bottom margin.
- const EXTRA_HEIGHT = 30;
- let newHeight;
-
- if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`)
- .matches) {
- // In a small (mobile) view, give the relation chain some space.
- newHeight = SMALL_RELATED_HEIGHT;
- } else if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`)
- .matches) {
- // Since related changes are below the commit message, but still next to
- // metadata, the height should be the height of the metadata minus the
- // height of the commit message to reduce jank. However, if that doesn't
- // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT.
- // Note: extraHeight is to take into account margin/padding.
- const medRelatedHeight = Math.max(
- this._getOffsetHeight(this.$.mainChangeInfo) -
- this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT,
- MINIMUM_RELATED_MAX_HEIGHT);
- newHeight = medRelatedHeight;
- } else {
- if (this._commitCollapsible) {
- // Make sure the content is lined up if both areas have buttons. If
- // the commit message is not collapsed, instead use the change info
- // height.
- newHeight = this._getOffsetHeight(this.$.commitMessage);
- } else {
- newHeight = this._getOffsetHeight(this.$.commitAndRelated) -
- EXTRA_HEIGHT;
- }
- }
- const stylesToUpdate = {};
-
- // Get the line height of related changes, and convert it to the nearest
- // integer.
- const lineHeight = this._getLineHeight(this.$.relatedChanges);
-
- // Figure out a new height that is divisible by the rounded line height.
- const remainder = newHeight % lineHeight;
- newHeight = newHeight - remainder;
-
- stylesToUpdate['--relation-chain-max-height'] = newHeight + 'px';
-
- // Update the max-height of the relation chain to this new height.
- if (this._commitCollapsible) {
- stylesToUpdate['--related-change-btn-top-padding'] = remainder + 'px';
- }
-
- this.updateStyles(stylesToUpdate);
- }
-
- _computeShowRelatedToggle() {
- // Make sure the max height has been applied, since there is now content
- // to populate.
- if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
- this._updateRelatedChangeMaxHeight();
- }
- // Prevents showMore from showing when click on related change, since the
- // line height would be positive, but related changes height is 0.
- if (!this._getScrollHeight(this.$.relatedChanges)) {
- return this._showRelatedToggle = false;
- }
-
- if (this._getScrollHeight(this.$.relatedChanges) >
- (this._getOffsetHeight(this.$.relatedChanges) +
- this._getLineHeight(this.$.relatedChanges))) {
- return this._showRelatedToggle = true;
- }
- this._showRelatedToggle = false;
- }
-
- _updateToggleContainerClass(showRelatedToggle) {
- if (showRelatedToggle) {
- this.$.relatedChangesToggle.classList.add('showToggle');
- } else {
- this.$.relatedChangesToggle.classList.remove('showToggle');
- }
- }
-
- _startUpdateCheckTimer() {
- if (!this._serverConfig ||
- !this._serverConfig.change ||
- this._serverConfig.change.update_delay === undefined ||
- this._serverConfig.change.update_delay <= MIN_CHECK_INTERVAL_SECS) {
- return;
- }
-
- this._updateCheckTimerHandle = this.async(() => {
- this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => {
- let toastMessage = null;
- if (!result.isLatest) {
- toastMessage = ReloadToastMessage.NEWER_REVISION;
- } else if (result.newStatus === this.ChangeStatus.MERGED) {
- toastMessage = ReloadToastMessage.MERGED;
- } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
- toastMessage = ReloadToastMessage.ABANDONED;
- } else if (result.newStatus === this.ChangeStatus.NEW) {
- toastMessage = ReloadToastMessage.RESTORED;
- } else if (result.newMessages) {
- toastMessage = ReloadToastMessage.NEW_MESSAGE;
- }
-
- if (!toastMessage) {
- this._startUpdateCheckTimer();
- return;
- }
-
- this._cancelUpdateCheckTimer();
- this.fire('show-alert', {
- message: toastMessage,
- // Persist this alert.
- dismissOnNavigation: true,
- action: 'Reload',
- callback: function() {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this._change);
- }.bind(this),
- });
- });
- }, this._serverConfig.change.update_delay * 1000);
- }
-
- _cancelUpdateCheckTimer() {
- if (this._updateCheckTimerHandle) {
- this.cancelAsync(this._updateCheckTimerHandle);
- }
- this._updateCheckTimerHandle = null;
- }
-
- _handleVisibilityChange() {
- if (document.hidden && this._updateCheckTimerHandle) {
- this._cancelUpdateCheckTimer();
- } else if (!this._updateCheckTimerHandle) {
- this._startUpdateCheckTimer();
- }
- }
-
- _handleTopicChanged() {
- this.$.relatedChanges.reload();
- }
-
- _computeHeaderClass(editMode) {
- const classes = ['header'];
- if (editMode) { classes.push('editMode'); }
- return classes.join(' ');
- }
-
- _computeEditMode(patchRangeRecord, paramsRecord) {
- if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (paramsRecord.base && paramsRecord.base.edit) { return true; }
-
- const patchRange = patchRangeRecord.base || {};
- return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
- }
-
- _handleFileActionTap(e) {
- e.preventDefault();
- const controls = this.$.fileListHeader.$.editControls;
- const path = e.detail.path;
- switch (e.detail.action) {
- case GrEditConstants.Actions.DELETE.id:
- controls.openDeleteDialog(path);
- break;
- case GrEditConstants.Actions.OPEN.id:
- Gerrit.Nav.navigateToRelativeUrl(
- Gerrit.Nav.getEditUrlForDiff(this._change, path,
- this._patchRange.patchNum));
- break;
- case GrEditConstants.Actions.RENAME.id:
- controls.openRenameDialog(path);
- break;
- case GrEditConstants.Actions.RESTORE.id:
- controls.openRestoreDialog(path);
- break;
- }
- }
-
- _computeCommitMessageKey(number, revision) {
- return `c${number}_rev${revision}`;
- }
-
- _patchNumChanged(patchNumStr) {
- if (!this._selectedRevision) {
- return;
- }
-
- let patchNum = parseInt(patchNumStr, 10);
- if (patchNumStr === 'edit') {
- patchNum = patchNumStr;
- }
-
- if (patchNum === this._selectedRevision._number) {
- return;
- }
- this._selectedRevision = Object.values(this._change.revisions).find(
- revision => revision._number === patchNum);
- }
-
- /**
- * If an edit exists already, load it. Otherwise, toggle edit mode via the
- * navigation API.
- */
- _handleEditTap() {
- const editInfo = Object.values(this._change.revisions).find(info =>
- info._number === this.EDIT_NAME);
-
- if (editInfo) {
- Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME);
- return;
- }
-
- // Avoid putting patch set in the URL unless a non-latest patch set is
- // selected.
- let patchNum;
- if (!this.patchNumEquals(this._patchRange.patchNum,
- this.computeLatestPatchNum(this._allPatchSets))) {
- patchNum = this._patchRange.patchNum;
- }
- Gerrit.Nav.navigateToChange(this._change, patchNum, null, true);
- }
-
- _handleStopEditTap() {
- Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum);
- }
-
- _resetReplyOverlayFocusStops() {
- this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
- }
-
- _handleToggleStar(e) {
- this.$.restAPI.saveChangeStarred(e.detail.change._number,
- e.detail.starred);
- }
-
- _getRevisionInfo(change) {
- return new Gerrit.RevisionInfo(change);
- }
-
- _computeCurrentRevision(currentRevision, revisions) {
- return currentRevision && revisions && revisions[currentRevision];
- }
-
- _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
- return disableDiffPrefs || !loggedIn;
+ if (this._updateCheckTimerHandle) {
+ this._cancelUpdateCheckTimer();
}
}
- customElements.define(GrChangeView.is, GrChangeView);
-})();
+ get messagesList() {
+ return this.shadowRoot.querySelector('gr-messages-list');
+ }
+
+ get threadList() {
+ return this.shadowRoot.querySelector('gr-thread-list');
+ }
+
+ /**
+ * @param {boolean=} opt_reset
+ */
+ _setDiffViewMode(opt_reset) {
+ if (!opt_reset && this.viewState.diffViewMode) { return; }
+
+ return this._getPreferences()
+ .then( prefs => {
+ if (!this.viewState.diffMode) {
+ this.set('viewState.diffMode', prefs.default_diff_view);
+ }
+ })
+ .then(() => {
+ if (!this.viewState.diffMode) {
+ this.set('viewState.diffMode', 'SIDE_BY_SIDE');
+ }
+ });
+ }
+
+ _onOpenFixPreview(e) {
+ this.$.applyFixDialog.open(e);
+ }
+
+ _onCloseFixPreview(e) {
+ this._reload();
+ }
+
+ _handleToggleDiffMode(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED);
+ } else {
+ this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE);
+ }
+ }
+
+ _handleCommentTabChange() {
+ this._currentView = this.$.commentTabs.selected;
+ const type = Object.keys(CommentTabs).find(key => CommentTabs[key] ===
+ this._currentView);
+ this.$.reporting.reportInteraction('comment-tab-changed', {tabName:
+ type});
+ }
+
+ _isSelectedView(currentView, view) {
+ return currentView === view;
+ }
+
+ _findIfTabMatches(currentTab, tab) {
+ return currentTab === tab;
+ }
+
+ _handleFileTabChange(e) {
+ const selectedIndex = e.target.selected;
+ const tabs = e.target.querySelectorAll('paper-tab');
+ this._currentTabName = tabs[selectedIndex] &&
+ tabs[selectedIndex].dataset.name;
+ const source = e && e.type ? e.type : '';
+ const pluginIndex = (this._dynamicTabHeaderEndpoints || []).indexOf(
+ this._currentTabName);
+ if (pluginIndex !== -1) {
+ this._selectedTabPluginEndpoint = this._dynamicTabContentEndpoints[
+ pluginIndex];
+ this._selectedTabPluginHeader = this._dynamicTabHeaderEndpoints[
+ pluginIndex];
+ } else {
+ this._selectedTabPluginEndpoint = '';
+ this._selectedTabPluginHeader = '';
+ }
+ this.$.reporting.reportInteraction('tab-changed',
+ {tabName: this._currentTabName, source});
+ }
+
+ _handleShowTab(e) {
+ const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
+ const tabs = primaryTabs.querySelectorAll('paper-tab');
+ let idx = -1;
+ tabs.forEach((tab, index) => {
+ if (tab.dataset.name === e.detail.tab) idx = index;
+ });
+ if (idx === -1) {
+ console.error(e.detail.tab + ' tab not found');
+ return;
+ }
+ primaryTabs.selected = idx;
+ primaryTabs.scrollIntoView();
+ this.$.reporting.reportInteraction('show-tab', {tabName: e.detail.tab});
+ }
+
+ _handleEditCommitMessage(e) {
+ this._editingCommitMessage = true;
+ this.$.commitMessageEditor.focusTextarea();
+ }
+
+ _handleCommitMessageSave(e) {
+ // Trim trailing whitespace from each line.
+ const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
+
+ this.$.jsAPI.handleCommitMessage(this._change, message);
+
+ this.$.commitMessageEditor.disabled = true;
+ this.$.restAPI.putChangeCommitMessage(
+ this._changeNum, message).then(resp => {
+ this.$.commitMessageEditor.disabled = false;
+ if (!resp.ok) { return; }
+
+ this._latestCommitMessage = this._prepareCommitMsgForLinkify(
+ message);
+ this._editingCommitMessage = false;
+ this._reloadWindow();
+ })
+ .catch(err => {
+ this.$.commitMessageEditor.disabled = false;
+ });
+ }
+
+ _reloadWindow() {
+ window.location.reload();
+ }
+
+ _handleCommitMessageCancel(e) {
+ this._editingCommitMessage = false;
+ }
+
+ _computeChangeStatusChips(change, mergeable, submitEnabled) {
+ // Polymer 2: check for undefined
+ if ([
+ change,
+ mergeable,
+ ].some(arg => arg === undefined)) {
+ // To keep consistent with Polymer 1, we are returning undefined
+ // if not all dependencies are defined
+ return undefined;
+ }
+
+ // Show no chips until mergeability is loaded.
+ if (mergeable === null) {
+ return [];
+ }
+
+ const options = {
+ includeDerived: true,
+ mergeable: !!mergeable,
+ submitEnabled: !!submitEnabled,
+ };
+ return this.changeStatuses(change, options);
+ }
+
+ _computeHideEditCommitMessage(
+ loggedIn, editing, change, editMode, collapsed, collapsible) {
+ if (!loggedIn || editing ||
+ (change && change.status === this.ChangeStatus.MERGED) ||
+ editMode ||
+ (collapsed && collapsible)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ _robotCommentCountPerPatchSet(threads) {
+ return threads.reduce((robotCommentCountMap, thread) => {
+ const comments = thread.comments;
+ const robotCommentsCount = comments.reduce((acc, comment) =>
+ (comment.robot_id ? acc + 1 : acc), 0);
+ robotCommentCountMap[comments[0].patch_set] =
+ (robotCommentCountMap[comments[0].patch_set] || 0) +
+ robotCommentsCount;
+ return robotCommentCountMap;
+ }, {});
+ }
+
+ _computeText(patch, commentThreads) {
+ const commentCount = this._robotCommentCountPerPatchSet(commentThreads);
+ const commentCnt = commentCount[patch._number] || 0;
+ if (commentCnt === 0) return `Patchset ${patch._number}`;
+ const findingsText = commentCnt === 1 ? 'finding' : 'findings';
+ return `Patchset ${patch._number}`
+ + ` (${commentCnt} ${findingsText})`;
+ }
+
+ _computeRobotCommentsPatchSetDropdownItems(change, commentThreads) {
+ if (!change || !commentThreads || !change.revisions) return [];
+
+ return Object.values(change.revisions)
+ .filter(patch => patch._number !== 'edit')
+ .map(patch => {
+ return {
+ text: this._computeText(patch, commentThreads),
+ value: patch._number,
+ };
+ })
+ .sort((a, b) => b.value - a.value);
+ }
+
+ _handleCurrentRevisionUpdate(currentRevision) {
+ this._currentRobotCommentsPatchSet = currentRevision._number;
+ }
+
+ _handleRobotCommentPatchSetChanged(e) {
+ const patchSet = parseInt(e.detail.value);
+ if (patchSet === this._currentRobotCommentsPatchSet) return;
+ this._currentRobotCommentsPatchSet = patchSet;
+ }
+
+ _computeShowText(showAllRobotComments) {
+ return showAllRobotComments ? 'Show Less' : 'Show more';
+ }
+
+ _toggleShowRobotComments() {
+ this._showAllRobotComments = !this._showAllRobotComments;
+ }
+
+ _computeRobotCommentThreads(commentThreads, currentRobotCommentsPatchSet,
+ showAllRobotComments) {
+ if (!commentThreads || !currentRobotCommentsPatchSet) return [];
+ const threads = commentThreads.filter(thread => {
+ const comments = thread.comments || [];
+ return comments.length && comments[0].robot_id && (comments[0].patch_set
+ === currentRobotCommentsPatchSet);
+ });
+ this._showRobotCommentsButton = threads.length > ROBOT_COMMENTS_LIMIT;
+ return threads.slice(0, showAllRobotComments ? undefined :
+ ROBOT_COMMENTS_LIMIT);
+ }
+
+ _handleReloadCommentThreads() {
+ // Get any new drafts that have been saved in the diff view and show
+ // in the comment thread view.
+ this._reloadDrafts().then(() => {
+ this._commentThreads = this._changeComments.getAllThreadsForChange()
+ .map(c => Object.assign({}, c));
+ flush();
+ });
+ }
+
+ _handleReloadDiffComments(e) {
+ // Keeps the file list counts updated.
+ this._reloadDrafts().then(() => {
+ // Get any new drafts that have been saved in the thread view and show
+ // in the diff view.
+ this.$.fileList.reloadCommentsForThreadWithRootId(e.detail.rootId,
+ e.detail.path);
+ flush();
+ });
+ }
+
+ _computeTotalCommentCounts(unresolvedCount, changeComments) {
+ if (!changeComments) return undefined;
+ const draftCount = changeComments.computeDraftCount();
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+ const draftString = GrCountStringFormatter.computePluralString(
+ draftCount, 'draft');
+
+ return unresolvedString +
+ // Add a comma and space if both unresolved and draft comments exist.
+ (unresolvedString && draftString ? ', ' : '') +
+ draftString;
+ }
+
+ _handleCommentSave(e) {
+ const draft = e.detail.comment;
+ if (!draft.__draft) { return; }
+
+ draft.patch_set = draft.patch_set || this._patchRange.patchNum;
+
+ // The use of path-based notification helpers (set, push) can’t be used
+ // because the paths could contain dots in them. A new object must be
+ // created to satisfy Polymer’s dirty checking.
+ // https://github.com/Polymer/polymer/issues/3127
+ const diffDrafts = Object.assign({}, this._diffDrafts);
+ if (!diffDrafts[draft.path]) {
+ diffDrafts[draft.path] = [draft];
+ this._diffDrafts = diffDrafts;
+ return;
+ }
+ for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
+ if (this._diffDrafts[draft.path][i].id === draft.id) {
+ diffDrafts[draft.path][i] = draft;
+ this._diffDrafts = diffDrafts;
+ return;
+ }
+ }
+ diffDrafts[draft.path].push(draft);
+ diffDrafts[draft.path].sort((c1, c2) =>
+ // No line number means that it’s a file comment. Sort it above the
+ // others.
+ (c1.line || -1) - (c2.line || -1)
+ );
+ this._diffDrafts = diffDrafts;
+ }
+
+ _handleCommentDiscard(e) {
+ const draft = e.detail.comment;
+ if (!draft.__draft) { return; }
+
+ if (!this._diffDrafts[draft.path]) {
+ return;
+ }
+ let index = -1;
+ for (let i = 0; i < this._diffDrafts[draft.path].length; i++) {
+ if (this._diffDrafts[draft.path][i].id === draft.id) {
+ index = i;
+ break;
+ }
+ }
+ if (index === -1) {
+ // It may be a draft that hasn’t been added to _diffDrafts since it was
+ // never saved.
+ return;
+ }
+
+ draft.patch_set = draft.patch_set || this._patchRange.patchNum;
+
+ // The use of path-based notification helpers (set, push) can’t be used
+ // because the paths could contain dots in them. A new object must be
+ // created to satisfy Polymer’s dirty checking.
+ // https://github.com/Polymer/polymer/issues/3127
+ const diffDrafts = Object.assign({}, this._diffDrafts);
+ diffDrafts[draft.path].splice(index, 1);
+ if (diffDrafts[draft.path].length === 0) {
+ delete diffDrafts[draft.path];
+ }
+ this._diffDrafts = diffDrafts;
+ }
+
+ _handleReplyTap(e) {
+ e.preventDefault();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ }
+
+ _handleOpenDiffPrefs() {
+ this.$.fileList.openDiffPrefs();
+ }
+
+ _handleOpenIncludedInDialog() {
+ this.$.includedInDialog.loadData().then(() => {
+ flush();
+ this.$.includedInOverlay.refit();
+ });
+ this.$.includedInOverlay.open();
+ }
+
+ _handleIncludedInDialogClose(e) {
+ this.$.includedInOverlay.close();
+ }
+
+ _handleOpenDownloadDialog() {
+ this.$.downloadOverlay.open().then(() => {
+ this.$.downloadOverlay
+ .setFocusStops(this.$.downloadDialog.getFocusStops());
+ this.$.downloadDialog.focus();
+ });
+ }
+
+ _handleDownloadDialogClose(e) {
+ this.$.downloadOverlay.close();
+ }
+
+ _handleOpenUploadHelpDialog(e) {
+ this.$.uploadHelpOverlay.open();
+ }
+
+ _handleCloseUploadHelpDialog(e) {
+ this.$.uploadHelpOverlay.close();
+ }
+
+ _handleMessageReply(e) {
+ const msg = e.detail.message.message;
+ const quoteStr = msg.split('\n').map(
+ line => '> ' + line)
+ .join('\n') + '\n\n';
+ this.$.replyDialog.quote = quoteStr;
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
+ }
+
+ _handleHideBackgroundContent() {
+ this.$.mainContent.classList.add('overlayOpen');
+ }
+
+ _handleShowBackgroundContent() {
+ this.$.mainContent.classList.remove('overlayOpen');
+ }
+
+ _handleReplySent(e) {
+ this.addEventListener('change-details-loaded',
+ () => {
+ this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
+ }, {once: true});
+ this.$.replyOverlay.close();
+ this._reload();
+ }
+
+ _handleReplyCancel(e) {
+ this.$.replyOverlay.close();
+ }
+
+ _handleReplyAutogrow(e) {
+ // If the textarea resizes, we need to re-fit the overlay.
+ this.debounce('reply-overlay-refit', () => {
+ this.$.replyOverlay.refit();
+ }, REPLY_REFIT_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _handleShowReplyDialog(e) {
+ let target = this.$.replyDialog.FocusTarget.REVIEWERS;
+ if (e.detail.value && e.detail.value.ccsOnly) {
+ target = this.$.replyDialog.FocusTarget.CCS;
+ }
+ this._openReplyDialog(target);
+ }
+
+ _handleScroll() {
+ this.debounce('scroll', () => {
+ this.viewState.scrollTop = document.body.scrollTop;
+ }, 150);
+ }
+
+ _setShownFiles(e) {
+ this._shownFileCount = e.detail.length;
+ }
+
+ _expandAllDiffs() {
+ this.$.fileList.expandAllDiffs();
+ }
+
+ _collapseAllDiffs() {
+ this.$.fileList.collapseAllDiffs();
+ }
+
+ _paramsChanged(value) {
+ this._currentView = CommentTabs.CHANGE_LOG;
+ this._setPrimaryTab();
+ if (value.view !== Gerrit.Nav.View.CHANGE) {
+ this._initialLoadComplete = false;
+ return;
+ }
+
+ if (value.changeNum && value.project) {
+ this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
+ }
+
+ const patchChanged = this._patchRange &&
+ (value.patchNum !== undefined && value.basePatchNum !== undefined) &&
+ (this._patchRange.patchNum !== value.patchNum ||
+ this._patchRange.basePatchNum !== value.basePatchNum);
+
+ if (this._changeNum !== value.changeNum) {
+ this._initialLoadComplete = false;
+ }
+
+ const patchRange = {
+ patchNum: value.patchNum,
+ basePatchNum: value.basePatchNum || 'PARENT',
+ };
+
+ this.$.fileList.collapseAllDiffs();
+ this._patchRange = patchRange;
+
+ // If the change has already been loaded and the parameter change is only
+ // in the patch range, then don't do a full reload.
+ if (this._initialLoadComplete && patchChanged) {
+ if (patchRange.patchNum == null) {
+ patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+ }
+ this._reloadPatchNumDependentResources().then(() => {
+ this._sendShowChangeEvent();
+ });
+ return;
+ }
+
+ this._changeNum = value.changeNum;
+ this.$.relatedChanges.clear();
+
+ this._reload(true).then(() => {
+ this._performPostLoadTasks();
+ });
+ }
+
+ _sendShowChangeEvent() {
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
+ change: this._change,
+ patchNum: this._patchRange.patchNum,
+ info: {mergeable: this._mergeable},
+ });
+ }
+
+ _setPrimaryTab() {
+ // Selected has to be set after the paper-tabs are visible, because
+ // the selected underline depends on calculations made by the browser.
+ // paper-tabs depends on iron-resizable-behavior, which only fires on
+ // attached() without using RenderStatus.beforeNextRender. Not changing
+ // this when migrating from Polymer 1 to 2 was probably an oversight by
+ // the paper component maintainers.
+ // https://polymer-library.polymer-project.org/2.0/docs/upgrade#attach-time-attached-connectedcallback
+ // By calling _onTabSizingChanged() we are reaching into the private API
+ // of paper-tabs, but we believe this workaround is acceptable for the
+ // time being.
+ beforeNextRender(this, () => {
+ this.$.commentTabs.selected = 0;
+ this.$.commentTabs._onTabSizingChanged();
+ const primaryTabs = this.shadowRoot.querySelector('#primaryTabs');
+ if (primaryTabs) {
+ primaryTabs.selected = 0;
+ primaryTabs._onTabSizingChanged();
+ }
+ });
+ }
+
+ _performPostLoadTasks() {
+ this._maybeShowReplyDialog();
+ this._maybeShowRevertDialog();
+
+ this._sendShowChangeEvent();
+
+ this.async(() => {
+ if (this.viewState.scrollTop) {
+ document.documentElement.scrollTop =
+ document.body.scrollTop = this.viewState.scrollTop;
+ } else {
+ this._maybeScrollToMessage(window.location.hash);
+ }
+ this._initialLoadComplete = true;
+ });
+ }
+
+ _paramsAndChangeChanged(value, change) {
+ // Polymer 2: check for undefined
+ if ([value, change].some(arg => arg === undefined)) {
+ return;
+ }
+
+ // If the change number or patch range is different, then reset the
+ // selected file index.
+ const patchRangeState = this.viewState.patchRange;
+ if (this.viewState.changeNum !== this._changeNum ||
+ patchRangeState.basePatchNum !== this._patchRange.basePatchNum ||
+ patchRangeState.patchNum !== this._patchRange.patchNum) {
+ this._resetFileListViewState();
+ }
+ }
+
+ _viewStateChanged(viewState) {
+ this._numFilesShown = viewState.numFilesShown ?
+ viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN;
+ }
+
+ _numFilesShownChanged(numFilesShown) {
+ this.viewState.numFilesShown = numFilesShown;
+ }
+
+ _handleMessageAnchorTap(e) {
+ const hash = MSG_PREFIX + e.detail.id;
+ const url = Gerrit.Nav.getUrlForChange(this._change,
+ this._patchRange.patchNum, this._patchRange.basePatchNum,
+ this._editMode, hash);
+ history.replaceState(null, '', url);
+ }
+
+ _maybeScrollToMessage(hash) {
+ if (hash.startsWith(MSG_PREFIX)) {
+ this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
+ }
+ }
+
+ _getLocationSearch() {
+ // Not inlining to make it easier to test.
+ return window.location.search;
+ }
+
+ _getUrlParameter(param) {
+ const pageURL = this._getLocationSearch().substring(1);
+ const vars = pageURL.split('&');
+ for (let i = 0; i < vars.length; i++) {
+ const name = vars[i].split('=');
+ if (name[0] == param) {
+ return name[0];
+ }
+ }
+ return null;
+ }
+
+ _maybeShowRevertDialog() {
+ Gerrit.awaitPluginsLoaded()
+ .then(this._getLoggedIn.bind(this))
+ .then(loggedIn => {
+ if (!loggedIn || !this._change ||
+ this._change.status !== this.ChangeStatus.MERGED) {
+ // Do not display dialog if not logged-in or the change is not
+ // merged.
+ return;
+ }
+ if (this._getUrlParameter('revert')) {
+ this.$.actions.showRevertDialog();
+ }
+ });
+ }
+
+ _maybeShowReplyDialog() {
+ this._getLoggedIn().then(loggedIn => {
+ if (!loggedIn) { return; }
+
+ if (this.viewState.showReplyDialog) {
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ // TODO(kaspern@): Find a better signal for when to call center.
+ this.async(() => { this.$.replyOverlay.center(); }, 100);
+ this.async(() => { this.$.replyOverlay.center(); }, 1000);
+ this.set('viewState.showReplyDialog', false);
+ }
+ });
+ }
+
+ _resetFileListViewState() {
+ this.set('viewState.selectedFileIndex', 0);
+ this.set('viewState.scrollTop', 0);
+ if (!!this.viewState.changeNum &&
+ this.viewState.changeNum !== this._changeNum) {
+ // Reset the diff mode to null when navigating from one change to
+ // another, so that the user's preference is restored.
+ this._setDiffViewMode(true);
+ this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN);
+ }
+ this.set('viewState.changeNum', this._changeNum);
+ this.set('viewState.patchRange', this._patchRange);
+ }
+
+ _changeChanged(change) {
+ if (!change || !this._patchRange || !this._allPatchSets) { return; }
+
+ // We get the parent first so we keep the original value for basePatchNum
+ // and not the updated value.
+ const parent = this._getBasePatchNum(change, this._patchRange);
+
+ this.set('_patchRange.patchNum', this._patchRange.patchNum ||
+ this.computeLatestPatchNum(this._allPatchSets));
+
+ this.set('_patchRange.basePatchNum', parent);
+
+ const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
+ this.fire('title-change', {title});
+ }
+
+ /**
+ * Gets base patch number, if it is a parent try and decide from
+ * preference whether to default to `auto merge`, `Parent 1` or `PARENT`.
+ *
+ * @param {Object} change
+ * @param {Object} patchRange
+ * @return {number|string}
+ */
+ _getBasePatchNum(change, patchRange) {
+ if (patchRange.basePatchNum &&
+ patchRange.basePatchNum !== 'PARENT') {
+ return patchRange.basePatchNum;
+ }
+
+ const revisionInfo = this._getRevisionInfo(change);
+ if (!revisionInfo) return 'PARENT';
+
+ const parentCounts = revisionInfo.getParentCountMap();
+ // check that there is at least 2 parents otherwise fall back to 1,
+ // which means there is only one parent.
+ const parentCount = parentCounts.hasOwnProperty(1) ?
+ parentCounts[1] : 1;
+
+ const preferFirst = this._prefs &&
+ this._prefs.default_base_for_merges === 'FIRST_PARENT';
+
+ if (parentCount > 1 && preferFirst && !patchRange.patchNum) {
+ return -1;
+ }
+
+ return 'PARENT';
+ }
+
+ _computeChangeUrl(change) {
+ return Gerrit.Nav.getUrlForChange(change);
+ }
+
+ _computeShowCommitInfo(changeStatus, current_revision) {
+ return changeStatus === 'Merged' && current_revision;
+ }
+
+ _computeMergedCommitInfo(current_revision, revisions) {
+ const rev = revisions[current_revision];
+ if (!rev || !rev.commit) { return {}; }
+ // CommitInfo.commit is optional. Set commit in all cases to avoid error
+ // in <gr-commit-info>. @see Issue 5337
+ if (!rev.commit.commit) { rev.commit.commit = current_revision; }
+ return rev.commit;
+ }
+
+ _computeChangeIdClass(displayChangeId) {
+ return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : '';
+ }
+
+ _computeTitleAttributeWarning(displayChangeId) {
+ if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) {
+ return 'Change-Id mismatch';
+ } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) {
+ return 'No Change-Id in commit message';
+ }
+ }
+
+ _computeChangeIdCommitMessageError(commitMessage, change) {
+ // Polymer 2: check for undefined
+ if ([commitMessage, change].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; }
+
+ // Find the last match in the commit message:
+ let changeId;
+ let changeIdArr;
+
+ while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) {
+ changeId = changeIdArr[1];
+ }
+
+ if (changeId) {
+ // A change-id is detected in the commit message.
+
+ if (changeId === change.change_id) {
+ // The change-id found matches the real change-id.
+ return null;
+ }
+ // The change-id found does not match the change-id.
+ return CHANGE_ID_ERROR.MISMATCH;
+ }
+ // There is no change-id in the commit message.
+ return CHANGE_ID_ERROR.MISSING;
+ }
+
+ _computeLabelNames(labels) {
+ return Object.keys(labels).sort();
+ }
+
+ _computeLabelValues(labelName, labels) {
+ const result = [];
+ const t = labels[labelName];
+ if (!t) { return result; }
+ const approvals = t.all || [];
+ for (const label of approvals) {
+ if (label.value && label.value != labels[labelName].default_value) {
+ let labelClassName;
+ let labelValPrefix = '';
+ if (label.value > 0) {
+ labelValPrefix = '+';
+ labelClassName = 'approved';
+ } else if (label.value < 0) {
+ labelClassName = 'notApproved';
+ }
+ result.push({
+ value: labelValPrefix + label.value,
+ className: labelClassName,
+ account: label,
+ });
+ }
+ }
+ return result;
+ }
+
+ _computeReplyButtonLabel(changeRecord, canStartReview) {
+ // Polymer 2: check for undefined
+ if ([changeRecord, canStartReview].some(arg => arg === undefined)) {
+ return 'Reply';
+ }
+
+ if (canStartReview) {
+ return 'Start review';
+ }
+
+ const drafts = (changeRecord && changeRecord.base) || {};
+ const draftCount = Object.keys(drafts)
+ .reduce((count, file) => count + drafts[file].length, 0);
+
+ let label = 'Reply';
+ if (draftCount > 0) {
+ label += ' (' + draftCount + ')';
+ }
+ return label;
+ }
+
+ _handleOpenReplyDialog(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) {
+ return;
+ }
+ this._getLoggedIn().then(isLoggedIn => {
+ if (!isLoggedIn) {
+ this.fire('show-auth-required');
+ return;
+ }
+
+ e.preventDefault();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ });
+ }
+
+ _handleOpenDownloadDialogShortcut(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.downloadOverlay.open();
+ }
+
+ _handleEditTopic(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.metadata.editTopic();
+ }
+
+ _handleRefreshChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ e.preventDefault();
+ Gerrit.Nav.navigateToChange(this._change);
+ }
+
+ _handleToggleChangeStar(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.changeStar.toggleStar();
+ }
+
+ _handleUpToDashboard(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._determinePageBack();
+ }
+
+ _handleExpandAllMessages(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.messagesList.handleExpandCollapse(true);
+ }
+
+ _handleCollapseAllMessages(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.messagesList.handleExpandCollapse(false);
+ }
+
+ _handleOpenDiffPrefsShortcut(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ if (this._diffPrefsDisabled) { return; }
+
+ e.preventDefault();
+ this.$.fileList.openDiffPrefs();
+ }
+
+ _determinePageBack() {
+ // Default backPage to root if user came to change view page
+ // via an email link, etc.
+ Gerrit.Nav.navigateToRelativeUrl(this.backPage ||
+ Gerrit.Nav.getUrlForRoot());
+ }
+
+ _handleLabelRemoved(splices, path) {
+ for (const splice of splices) {
+ for (const removed of splice.removed) {
+ const changePath = path.split('.');
+ const labelPath = changePath.splice(0, changePath.length - 2);
+ const labelDict = this.get(labelPath);
+ if (labelDict.approved &&
+ labelDict.approved._account_id === removed._account_id) {
+ this._reload();
+ return;
+ }
+ }
+ }
+ }
+
+ _labelsChanged(changeRecord) {
+ if (!changeRecord) { return; }
+ if (changeRecord.value && changeRecord.value.indexSplices) {
+ this._handleLabelRemoved(changeRecord.value.indexSplices,
+ changeRecord.path);
+ }
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
+ change: this._change,
+ });
+ }
+
+ /**
+ * @param {string=} opt_section
+ */
+ _openReplyDialog(opt_section) {
+ this.$.replyOverlay.open().finally(() => {
+ // the following code should be executed no matter open succeed or not
+ this._resetReplyOverlayFocusStops();
+ this.$.replyDialog.open(opt_section);
+ flush();
+ this.$.replyOverlay.center();
+ });
+ }
+
+ _handleReloadChange(e) {
+ return this._reload().then(() => {
+ // If the change was rebased or submitted, we need to reload the page
+ // with the latest patch.
+ const action = e.detail.action;
+ if (action === 'rebase' || action === 'submit') {
+ Gerrit.Nav.navigateToChange(this._change);
+ }
+ });
+ }
+
+ _handleGetChangeDetailError(response) {
+ this.fire('page-error', {response});
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getServerConfig() {
+ return this.$.restAPI.getConfig();
+ }
+
+ _getProjectConfig() {
+ if (!this._change) return;
+ return this.$.restAPI.getProjectConfig(this._change.project).then(
+ config => {
+ this._projectConfig = config;
+ });
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ _prepareCommitMsgForLinkify(msg) {
+ // TODO(wyatta) switch linkify sequence, see issue 5526.
+ // This is a zero-with space. It is added to prevent the linkify library
+ // from including R= or CC= as part of the email address.
+ return msg.replace(REVIEWERS_REGEX, '$1=\u200B');
+ }
+
+ /**
+ * Utility function to make the necessary modifications to a change in the
+ * case an edit exists.
+ *
+ * @param {!Object} change
+ * @param {?Object} edit
+ */
+ _processEdit(change, edit) {
+ if (!edit) { return; }
+ change.revisions[edit.commit.commit] = {
+ _number: this.EDIT_NAME,
+ basePatchNum: edit.base_patch_set_number,
+ commit: edit.commit,
+ fetch: edit.fetch,
+ };
+ // If the edit is based on the most recent patchset, load it by
+ // default, unless another patch set to load was specified in the URL.
+ if (!this._patchRange.patchNum &&
+ change.current_revision === edit.base_revision) {
+ change.current_revision = edit.commit.commit;
+ this.set('_patchRange.patchNum', this.EDIT_NAME);
+ // Because edits are fibbed as revisions and added to the revisions
+ // array, and revision actions are always derived from the 'latest'
+ // patch set, we must copy over actions from the patch set base.
+ // Context: Issue 7243
+ change.revisions[edit.commit.commit].actions =
+ change.revisions[edit.base_revision].actions;
+ }
+ }
+
+ _getChangeDetail() {
+ const detailCompletes = this.$.restAPI.getChangeDetail(
+ this._changeNum, this._handleGetChangeDetailError.bind(this));
+ const editCompletes = this._getEdit();
+ const prefCompletes = this._getPreferences();
+
+ return Promise.all([detailCompletes, editCompletes, prefCompletes])
+ .then(([change, edit, prefs]) => {
+ this._prefs = prefs;
+
+ if (!change) {
+ return '';
+ }
+ this._processEdit(change, edit);
+ // Issue 4190: Coalesce missing topics to null.
+ if (!change.topic) { change.topic = null; }
+ if (!change.reviewer_updates) {
+ change.reviewer_updates = null;
+ }
+ const latestRevisionSha = this._getLatestRevisionSHA(change);
+ const currentRevision = change.revisions[latestRevisionSha];
+ if (currentRevision.commit && currentRevision.commit.message) {
+ this._latestCommitMessage = this._prepareCommitMsgForLinkify(
+ currentRevision.commit.message);
+ } else {
+ this._latestCommitMessage = null;
+ }
+
+ const lineHeight = getComputedStyle(this).lineHeight;
+
+ // Slice returns a number as a string, convert to an int.
+ this._lineHeight =
+ parseInt(lineHeight.slice(0, lineHeight.length - 2), 10);
+
+ this._change = change;
+ if (!this._patchRange || !this._patchRange.patchNum ||
+ this.patchNumEquals(this._patchRange.patchNum,
+ currentRevision._number)) {
+ // CommitInfo.commit is optional, and may need patching.
+ if (!currentRevision.commit.commit) {
+ currentRevision.commit.commit = latestRevisionSha;
+ }
+ this._commitInfo = currentRevision.commit;
+ this._selectedRevision = currentRevision;
+ // TODO: Fetch and process files.
+ } else {
+ this._selectedRevision =
+ Object.values(this._change.revisions).find(
+ revision => {
+ // edit patchset is a special one
+ const thePatchNum = this._patchRange.patchNum;
+ if (thePatchNum === 'edit') {
+ return revision._number === thePatchNum;
+ }
+ return revision._number === parseInt(thePatchNum, 10);
+ });
+ }
+ });
+ }
+
+ _isSubmitEnabled(revisionActions) {
+ return !!(revisionActions && revisionActions.submit &&
+ revisionActions.submit.enabled);
+ }
+
+ _getEdit() {
+ return this.$.restAPI.getChangeEdit(this._changeNum, true);
+ }
+
+ _getLatestCommitMessage() {
+ return this.$.restAPI.getChangeCommitInfo(this._changeNum,
+ this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => {
+ if (!commitInfo) return Promise.resolve();
+ this._latestCommitMessage =
+ this._prepareCommitMsgForLinkify(commitInfo.message);
+ });
+ }
+
+ _getLatestRevisionSHA(change) {
+ if (change.current_revision) {
+ return change.current_revision;
+ }
+ // current_revision may not be present in the case where the latest rev is
+ // a draft and the user doesn’t have permission to view that rev.
+ let latestRev = null;
+ let latestPatchNum = -1;
+ for (const rev in change.revisions) {
+ if (!change.revisions.hasOwnProperty(rev)) { continue; }
+
+ if (change.revisions[rev]._number > latestPatchNum) {
+ latestRev = rev;
+ latestPatchNum = change.revisions[rev]._number;
+ }
+ }
+ return latestRev;
+ }
+
+ _getCommitInfo() {
+ return this.$.restAPI.getChangeCommitInfo(
+ this._changeNum, this._patchRange.patchNum).then(
+ commitInfo => {
+ this._commitInfo = commitInfo;
+ });
+ }
+
+ _reloadDraftsWithCallback(e) {
+ return this._reloadDrafts().then(() => e.detail.resolve());
+ }
+
+ /**
+ * Fetches a new changeComment object, and data for all types of comments
+ * (comments, robot comments, draft comments) is requested.
+ */
+ _reloadComments() {
+ return this.$.commentAPI.loadAll(this._changeNum)
+ .then(comments => this._recomputeComments(comments));
+ }
+
+ /**
+ * Fetches a new changeComment object, but only updated data for drafts is
+ * requested.
+ *
+ * TODO(taoalpha): clean up this and _reloadComments, as single comment
+ * can be a thread so it does not make sense to only update drafts
+ * without updating threads
+ */
+ _reloadDrafts() {
+ return this.$.commentAPI.reloadDrafts(this._changeNum)
+ .then(comments => this._recomputeComments(comments));
+ }
+
+ _recomputeComments(comments) {
+ this._changeComments = comments;
+ this._diffDrafts = Object.assign({}, this._changeComments.drafts);
+ this._commentThreads = this._changeComments.getAllThreadsForChange()
+ .map(c => Object.assign({}, c));
+ this._draftCommentThreads = this._commentThreads
+ .filter(c => c.comments[c.comments.length - 1].__draft);
+ }
+
+ /**
+ * Reload the change.
+ *
+ * @param {boolean=} opt_isLocationChange Reloads the related changes
+ * when true and ends reporting events that started on location change.
+ * @return {Promise} A promise that resolves when the core data has loaded.
+ * Some non-core data loading may still be in-flight when the core data
+ * promise resolves.
+ */
+ _reload(opt_isLocationChange) {
+ this._loading = true;
+ this._relatedChangesCollapsed = true;
+ this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
+ this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
+
+ // Array to house all promises related to data requests.
+ const allDataPromises = [];
+
+ // Resolves when the change detail and the edit patch set (if available)
+ // are loaded.
+ const detailCompletes = this._getChangeDetail();
+ allDataPromises.push(detailCompletes);
+
+ // Resolves when the loading flag is set to false, meaning that some
+ // change content may start appearing.
+ const loadingFlagSet = detailCompletes
+ .then(() => {
+ this._loading = false;
+ this.dispatchEvent(new CustomEvent('change-details-loaded',
+ {bubbles: true, composed: true}));
+ })
+ .then(() => {
+ this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
+ if (opt_isLocationChange) {
+ this.$.reporting.changeDisplayed();
+ }
+ });
+
+ // Resolves when the project config has loaded.
+ const projectConfigLoaded = detailCompletes
+ .then(() => this._getProjectConfig());
+ allDataPromises.push(projectConfigLoaded);
+
+ // Resolves when change comments have loaded (comments, drafts and robot
+ // comments).
+ const commentsLoaded = this._reloadComments();
+ allDataPromises.push(commentsLoaded);
+
+ let coreDataPromise;
+
+ // If the patch number is specified
+ if (this._patchRange && this._patchRange.patchNum) {
+ // Because a specific patchset is specified, reload the resources that
+ // are keyed by patch number or patch range.
+ const patchResourcesLoaded = this._reloadPatchNumDependentResources();
+ allDataPromises.push(patchResourcesLoaded);
+
+ // Promise resolves when the change detail and patch dependent resources
+ // have loaded.
+ const detailAndPatchResourcesLoaded =
+ Promise.all([patchResourcesLoaded, loadingFlagSet]);
+
+ // Promise resolves when mergeability information has loaded.
+ const mergeabilityLoaded = detailAndPatchResourcesLoaded
+ .then(() => this._getMergeability());
+ allDataPromises.push(mergeabilityLoaded);
+
+ // Promise resovles when the change actions have loaded.
+ const actionsLoaded = detailAndPatchResourcesLoaded
+ .then(() => this.$.actions.reload());
+ allDataPromises.push(actionsLoaded);
+
+ // The core data is loaded when both mergeability and actions are known.
+ coreDataPromise = Promise.all([mergeabilityLoaded, actionsLoaded]);
+ } else {
+ // Resolves when the file list has loaded.
+ const fileListReload = loadingFlagSet
+ .then(() => this.$.fileList.reload());
+ allDataPromises.push(fileListReload);
+
+ const latestCommitMessageLoaded = loadingFlagSet.then(() => {
+ // If the latest commit message is known, there is nothing to do.
+ if (this._latestCommitMessage) { return Promise.resolve(); }
+ return this._getLatestCommitMessage();
+ });
+ allDataPromises.push(latestCommitMessageLoaded);
+
+ // Promise resolves when mergeability information has loaded.
+ const mergeabilityLoaded = loadingFlagSet
+ .then(() => this._getMergeability());
+ allDataPromises.push(mergeabilityLoaded);
+
+ // Core data is loaded when mergeability has been loaded.
+ coreDataPromise = mergeabilityLoaded;
+ }
+
+ if (opt_isLocationChange) {
+ const relatedChangesLoaded = coreDataPromise
+ .then(() => this.$.relatedChanges.reload());
+ allDataPromises.push(relatedChangesLoaded);
+ }
+
+ Promise.all(allDataPromises).then(() => {
+ this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
+ if (opt_isLocationChange) {
+ this.$.reporting.changeFullyLoaded();
+ }
+ });
+
+ return coreDataPromise;
+ }
+
+ /**
+ * Kicks off requests for resources that rely on the patch range
+ * (`this._patchRange`) being defined.
+ */
+ _reloadPatchNumDependentResources() {
+ return Promise.all([
+ this._getCommitInfo(),
+ this.$.fileList.reload(),
+ ]);
+ }
+
+ _getMergeability() {
+ if (!this._change) {
+ this._mergeable = null;
+ return Promise.resolve();
+ }
+ // If the change is closed, it is not mergeable. Note: already merged
+ // changes are obviously not mergeable, but the mergeability API will not
+ // answer for abandoned changes.
+ if (this._change.status === this.ChangeStatus.MERGED ||
+ this._change.status === this.ChangeStatus.ABANDONED) {
+ this._mergeable = false;
+ return Promise.resolve();
+ }
+
+ this._mergeable = null;
+ return this.$.restAPI.getMergeable(this._changeNum).then(m => {
+ this._mergeable = m.mergeable;
+ });
+ }
+
+ _computeCanStartReview(change) {
+ return !!(change.actions && change.actions.ready &&
+ change.actions.ready.enabled);
+ }
+
+ _computeReplyDisabled() { return false; }
+
+ _computeChangePermalinkAriaLabel(changeNum) {
+ return 'Change ' + changeNum;
+ }
+
+ _computeCommitMessageCollapsed(collapsed, collapsible) {
+ return collapsible && collapsed;
+ }
+
+ _computeRelatedChangesClass(collapsed) {
+ return collapsed ? 'collapsed' : '';
+ }
+
+ _computeCollapseText(collapsed) {
+ // Symbols are up and down triangles.
+ return collapsed ? '\u25bc Show more' : '\u25b2 Show less';
+ }
+
+ /**
+ * Returns the text to be copied when
+ * click the copy icon next to change subject
+ *
+ * @param {!Object} change
+ */
+ _computeCopyTextForTitle(change) {
+ return `${change._number}: ${change.subject}` +
+ ` | https://${location.host}${this._computeChangeUrl(change)}`;
+ }
+
+ _toggleCommitCollapsed() {
+ this._commitCollapsed = !this._commitCollapsed;
+ if (this._commitCollapsed) {
+ window.scrollTo(0, 0);
+ }
+ }
+
+ _toggleRelatedChangesCollapsed() {
+ this._relatedChangesCollapsed = !this._relatedChangesCollapsed;
+ if (this._relatedChangesCollapsed) {
+ window.scrollTo(0, 0);
+ }
+ }
+
+ _computeCommitCollapsible(commitMessage) {
+ if (!commitMessage) { return false; }
+ return commitMessage.split('\n').length >= MIN_LINES_FOR_COMMIT_COLLAPSE;
+ }
+
+ _getOffsetHeight(element) {
+ return element.offsetHeight;
+ }
+
+ _getScrollHeight(element) {
+ return element.scrollHeight;
+ }
+
+ /**
+ * Get the line height of an element to the nearest integer.
+ */
+ _getLineHeight(element) {
+ const lineHeightStr = getComputedStyle(element).lineHeight;
+ return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2));
+ }
+
+ /**
+ * New max height for the related changes section, shorter than the existing
+ * change info height.
+ */
+ _updateRelatedChangeMaxHeight() {
+ // Takes into account approximate height for the expand button and
+ // bottom margin.
+ const EXTRA_HEIGHT = 30;
+ let newHeight;
+
+ if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`)
+ .matches) {
+ // In a small (mobile) view, give the relation chain some space.
+ newHeight = SMALL_RELATED_HEIGHT;
+ } else if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`)
+ .matches) {
+ // Since related changes are below the commit message, but still next to
+ // metadata, the height should be the height of the metadata minus the
+ // height of the commit message to reduce jank. However, if that doesn't
+ // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT.
+ // Note: extraHeight is to take into account margin/padding.
+ const medRelatedHeight = Math.max(
+ this._getOffsetHeight(this.$.mainChangeInfo) -
+ this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT,
+ MINIMUM_RELATED_MAX_HEIGHT);
+ newHeight = medRelatedHeight;
+ } else {
+ if (this._commitCollapsible) {
+ // Make sure the content is lined up if both areas have buttons. If
+ // the commit message is not collapsed, instead use the change info
+ // height.
+ newHeight = this._getOffsetHeight(this.$.commitMessage);
+ } else {
+ newHeight = this._getOffsetHeight(this.$.commitAndRelated) -
+ EXTRA_HEIGHT;
+ }
+ }
+ const stylesToUpdate = {};
+
+ // Get the line height of related changes, and convert it to the nearest
+ // integer.
+ const lineHeight = this._getLineHeight(this.$.relatedChanges);
+
+ // Figure out a new height that is divisible by the rounded line height.
+ const remainder = newHeight % lineHeight;
+ newHeight = newHeight - remainder;
+
+ stylesToUpdate['--relation-chain-max-height'] = newHeight + 'px';
+
+ // Update the max-height of the relation chain to this new height.
+ if (this._commitCollapsible) {
+ stylesToUpdate['--related-change-btn-top-padding'] = remainder + 'px';
+ }
+
+ this.updateStyles(stylesToUpdate);
+ }
+
+ _computeShowRelatedToggle() {
+ // Make sure the max height has been applied, since there is now content
+ // to populate.
+ if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
+ this._updateRelatedChangeMaxHeight();
+ }
+ // Prevents showMore from showing when click on related change, since the
+ // line height would be positive, but related changes height is 0.
+ if (!this._getScrollHeight(this.$.relatedChanges)) {
+ return this._showRelatedToggle = false;
+ }
+
+ if (this._getScrollHeight(this.$.relatedChanges) >
+ (this._getOffsetHeight(this.$.relatedChanges) +
+ this._getLineHeight(this.$.relatedChanges))) {
+ return this._showRelatedToggle = true;
+ }
+ this._showRelatedToggle = false;
+ }
+
+ _updateToggleContainerClass(showRelatedToggle) {
+ if (showRelatedToggle) {
+ this.$.relatedChangesToggle.classList.add('showToggle');
+ } else {
+ this.$.relatedChangesToggle.classList.remove('showToggle');
+ }
+ }
+
+ _startUpdateCheckTimer() {
+ if (!this._serverConfig ||
+ !this._serverConfig.change ||
+ this._serverConfig.change.update_delay === undefined ||
+ this._serverConfig.change.update_delay <= MIN_CHECK_INTERVAL_SECS) {
+ return;
+ }
+
+ this._updateCheckTimerHandle = this.async(() => {
+ this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => {
+ let toastMessage = null;
+ if (!result.isLatest) {
+ toastMessage = ReloadToastMessage.NEWER_REVISION;
+ } else if (result.newStatus === this.ChangeStatus.MERGED) {
+ toastMessage = ReloadToastMessage.MERGED;
+ } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
+ toastMessage = ReloadToastMessage.ABANDONED;
+ } else if (result.newStatus === this.ChangeStatus.NEW) {
+ toastMessage = ReloadToastMessage.RESTORED;
+ } else if (result.newMessages) {
+ toastMessage = ReloadToastMessage.NEW_MESSAGE;
+ }
+
+ if (!toastMessage) {
+ this._startUpdateCheckTimer();
+ return;
+ }
+
+ this._cancelUpdateCheckTimer();
+ this.fire('show-alert', {
+ message: toastMessage,
+ // Persist this alert.
+ dismissOnNavigation: true,
+ action: 'Reload',
+ callback: function() {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this._change);
+ }.bind(this),
+ });
+ });
+ }, this._serverConfig.change.update_delay * 1000);
+ }
+
+ _cancelUpdateCheckTimer() {
+ if (this._updateCheckTimerHandle) {
+ this.cancelAsync(this._updateCheckTimerHandle);
+ }
+ this._updateCheckTimerHandle = null;
+ }
+
+ _handleVisibilityChange() {
+ if (document.hidden && this._updateCheckTimerHandle) {
+ this._cancelUpdateCheckTimer();
+ } else if (!this._updateCheckTimerHandle) {
+ this._startUpdateCheckTimer();
+ }
+ }
+
+ _handleTopicChanged() {
+ this.$.relatedChanges.reload();
+ }
+
+ _computeHeaderClass(editMode) {
+ const classes = ['header'];
+ if (editMode) { classes.push('editMode'); }
+ return classes.join(' ');
+ }
+
+ _computeEditMode(patchRangeRecord, paramsRecord) {
+ if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (paramsRecord.base && paramsRecord.base.edit) { return true; }
+
+ const patchRange = patchRangeRecord.base || {};
+ return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
+ }
+
+ _handleFileActionTap(e) {
+ e.preventDefault();
+ const controls = this.$.fileListHeader.$.editControls;
+ const path = e.detail.path;
+ switch (e.detail.action) {
+ case GrEditConstants.Actions.DELETE.id:
+ controls.openDeleteDialog(path);
+ break;
+ case GrEditConstants.Actions.OPEN.id:
+ Gerrit.Nav.navigateToRelativeUrl(
+ Gerrit.Nav.getEditUrlForDiff(this._change, path,
+ this._patchRange.patchNum));
+ break;
+ case GrEditConstants.Actions.RENAME.id:
+ controls.openRenameDialog(path);
+ break;
+ case GrEditConstants.Actions.RESTORE.id:
+ controls.openRestoreDialog(path);
+ break;
+ }
+ }
+
+ _computeCommitMessageKey(number, revision) {
+ return `c${number}_rev${revision}`;
+ }
+
+ _patchNumChanged(patchNumStr) {
+ if (!this._selectedRevision) {
+ return;
+ }
+
+ let patchNum = parseInt(patchNumStr, 10);
+ if (patchNumStr === 'edit') {
+ patchNum = patchNumStr;
+ }
+
+ if (patchNum === this._selectedRevision._number) {
+ return;
+ }
+ this._selectedRevision = Object.values(this._change.revisions).find(
+ revision => revision._number === patchNum);
+ }
+
+ /**
+ * If an edit exists already, load it. Otherwise, toggle edit mode via the
+ * navigation API.
+ */
+ _handleEditTap() {
+ const editInfo = Object.values(this._change.revisions).find(info =>
+ info._number === this.EDIT_NAME);
+
+ if (editInfo) {
+ Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME);
+ return;
+ }
+
+ // Avoid putting patch set in the URL unless a non-latest patch set is
+ // selected.
+ let patchNum;
+ if (!this.patchNumEquals(this._patchRange.patchNum,
+ this.computeLatestPatchNum(this._allPatchSets))) {
+ patchNum = this._patchRange.patchNum;
+ }
+ Gerrit.Nav.navigateToChange(this._change, patchNum, null, true);
+ }
+
+ _handleStopEditTap() {
+ Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum);
+ }
+
+ _resetReplyOverlayFocusStops() {
+ this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+ }
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ }
+
+ _getRevisionInfo(change) {
+ return new Gerrit.RevisionInfo(change);
+ }
+
+ _computeCurrentRevision(currentRevision, revisions) {
+ return currentRevision && revisions && revisions[currentRevision];
+ }
+
+ _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
+ return disableDiffPrefs || !loggedIn;
+ }
+}
+
+customElements.define(GrChangeView.is, GrChangeView);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
index 9a53342..b7fdbb7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -1,63 +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="/bower_components/polymer/polymer.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/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../../edit/gr-edit-constants.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
-<link rel="import" href="../../shared/gr-change-status/gr-change-status.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-editable-content/gr-editable-content.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
-<link rel="import" href="../gr-change-actions/gr-change-actions.html">
-<link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../gr-commit-info/gr-commit-info.html">
-<link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
-<link rel="import" href="../gr-file-list-header/gr-file-list-header.html">
-<link rel="import" href="../gr-file-list/gr-file-list.html">
-<link rel="import" href="../gr-included-in-dialog/gr-included-in-dialog.html">
-<link rel="import" href="../gr-messages-list/gr-messages-list.html">
-<link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html">
-<link rel="import" href="../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html">
-<link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html">
-<link rel="import" href="../gr-thread-list/gr-thread-list.html">
-<link rel="import" href="../gr-upload-help-dialog/gr-upload-help-dialog.html">
-
-<dom-module id="gr-change-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.container:not(.loading) {
background-color: var(--background-color-tertiary);
@@ -374,136 +333,61 @@
margin: var(--spacing-m);
}
</style>
- <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
- <div
- id="mainContent"
- class="container"
- on-show-checks-table="_handleShowTab"
- hidden$="{{_loading}}">
+ <div class="container loading" hidden\$="[[!_loading]]">Loading...</div>
+ <div id="mainContent" class="container" on-show-checks-table="_handleShowTab" hidden\$="{{_loading}}">
<section class="changeInfoSection">
- <div class$="[[_computeHeaderClass(_editMode)]]">
+ <div class\$="[[_computeHeaderClass(_editMode)]]">
<div class="headerTitle">
<div class="changeStatuses">
<template is="dom-repeat" items="[[_changeStatuses]]" as="status">
- <gr-change-status
- max-width="100"
- status="[[status]]"></gr-change-status>
+ <gr-change-status max-width="100" status="[[status]]"></gr-change-status>
</template>
</div>
<div class="statusText">
- <template
- is="dom-if"
- if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]">
+ <template is="dom-if" if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]">
<span class="text"> as </span>
- <gr-commit-info
- change="[[_change]]"
- commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]"
- server-config="[[_serverConfig]]"></gr-commit-info>
+ <gr-commit-info change="[[_change]]" commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]" server-config="[[_serverConfig]]"></gr-commit-info>
</template>
</div>
- <gr-change-star
- id="changeStar"
- change="{{_change}}"
- on-toggle-star="_handleToggleStar"
- hidden$="[[!_loggedIn]]"></gr-change-star>
+ <gr-change-star id="changeStar" change="{{_change}}" on-toggle-star="_handleToggleStar" hidden\$="[[!_loggedIn]]"></gr-change-star>
- <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
- href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
+ <a aria-label\$="[[_computeChangePermalinkAriaLabel(_change._number)]]" href\$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
<span class="changeNumberColon">: </span>
<span class="headerSubject">[[_change.subject]]</span>
- <gr-copy-clipboard
- class="changeCopyClipboard"
- hide-input
- text="[[_computeCopyTextForTitle(_change)]]">
+ <gr-copy-clipboard class="changeCopyClipboard" hide-input="" text="[[_computeCopyTextForTitle(_change)]]">
</gr-copy-clipboard>
</div><!-- end headerTitle -->
- <div class="commitActions" hidden$="[[!_loggedIn]]">
- <gr-change-actions
- id="actions"
- change="[[_change]]"
- disable-edit="[[disableEdit]]"
- has-parent="[[hasParent]]"
- actions="[[_change.actions]]"
- revision-actions="{{_currentRevisionActions}}"
- change-num="[[_changeNum]]"
- change-status="[[_change.status]]"
- commit-num="[[_commitInfo.commit]]"
- latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
- commit-message="[[_latestCommitMessage]]"
- edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]"
- edit-mode="[[_editMode]]"
- edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
- private-by-default="[[_projectConfig.private_by_default]]"
- on-reload-change="_handleReloadChange"
- on-edit-tap="_handleEditTap"
- on-stop-edit-tap="_handleStopEditTap"
- on-download-tap="_handleOpenDownloadDialog"></gr-change-actions>
+ <div class="commitActions" hidden\$="[[!_loggedIn]]">
+ <gr-change-actions id="actions" change="[[_change]]" disable-edit="[[disableEdit]]" has-parent="[[hasParent]]" actions="[[_change.actions]]" revision-actions="{{_currentRevisionActions}}" change-num="[[_changeNum]]" change-status="[[_change.status]]" commit-num="[[_commitInfo.commit]]" latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]" commit-message="[[_latestCommitMessage]]" edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]" edit-mode="[[_editMode]]" edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]" private-by-default="[[_projectConfig.private_by_default]]" on-reload-change="_handleReloadChange" on-edit-tap="_handleEditTap" on-stop-edit-tap="_handleStopEditTap" on-download-tap="_handleOpenDownloadDialog"></gr-change-actions>
</div><!-- end commit actions -->
</div><!-- end header -->
<div class="changeInfo">
<div class="changeInfo-column changeMetadata hideOnMobileOverlay">
- <gr-change-metadata
- id="metadata"
- change="{{_change}}"
- account="[[_account]]"
- revision="[[_selectedRevision]]"
- commit-info="[[_commitInfo]]"
- server-config="[[_serverConfig]]"
- parent-is-current="[[_parentIsCurrent]]"
- on-show-reply-dialog="_handleShowReplyDialog">
+ <gr-change-metadata id="metadata" change="{{_change}}" account="[[_account]]" revision="[[_selectedRevision]]" commit-info="[[_commitInfo]]" server-config="[[_serverConfig]]" parent-is-current="[[_parentIsCurrent]]" on-show-reply-dialog="_handleShowReplyDialog">
</gr-change-metadata>
</div>
<div id="mainChangeInfo" class="changeInfo-column mainChangeInfo">
<div id="commitAndRelated" class="hideOnMobileOverlay">
<div class="commitContainer">
<div>
- <gr-button
- id="replyBtn"
- class="reply"
- title="[[createTitle(Shortcut.OPEN_REPLY_DIALOG,
- ShortcutSection.ACTIONS)]]"
- hidden$="[[!_loggedIn]]"
- primary
- disabled="[[_replyDisabled]]"
- on-click="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+ <gr-button id="replyBtn" class="reply" title="[[createTitle(Shortcut.OPEN_REPLY_DIALOG,
+ ShortcutSection.ACTIONS)]]" hidden\$="[[!_loggedIn]]" primary="" disabled="[[_replyDisabled]]" on-click="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
</div>
- <div
- id="commitMessage"
- class="commitMessage">
- <gr-editable-content id="commitMessageEditor"
- editing="[[_editingCommitMessage]]"
- content="{{_latestCommitMessage}}"
- storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]"
- remove-zero-width-space
- collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]">
- <gr-linked-text pre
- content="[[_latestCommitMessage]]"
- config="[[_projectConfig.commentlinks]]"
- remove-zero-width-space></gr-linked-text>
+ <div id="commitMessage" class="commitMessage">
+ <gr-editable-content id="commitMessageEditor" editing="[[_editingCommitMessage]]" content="{{_latestCommitMessage}}" storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]" remove-zero-width-space="" collapsed\$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]">
+ <gr-linked-text pre="" content="[[_latestCommitMessage]]" config="[[_projectConfig.commentlinks]]" remove-zero-width-space=""></gr-linked-text>
</gr-editable-content>
- <gr-button link
- class="editCommitMessage"
- on-click="_handleEditCommitMessage"
- hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
- <div class="changeId" hidden$="[[!_changeIdCommitMessageError]]">
+ <gr-button link="" class="editCommitMessage" on-click="_handleEditCommitMessage" hidden\$="[[_hideEditCommitMessage]]">Edit</gr-button>
+ <div class="changeId" hidden\$="[[!_changeIdCommitMessageError]]">
<hr>
Change-Id:
- <span
- class$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]"
- title$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]">
+ <span class\$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]" title\$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]">
[[_change.change_id]]
</span>
</div>
</div>
- <div
- id="commitCollapseToggle"
- class="collapseToggleContainer"
- hidden$="[[!_commitCollapsible]]">
- <gr-button
- link
- id="commitCollapseToggleButton"
- class="collapseToggleButton"
- on-click="_toggleCommitCollapsed">
+ <div id="commitCollapseToggle" class="collapseToggleContainer" hidden\$="[[!_commitCollapsible]]">
+ <gr-button link="" id="commitCollapseToggleButton" class="collapseToggleButton" on-click="_toggleCommitCollapsed">
[[_computeCollapseText(_commitCollapsed)]]
</gr-button>
</div>
@@ -515,23 +399,10 @@
</gr-endpoint-decorator>
</div>
<div class="relatedChanges">
- <gr-related-changes-list id="relatedChanges"
- class$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]"
- change="[[_change]]"
- mergeable="[[_mergeable]]"
- has-parent="{{hasParent}}"
- on-update="_updateRelatedChangeMaxHeight"
- patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
- on-new-section-loaded="_computeShowRelatedToggle">
+ <gr-related-changes-list id="relatedChanges" class\$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]" change="[[_change]]" mergeable="[[_mergeable]]" has-parent="{{hasParent}}" on-update="_updateRelatedChangeMaxHeight" patch-num="[[computeLatestPatchNum(_allPatchSets)]]" on-new-section-loaded="_computeShowRelatedToggle">
</gr-related-changes-list>
- <div
- id="relatedChangesToggle"
- class="collapseToggleContainer">
- <gr-button
- link
- id="relatedChangesToggleButton"
- class="collapseToggleButton"
- on-click="_toggleRelatedChangesCollapsed">
+ <div id="relatedChangesToggle" class="collapseToggleContainer">
+ <gr-button link="" id="relatedChangesToggleButton" class="collapseToggleButton" on-click="_toggleRelatedChangesCollapsed">
[[_computeCollapseText(_relatedChangesCollapsed)]]
</gr-button>
</div>
@@ -542,11 +413,10 @@
</section>
<paper-tabs id="primaryTabs" on-selected-changed="_handleFileTabChange">
- <paper-tab data-name$="[[_files_tab_name]]">Files</paper-tab>
- <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]"
- as="tabHeader">
- <paper-tab data-name$="[[tabHeader]]">
- <gr-endpoint-decorator name$="[[tabHeader]]">
+ <paper-tab data-name\$="[[_files_tab_name]]">Files</paper-tab>
+ <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]" as="tabHeader">
+ <paper-tab data-name\$="[[tabHeader]]">
+ <gr-endpoint-decorator name\$="[[tabHeader]]">
<gr-endpoint-param name="change" value="[[_change]]">
</gr-endpoint-param>
<gr-endpoint-param name="revision" value="[[_selectedRevision]]">
@@ -554,78 +424,23 @@
</gr-endpoint-decorator>
</paper-tab>
</template>
- <paper-tab data-name$="[[_findings_tab_name]]">
+ <paper-tab data-name\$="[[_findings_tab_name]]">
Findings
</paper-tab>
</paper-tabs>
<section class="patchInfo">
- <div hidden$="[[!_findIfTabMatches(_currentTabName, _files_tab_name)]]">
- <gr-file-list-header
- id="fileListHeader"
- account="[[_account]]"
- all-patch-sets="[[_allPatchSets]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- revision-info="[[_revisionInfo]]"
- change-comments="[[_changeComments]]"
- commit-info="[[_commitInfo]]"
- change-url="[[_computeChangeUrl(_change)]]"
- edit-mode="[[_editMode]]"
- logged-in="[[_loggedIn]]"
- server-config="[[_serverConfig]]"
- shown-file-count="[[_shownFileCount]]"
- diff-prefs="[[_diffPrefs]]"
- diff-view-mode="{{viewState.diffMode}}"
- patch-num="{{_patchRange.patchNum}}"
- base-patch-num="{{_patchRange.basePatchNum}}"
- files-expanded="[[_filesExpanded]]"
- diff-prefs-disabled="[[_diffPrefsDisabled]]"
- on-open-diff-prefs="_handleOpenDiffPrefs"
- on-open-download-dialog="_handleOpenDownloadDialog"
- on-open-upload-help-dialog="_handleOpenUploadHelpDialog"
- on-open-included-in-dialog="_handleOpenIncludedInDialog"
- on-expand-diffs="_expandAllDiffs"
- on-collapse-diffs="_collapseAllDiffs">
+ <div hidden\$="[[!_findIfTabMatches(_currentTabName, _files_tab_name)]]">
+ <gr-file-list-header id="fileListHeader" account="[[_account]]" all-patch-sets="[[_allPatchSets]]" change="[[_change]]" change-num="[[_changeNum]]" revision-info="[[_revisionInfo]]" change-comments="[[_changeComments]]" commit-info="[[_commitInfo]]" change-url="[[_computeChangeUrl(_change)]]" edit-mode="[[_editMode]]" logged-in="[[_loggedIn]]" server-config="[[_serverConfig]]" shown-file-count="[[_shownFileCount]]" diff-prefs="[[_diffPrefs]]" diff-view-mode="{{viewState.diffMode}}" patch-num="{{_patchRange.patchNum}}" base-patch-num="{{_patchRange.basePatchNum}}" files-expanded="[[_filesExpanded]]" diff-prefs-disabled="[[_diffPrefsDisabled]]" on-open-diff-prefs="_handleOpenDiffPrefs" on-open-download-dialog="_handleOpenDownloadDialog" on-open-upload-help-dialog="_handleOpenUploadHelpDialog" on-open-included-in-dialog="_handleOpenIncludedInDialog" on-expand-diffs="_expandAllDiffs" on-collapse-diffs="_collapseAllDiffs">
</gr-file-list-header>
- <gr-file-list
- id="fileList"
- class="hideOnMobileOverlay"
- diff-prefs="{{_diffPrefs}}"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- patch-range="{{_patchRange}}"
- change-comments="[[_changeComments]]"
- drafts="[[_diffDrafts]]"
- revisions="[[_change.revisions]]"
- project-config="[[_projectConfig]]"
- selected-index="{{viewState.selectedFileIndex}}"
- diff-view-mode="[[viewState.diffMode]]"
- edit-mode="[[_editMode]]"
- num-files-shown="{{_numFilesShown}}"
- files-expanded="{{_filesExpanded}}"
- file-list-increment="{{_numFilesShown}}"
- on-files-shown-changed="_setShownFiles"
- on-file-action-tap="_handleFileActionTap"
- on-reload-drafts="_reloadDraftsWithCallback">
+ <gr-file-list id="fileList" class="hideOnMobileOverlay" diff-prefs="{{_diffPrefs}}" change="[[_change]]" change-num="[[_changeNum]]" patch-range="{{_patchRange}}" change-comments="[[_changeComments]]" drafts="[[_diffDrafts]]" revisions="[[_change.revisions]]" project-config="[[_projectConfig]]" selected-index="{{viewState.selectedFileIndex}}" diff-view-mode="[[viewState.diffMode]]" edit-mode="[[_editMode]]" num-files-shown="{{_numFilesShown}}" files-expanded="{{_filesExpanded}}" file-list-increment="{{_numFilesShown}}" on-files-shown-changed="_setShownFiles" on-file-action-tap="_handleFileActionTap" on-reload-drafts="_reloadDraftsWithCallback">
</gr-file-list>
</div>
<template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _findings_tab_name)]]">
- <gr-dropdown-list
- class="patch-set-dropdown"
- items="[[_robotCommentsPatchSetDropdownItems]]"
- on-value-change="_handleRobotCommentPatchSetChanged"
- value="[[_currentRobotCommentsPatchSet]]">
+ <gr-dropdown-list class="patch-set-dropdown" items="[[_robotCommentsPatchSetDropdownItems]]" on-value-change="_handleRobotCommentPatchSetChanged" value="[[_currentRobotCommentsPatchSet]]">
</gr-dropdown-list>
- <gr-thread-list
- threads="[[_robotCommentThreads]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- logged-in="[[_loggedIn]]"
- tab="[[_findings_tab_name]]"
- hide-toggle-buttons
- on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
+ <gr-thread-list threads="[[_robotCommentThreads]]" change="[[_change]]" change-num="[[_changeNum]]" logged-in="[[_loggedIn]]" tab="[[_findings_tab_name]]" hide-toggle-buttons="" on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
<template is="dom-if" if="[[_showRobotCommentsButton]]">
<gr-button class="show-robot-comments" on-click="_toggleShowRobotComments">
[[_computeShowText(_showAllRobotComments)]]
@@ -634,7 +449,7 @@
</template>
<template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _selectedTabPluginHeader)]]">
- <gr-endpoint-decorator name$="[[_selectedTabPluginEndpoint]]">
+ <gr-endpoint-decorator name\$="[[_selectedTabPluginEndpoint]]">
<gr-endpoint-param name="change" value="[[_change]]">
</gr-endpoint-param>
<gr-endpoint-param name="revision" value="[[_selectedRevision]]">
@@ -650,94 +465,41 @@
</gr-endpoint-param>
</gr-endpoint-decorator>
- <paper-tabs
- id="commentTabs"
- on-selected-changed="_handleCommentTabChange">
+ <paper-tabs id="commentTabs" on-selected-changed="_handleCommentTabChange">
<paper-tab class="changeLog">Change Log</paper-tab>
- <paper-tab
- class="commentThreads">
- <gr-tooltip-content
- has-tooltip
- title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]">
+ <paper-tab class="commentThreads">
+ <gr-tooltip-content has-tooltip="" title\$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]">
<span>Comment Threads</span></gr-tooltip-content>
</paper-tab>
</paper-tabs>
<section class="changeLog">
<template is="dom-if" if="[[_isSelectedView(_currentView,
_commentTabs.CHANGE_LOG)]]">
- <gr-messages-list
- class="hideOnMobileOverlay"
- change-num="[[_changeNum]]"
- labels="[[_change.labels]]"
- messages="[[_change.messages]]"
- reviewer-updates="[[_change.reviewer_updates]]"
- change-comments="[[_changeComments]]"
- project-name="[[_change.project]]"
- show-reply-buttons="[[_loggedIn]]"
- on-message-anchor-tap="_handleMessageAnchorTap"
- on-reply="_handleMessageReply"></gr-messages-list>
+ <gr-messages-list class="hideOnMobileOverlay" change-num="[[_changeNum]]" labels="[[_change.labels]]" messages="[[_change.messages]]" reviewer-updates="[[_change.reviewer_updates]]" change-comments="[[_changeComments]]" project-name="[[_change.project]]" show-reply-buttons="[[_loggedIn]]" on-message-anchor-tap="_handleMessageAnchorTap" on-reply="_handleMessageReply"></gr-messages-list>
</template>
<template is="dom-if" if="[[_isSelectedView(_currentView,
_commentTabs.COMMENT_THREADS)]]">
- <gr-thread-list
- threads="[[_commentThreads]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- logged-in="[[_loggedIn]]"
- only-show-robot-comments-with-human-reply
- on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
+ <gr-thread-list threads="[[_commentThreads]]" change="[[_change]]" change-num="[[_changeNum]]" logged-in="[[_loggedIn]]" only-show-robot-comments-with-human-reply="" on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
</template>
</section>
</div>
- <gr-apply-fix-dialog
- id="applyFixDialog"
- prefs="[[_diffPrefs]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"></gr-apply-fix-dialog>
- <gr-overlay id="downloadOverlay" with-backdrop>
- <gr-download-dialog
- id="downloadDialog"
- change="[[_change]]"
- patch-num="[[_patchRange.patchNum]]"
- config="[[_serverConfig.download]]"
- on-close="_handleDownloadDialogClose"></gr-download-dialog>
+ <gr-apply-fix-dialog id="applyFixDialog" prefs="[[_diffPrefs]]" change="[[_change]]" change-num="[[_changeNum]]"></gr-apply-fix-dialog>
+ <gr-overlay id="downloadOverlay" with-backdrop="">
+ <gr-download-dialog id="downloadDialog" change="[[_change]]" patch-num="[[_patchRange.patchNum]]" config="[[_serverConfig.download]]" on-close="_handleDownloadDialogClose"></gr-download-dialog>
</gr-overlay>
- <gr-overlay id="uploadHelpOverlay" with-backdrop>
- <gr-upload-help-dialog
- revision="[[_currentRevision]]"
- target-branch="[[_change.branch]]"
- on-close="_handleCloseUploadHelpDialog"></gr-upload-help-dialog>
+ <gr-overlay id="uploadHelpOverlay" with-backdrop="">
+ <gr-upload-help-dialog revision="[[_currentRevision]]" target-branch="[[_change.branch]]" on-close="_handleCloseUploadHelpDialog"></gr-upload-help-dialog>
</gr-overlay>
- <gr-overlay id="includedInOverlay" with-backdrop>
- <gr-included-in-dialog
- id="includedInDialog"
- change-num="[[_changeNum]]"
- on-close="_handleIncludedInDialogClose"></gr-included-in-dialog>
+ <gr-overlay id="includedInOverlay" with-backdrop="">
+ <gr-included-in-dialog id="includedInDialog" change-num="[[_changeNum]]" on-close="_handleIncludedInDialogClose"></gr-included-in-dialog>
</gr-overlay>
- <gr-overlay id="replyOverlay"
- class="scrollable"
- no-cancel-on-outside-click
- no-cancel-on-esc-key
- with-backdrop>
- <gr-reply-dialog id="replyDialog"
- change="{{_change}}"
- patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
- permitted-labels="[[_change.permitted_labels]]"
- draft-comment-threads="[[_draftCommentThreads]]"
- project-config="[[_projectConfig]]"
- can-be-started="[[_canStartReview]]"
- on-send="_handleReplySent"
- on-cancel="_handleReplyCancel"
- on-autogrow="_handleReplyAutogrow"
- on-send-disabled-changed="_resetReplyOverlayFocusStops"
- hidden$="[[!_loggedIn]]">
+ <gr-overlay id="replyOverlay" class="scrollable" no-cancel-on-outside-click="" no-cancel-on-esc-key="" with-backdrop="">
+ <gr-reply-dialog id="replyDialog" change="{{_change}}" patch-num="[[computeLatestPatchNum(_allPatchSets)]]" permitted-labels="[[_change.permitted_labels]]" draft-comment-threads="[[_draftCommentThreads]]" project-config="[[_projectConfig]]" can-be-started="[[_canStartReview]]" on-send="_handleReplySent" on-cancel="_handleReplyCancel" on-autogrow="_handleReplyAutogrow" on-send-disabled-changed="_resetReplyOverlayFocusStops" hidden\$="[[!_loggedIn]]">
</gr-reply-dialog>
</gr-overlay>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-comment-api id="commentAPI"></gr-comment-api>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-change-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 26bcbf2..4dc0e9b3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_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-change-view</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="../../edit/gr-edit-constants.html">
-<link rel="import" href="gr-change-view.html">
+<script type="module" src="../../edit/gr-edit-constants.js"></script>
+<script type="module" src="./gr-change-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../edit/gr-edit-constants.js';
+import './gr-change-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -44,887 +50,667 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-view tests', async () => {
- await readyToTest();
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
- kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
- kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
- kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
- kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
- kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../edit/gr-edit-constants.js';
+import './gr-change-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-change-view tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
+ kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
+ kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+ kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+ kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+ kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+ kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
- let element;
- let sandbox;
- let navigateToChangeStub;
- const TEST_SCROLL_TOP_PX = 100;
+ let element;
+ let sandbox;
+ let navigateToChangeStub;
+ const TEST_SCROLL_TOP_PX = 100;
- const CommentTabs = {
- CHANGE_LOG: 0,
- COMMENT_THREADS: 1,
+ const CommentTabs = {
+ CHANGE_LOG: 0,
+ COMMENT_THREADS: 1,
+ };
+ const ROBOT_COMMENTS_LIMIT = 10;
+
+ const THREADS = [
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2,
+ robot_id: 'rb1',
+ id: 'ecf9fa_fe1a5f62',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'ecf0b9fa_fe1a5f62',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ id: '503008e2_0ab203ee',
+ path: '/COMMIT_MSG',
+ line: 5,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
+ updated: '2018-02-13 22:48:48.018000000',
+ message: 'draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'ecf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 3,
+ id: 'ecf0b9fa_fe5f62',
+ robot_id: 'rb2',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ __path: 'test.txt',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 3,
+ id: '09a9fb0a_1484e6cf',
+ side: 'PARENT',
+ updated: '2018-02-13 22:47:19.000000000',
+ message: 'Some comment on another patchset.',
+ unresolved: false,
+ },
+ ],
+ patchNum: 3,
+ path: 'test.txt',
+ rootId: '09a9fb0a_1484e6cf',
+ start_datetime: '2018-02-13 22:47:19.000000000',
+ commentSide: 'PARENT',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2,
+ id: '8caddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-13 22:48:40.000000000',
+ message: 'Another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: '8caddf38_44770ec1',
+ start_datetime: '2018-02-13 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2,
+ id: 'scaddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-14 22:48:40.000000000',
+ message: 'Yet another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: 'scaddf38_44770ec1',
+ start_datetime: '2018-02-14 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ id: 'zcf0b9fa_fe1a5f62',
+ path: '/COMMIT_MSG',
+ line: 6,
+ updated: '2018-02-15 22:48:48.018000000',
+ message: 'resolved draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 6,
+ rootId: 'zcf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-09 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'rc1',
+ line: 5,
+ updated: '2019-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc1',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc1',
+ start_datetime: '2019-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'rc2',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc2',
+ },
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'c2_1',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc2',
+ start_datetime: '2019-03-08 18:49:18.000000000',
+ },
+ ];
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
+ // Since _endpoints are global, must reset state.
+ Gerrit._endpoints = new GrPluginEndpoints();
+ navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({test: 'config'}); },
+ getAccount() { return Promise.resolve(null); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ _fetchSharedCacheURL() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
+ Gerrit._loadPlugins([]);
+ Gerrit.install(
+ plugin => {
+ plugin.registerDynamicCustomComponent(
+ 'change-view-tab-header',
+ 'gr-checks-change-view-tab-header-view'
+ );
+ plugin.registerDynamicCustomComponent(
+ 'change-view-tab-content',
+ 'gr-checks-view'
+ );
+ },
+ '0.1',
+ 'http://some/plugins/url.html'
+ );
+ });
+
+ teardown(done => {
+ flush(() => {
+ sandbox.restore();
+ done();
+ });
+ });
+
+ const getCustomCssValue =
+ cssParam => util.getComputedStyleValue(cssParam, element);
+
+ test('_handleMessageAnchorTap', () => {
+ element._changeNum = '1';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
};
- const ROBOT_COMMENTS_LIMIT = 10;
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChange');
+ const replaceStateStub = sandbox.stub(history, 'replaceState');
+ element._handleMessageAnchorTap({detail: {id: 'a12345'}});
- const THREADS = [
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- robot_id: 'rb1',
- id: 'ecf9fa_fe1a5f62',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'ecf0b9fa_fe1a5f62',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- {
- id: '503008e2_0ab203ee',
- path: '/COMMIT_MSG',
- line: 5,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-13 22:48:48.018000000',
- message: 'draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'ecf0b9fa_fe1a5f62',
- start_datetime: '2018-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 3,
- id: 'ecf0b9fa_fe5f62',
- robot_id: 'rb2',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- {
- __path: 'test.txt',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 3,
- id: '09a9fb0a_1484e6cf',
- side: 'PARENT',
- updated: '2018-02-13 22:47:19.000000000',
- message: 'Some comment on another patchset.',
- unresolved: false,
- },
- ],
- patchNum: 3,
- path: 'test.txt',
- rootId: '09a9fb0a_1484e6cf',
- start_datetime: '2018-02-13 22:47:19.000000000',
- commentSide: 'PARENT',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: '8caddf38_44770ec1',
- line: 4,
- updated: '2018-02-13 22:48:40.000000000',
- message: 'Another unresolved comment',
- unresolved: true,
- },
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: '8caddf38_44770ec1',
- start_datetime: '2018-02-13 22:48:40.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: 'scaddf38_44770ec1',
- line: 4,
- updated: '2018-02-14 22:48:40.000000000',
- message: 'Yet another unresolved comment',
- unresolved: true,
- },
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: 'scaddf38_44770ec1',
- start_datetime: '2018-02-14 22:48:40.000000000',
- },
- {
- comments: [
- {
- id: 'zcf0b9fa_fe1a5f62',
- path: '/COMMIT_MSG',
- line: 6,
- updated: '2018-02-15 22:48:48.018000000',
- message: 'resolved draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 6,
- rootId: 'zcf0b9fa_fe1a5f62',
- start_datetime: '2018-02-09 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc1',
- line: 5,
- updated: '2019-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc1',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc1',
- start_datetime: '2019-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc2',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc2',
- },
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'c2_1',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc2',
- start_datetime: '2019-03-08 18:49:18.000000000',
- },
- ];
+ assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
+ assert.isTrue(replaceStateStub.called);
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-endpoint-decorator', {
- _import: sandbox.stub().returns(Promise.resolve()),
- });
- // Since _endpoints are global, must reset state.
- Gerrit._endpoints = new GrPluginEndpoints();
- navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({test: 'config'}); },
- getAccount() { return Promise.resolve(null); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- _fetchSharedCacheURL() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
- Gerrit._loadPlugins([]);
- Gerrit.install(
- plugin => {
- plugin.registerDynamicCustomComponent(
- 'change-view-tab-header',
- 'gr-checks-change-view-tab-header-view'
- );
- plugin.registerDynamicCustomComponent(
- 'change-view-tab-content',
- 'gr-checks-view'
- );
- },
- '0.1',
- 'http://some/plugins/url.html'
- );
+ suite('plugins adding to file tab', () => {
+ setup(done => {
+ // Resolving it here instead of during setup() as other tests depend
+ // on flush() not being called during setup.
+ flush(() => done());
});
- teardown(done => {
+ test('plugin added tab shows up as a dynamic endpoint', () => {
+ assert(element._dynamicTabHeaderEndpoints.includes(
+ 'change-view-tab-header-url'));
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ // 3 Tabs are : Files, Plugin, Findings
+ assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3);
+ assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name,
+ 'change-view-tab-header-url');
+ });
+
+ test('handleShowTab switched tab correctly', done => {
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ assert.equal(paperTabs.selected, 0);
+ element._handleShowTab({detail:
+ {tab: 'change-view-tab-header-url'}});
flush(() => {
- sandbox.restore();
+ assert.equal(paperTabs.selected, 1);
done();
});
});
- const getCustomCssValue =
- cssParam => util.getComputedStyleValue(cssParam, element);
+ test('switching tab sets _selectedTabPluginEndpoint', done => {
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]);
+ flush(() => {
+ assert.equal(element._selectedTabPluginEndpoint,
+ 'change-view-tab-content-url');
+ done();
+ });
+ });
+ });
- test('_handleMessageAnchorTap', () => {
+ suite('keyboard shortcuts', () => {
+ test('t to add topic', () => {
+ const editStub = sandbox.stub(element.$.metadata, 'editTopic');
+ MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't');
+ assert(editStub.called);
+ });
+
+ test('S should toggle the CL star', () => {
+ const starStub = sandbox.stub(element.$.changeStar, 'toggleStar');
+ MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's');
+ assert(starStub.called);
+ });
+
+ test('U should navigate to root if no backPage set', () => {
+ const relativeNavStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToRelativeUrl');
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert.isTrue(relativeNavStub.called);
+ assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
+ Gerrit.Nav.getUrlForRoot()));
+ });
+
+ test('U should navigate to backPage if set', () => {
+ const relativeNavStub = sandbox.stub(Gerrit.Nav,
+ 'navigateToRelativeUrl');
+ element.backPage = '/dashboard/self';
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert.isTrue(relativeNavStub.called);
+ assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
+ '/dashboard/self'));
+ });
+
+ test('A fires an error event when not logged in', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
+ const loggedInErrorSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ flush(() => {
+ assert.isFalse(element.$.replyOverlay.opened);
+ assert.isTrue(loggedInErrorSpy.called);
+ done();
+ });
+ });
+
+ test('shift A does not open reply overlay', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
+ flush(() => {
+ assert.isFalse(element.$.replyOverlay.opened);
+ done();
+ });
+ });
+
+ test('A toggles overlay when logged in', done => {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ element._change = {labels: {}};
+ const openSpy = sandbox.spy(element, '_openReplyDialog');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ flush(() => {
+ assert.isTrue(element.$.replyOverlay.opened);
+ element.$.replyOverlay.close();
+ assert.isFalse(element.$.replyOverlay.opened);
+ assert(openSpy.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openSpy.callCount, 1);
+ done();
+ });
+ });
+
+ test('fullscreen-overlay-opened hides content', () => {
+ element._loggedIn = true;
+ element._loading = false;
+ element._change = {
+ owner: {_account_id: 1},
+ labels: {},
+ actions: {
+ abandon: {
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon',
+ },
+ },
+ };
+ sandbox.spy(element, '_handleHideBackgroundContent');
+ element.$.replyDialog.fire('fullscreen-overlay-opened');
+ assert.isTrue(element._handleHideBackgroundContent.called);
+ assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
+ assert.equal(getComputedStyle(element.$.actions).display, 'flex');
+ });
+
+ test('fullscreen-overlay-closed shows content', () => {
+ element._loggedIn = true;
+ element._loading = false;
+ element._change = {
+ owner: {_account_id: 1},
+ labels: {},
+ actions: {
+ abandon: {
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon',
+ },
+ },
+ };
+ sandbox.spy(element, '_handleShowBackgroundContent');
+ element.$.replyDialog.fire('fullscreen-overlay-closed');
+ assert.isTrue(element._handleShowBackgroundContent.called);
+ assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
+ test('expand all messages when expand-diffs fired', () => {
+ const handleExpand =
+ sandbox.stub(element.$.fileList, 'expandAllDiffs');
+ element.$.fileListHeader.fire('expand-diffs');
+ assert.isTrue(handleExpand.called);
+ });
+
+ test('collapse all messages when collapse-diffs fired', () => {
+ const handleCollapse =
+ sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+ element.$.fileListHeader.fire('collapse-diffs');
+ assert.isTrue(handleCollapse.called);
+ });
+
+ test('X should expand all messages', done => {
+ flush(() => {
+ const handleExpand = sandbox.stub(element.messagesList,
+ 'handleExpandCollapse');
+ MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
+ assert(handleExpand.calledWith(true));
+ done();
+ });
+ });
+
+ test('Z should collapse all messages', done => {
+ flush(() => {
+ const handleExpand = sandbox.stub(element.messagesList,
+ 'handleExpandCollapse');
+ MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
+ assert(handleExpand.calledWith(false));
+ done();
+ });
+ });
+
+ test('shift + R should fetch and navigate to the latest patch set',
+ done => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ _number: 42,
+ revisions: {
+ rev1: {_number: 1, commit: {parents: []}},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ actions: {},
+ };
+
+ navigateToChangeStub.restore();
+ navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange',
+ (change, patchNum, basePatchNum) => {
+ assert.equal(change, element._change);
+ assert.isUndefined(patchNum);
+ assert.isUndefined(basePatchNum);
+ done();
+ });
+
+ MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ });
+
+ test('d should open download overlay', () => {
+ const stub = sandbox.stub(element.$.downloadOverlay, 'open');
+ MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
+ assert.isTrue(stub.called);
+ });
+
+ test(', should open diff preferences', () => {
+ const stub = sandbox.stub(
+ element.$.fileList.$.diffPreferencesDialog, 'open');
+ element._loggedIn = false;
+ element.disableDiffPrefs = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert.isFalse(stub.called);
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert.isFalse(stub.called);
+
+ element.disableDiffPrefs = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert.isTrue(stub.called);
+ });
+
+ test('m should toggle diff mode', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ const setModeStub = sandbox.stub(element.$.fileListHeader,
+ 'setDiffViewMode');
+ const e = {preventDefault: () => {}};
+ flushAsynchronousOperations();
+
+ element.viewState.diffMode = 'SIDE_BY_SIDE';
+ element._handleToggleDiffMode(e);
+ assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
+
+ element.viewState.diffMode = 'UNIFIED_DIFF';
+ element._handleToggleDiffMode(e);
+ assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
+ });
+ });
+
+ suite('reloading drafts', () => {
+ let reloadStub;
+ const drafts = {
+ 'testfile.txt': [
+ {
+ patch_set: 5,
+ id: 'dd2982f5_c01c9e6a',
+ line: 1,
+ updated: '2017-11-08 18:47:45.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ ],
+ };
+ setup(() => {
+ // Fake computeDraftCount as its required for ChangeComments,
+ // see gr-comment-api#reloadDrafts.
+ reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
+ .returns(Promise.resolve({
+ drafts,
+ getAllThreadsForChange: () => ([]),
+ computeDraftCount: () => 1,
+ }));
+ });
+
+ test('drafts are reloaded when reload-drafts fired', done => {
+ element.$.fileList.fire('reload-drafts', {
+ resolve: () => {
+ assert.isTrue(reloadStub.called);
+ assert.deepEqual(element._diffDrafts, drafts);
+ done();
+ },
+ });
+ });
+
+ test('drafts are reloaded when comment-refresh fired', () => {
+ element.fire('comment-refresh');
+ assert.isTrue(reloadStub.called);
+ });
+ });
+
+ test('diff comments modified', () => {
+ sandbox.spy(element, '_handleReloadCommentThreads');
+ return element._reloadComments().then(() => {
+ element.fire('diff-comments-modified');
+ assert.isTrue(element._handleReloadCommentThreads.called);
+ });
+ });
+
+ test('thread list modified', () => {
+ sandbox.spy(element, '_handleReloadDiffComments');
+ element._currentView = CommentTabs.COMMENT_THREADS;
+ flushAsynchronousOperations();
+
+ return element._reloadComments().then(() => {
+ element.threadList.fire('thread-list-modified');
+ assert.isTrue(element._handleReloadDiffComments.called);
+
+ let draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+ .returns(1);
+ assert.equal(element._computeTotalCommentCounts(5,
+ element._changeComments), '5 unresolved, 1 draft');
+ assert.equal(element._computeTotalCommentCounts(0,
+ element._changeComments), '1 draft');
+ draftStub.restore();
+ draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+ .returns(0);
+ assert.equal(element._computeTotalCommentCounts(0,
+ element._changeComments), '');
+ assert.equal(element._computeTotalCommentCounts(1,
+ element._changeComments), '1 unresolved');
+ draftStub.restore();
+ draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
+ .returns(2);
+ assert.equal(element._computeTotalCommentCounts(1,
+ element._changeComments), '1 unresolved, 2 drafts');
+ draftStub.restore();
+ });
+ });
+
+ suite('thread list and change log tabs', () => {
+ setup(() => {
element._changeNum = '1';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChange');
- const replaceStateStub = sandbox.stub(history, 'replaceState');
- element._handleMessageAnchorTap({detail: {id: 'a12345'}});
-
- assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
- assert.isTrue(replaceStateStub.called);
- });
-
- suite('plugins adding to file tab', () => {
- setup(done => {
- // Resolving it here instead of during setup() as other tests depend
- // on flush() not being called during setup.
- flush(() => done());
- });
-
- test('plugin added tab shows up as a dynamic endpoint', () => {
- assert(element._dynamicTabHeaderEndpoints.includes(
- 'change-view-tab-header-url'));
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- // 3 Tabs are : Files, Plugin, Findings
- assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3);
- assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name,
- 'change-view-tab-header-url');
- });
-
- test('handleShowTab switched tab correctly', done => {
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- assert.equal(paperTabs.selected, 0);
- element._handleShowTab({detail:
- {tab: 'change-view-tab-header-url'}});
- flush(() => {
- assert.equal(paperTabs.selected, 1);
- done();
- });
- });
-
- test('switching tab sets _selectedTabPluginEndpoint', done => {
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]);
- flush(() => {
- assert.equal(element._selectedTabPluginEndpoint,
- 'change-view-tab-content-url');
- done();
- });
- });
- });
-
- suite('keyboard shortcuts', () => {
- test('t to add topic', () => {
- const editStub = sandbox.stub(element.$.metadata, 'editTopic');
- MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't');
- assert(editStub.called);
- });
-
- test('S should toggle the CL star', () => {
- const starStub = sandbox.stub(element.$.changeStar, 'toggleStar');
- MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's');
- assert(starStub.called);
- });
-
- test('U should navigate to root if no backPage set', () => {
- const relativeNavStub = sandbox.stub(Gerrit.Nav,
- 'navigateToRelativeUrl');
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert.isTrue(relativeNavStub.called);
- assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
- Gerrit.Nav.getUrlForRoot()));
- });
-
- test('U should navigate to backPage if set', () => {
- const relativeNavStub = sandbox.stub(Gerrit.Nav,
- 'navigateToRelativeUrl');
- element.backPage = '/dashboard/self';
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert.isTrue(relativeNavStub.called);
- assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
- '/dashboard/self'));
- });
-
- test('A fires an error event when not logged in', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
- const loggedInErrorSpy = sandbox.spy();
- element.addEventListener('show-auth-required', loggedInErrorSpy);
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- flush(() => {
- assert.isFalse(element.$.replyOverlay.opened);
- assert.isTrue(loggedInErrorSpy.called);
- done();
- });
- });
-
- test('shift A does not open reply overlay', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
- flush(() => {
- assert.isFalse(element.$.replyOverlay.opened);
- done();
- });
- });
-
- test('A toggles overlay when logged in', done => {
- sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
- element._change = {labels: {}};
- const openSpy = sandbox.spy(element, '_openReplyDialog');
-
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- flush(() => {
- assert.isTrue(element.$.replyOverlay.opened);
- element.$.replyOverlay.close();
- assert.isFalse(element.$.replyOverlay.opened);
- assert(openSpy.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY),
- '_openReplyDialog should have been passed ANY');
- assert.equal(openSpy.callCount, 1);
- done();
- });
- });
-
- test('fullscreen-overlay-opened hides content', () => {
- element._loggedIn = true;
- element._loading = false;
- element._change = {
- owner: {_account_id: 1},
- labels: {},
- actions: {
- abandon: {
- enabled: true,
- label: 'Abandon',
- method: 'POST',
- title: 'Abandon',
- },
- },
- };
- sandbox.spy(element, '_handleHideBackgroundContent');
- element.$.replyDialog.fire('fullscreen-overlay-opened');
- assert.isTrue(element._handleHideBackgroundContent.called);
- assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
- assert.equal(getComputedStyle(element.$.actions).display, 'flex');
- });
-
- test('fullscreen-overlay-closed shows content', () => {
- element._loggedIn = true;
- element._loading = false;
- element._change = {
- owner: {_account_id: 1},
- labels: {},
- actions: {
- abandon: {
- enabled: true,
- label: 'Abandon',
- method: 'POST',
- title: 'Abandon',
- },
- },
- };
- sandbox.spy(element, '_handleShowBackgroundContent');
- element.$.replyDialog.fire('fullscreen-overlay-closed');
- assert.isTrue(element._handleShowBackgroundContent.called);
- assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
- });
-
- test('expand all messages when expand-diffs fired', () => {
- const handleExpand =
- sandbox.stub(element.$.fileList, 'expandAllDiffs');
- element.$.fileListHeader.fire('expand-diffs');
- assert.isTrue(handleExpand.called);
- });
-
- test('collapse all messages when collapse-diffs fired', () => {
- const handleCollapse =
- sandbox.stub(element.$.fileList, 'collapseAllDiffs');
- element.$.fileListHeader.fire('collapse-diffs');
- assert.isTrue(handleCollapse.called);
- });
-
- test('X should expand all messages', done => {
- flush(() => {
- const handleExpand = sandbox.stub(element.messagesList,
- 'handleExpandCollapse');
- MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
- assert(handleExpand.calledWith(true));
- done();
- });
- });
-
- test('Z should collapse all messages', done => {
- flush(() => {
- const handleExpand = sandbox.stub(element.messagesList,
- 'handleExpandCollapse');
- MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
- assert(handleExpand.calledWith(false));
- done();
- });
- });
-
- test('shift + R should fetch and navigate to the latest patch set',
- done => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- _number: 42,
- revisions: {
- rev1: {_number: 1, commit: {parents: []}},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- actions: {},
- };
-
- navigateToChangeStub.restore();
- navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange',
- (change, patchNum, basePatchNum) => {
- assert.equal(change, element._change);
- assert.isUndefined(patchNum);
- assert.isUndefined(basePatchNum);
- done();
- });
-
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
- });
-
- test('d should open download overlay', () => {
- const stub = sandbox.stub(element.$.downloadOverlay, 'open');
- MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
- assert.isTrue(stub.called);
- });
-
- test(', should open diff preferences', () => {
- const stub = sandbox.stub(
- element.$.fileList.$.diffPreferencesDialog, 'open');
- element._loggedIn = false;
- element.disableDiffPrefs = true;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert.isFalse(stub.called);
-
- element._loggedIn = true;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert.isFalse(stub.called);
-
- element.disableDiffPrefs = false;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert.isTrue(stub.called);
- });
-
- test('m should toggle diff mode', () => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- const setModeStub = sandbox.stub(element.$.fileListHeader,
- 'setDiffViewMode');
- const e = {preventDefault: () => {}};
- flushAsynchronousOperations();
-
- element.viewState.diffMode = 'SIDE_BY_SIDE';
- element._handleToggleDiffMode(e);
- assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
-
- element.viewState.diffMode = 'UNIFIED_DIFF';
- element._handleToggleDiffMode(e);
- assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
- });
- });
-
- suite('reloading drafts', () => {
- let reloadStub;
- const drafts = {
- 'testfile.txt': [
- {
- patch_set: 5,
- id: 'dd2982f5_c01c9e6a',
- line: 1,
- updated: '2017-11-08 18:47:45.000000000',
- message: 'test',
- unresolved: true,
- },
- ],
- };
- setup(() => {
- // Fake computeDraftCount as its required for ChangeComments,
- // see gr-comment-api#reloadDrafts.
- reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
- .returns(Promise.resolve({
- drafts,
- getAllThreadsForChange: () => ([]),
- computeDraftCount: () => 1,
- }));
- });
-
- test('drafts are reloaded when reload-drafts fired', done => {
- element.$.fileList.fire('reload-drafts', {
- resolve: () => {
- assert.isTrue(reloadStub.called);
- assert.deepEqual(element._diffDrafts, drafts);
- done();
- },
- });
- });
-
- test('drafts are reloaded when comment-refresh fired', () => {
- element.fire('comment-refresh');
- assert.isTrue(reloadStub.called);
- });
- });
-
- test('diff comments modified', () => {
- sandbox.spy(element, '_handleReloadCommentThreads');
- return element._reloadComments().then(() => {
- element.fire('diff-comments-modified');
- assert.isTrue(element._handleReloadCommentThreads.called);
- });
- });
-
- test('thread list modified', () => {
- sandbox.spy(element, '_handleReloadDiffComments');
- element._currentView = CommentTabs.COMMENT_THREADS;
- flushAsynchronousOperations();
-
- return element._reloadComments().then(() => {
- element.threadList.fire('thread-list-modified');
- assert.isTrue(element._handleReloadDiffComments.called);
-
- let draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
- .returns(1);
- assert.equal(element._computeTotalCommentCounts(5,
- element._changeComments), '5 unresolved, 1 draft');
- assert.equal(element._computeTotalCommentCounts(0,
- element._changeComments), '1 draft');
- draftStub.restore();
- draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
- .returns(0);
- assert.equal(element._computeTotalCommentCounts(0,
- element._changeComments), '');
- assert.equal(element._computeTotalCommentCounts(1,
- element._changeComments), '1 unresolved');
- draftStub.restore();
- draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
- .returns(2);
- assert.equal(element._computeTotalCommentCounts(1,
- element._changeComments), '1 unresolved, 2 drafts');
- draftStub.restore();
- });
- });
-
- suite('thread list and change log tabs', () => {
- setup(() => {
- element._changeNum = '1';
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev2: {_number: 2, commit: {parents: []}},
- rev1: {_number: 1, commit: {parents: []}},
- rev13: {_number: 13, commit: {parents: []}},
- rev3: {_number: 3, commit: {parents: []}},
- },
- current_revision: 'rev3',
- status: 'NEW',
- labels: {
- test: {
- all: [],
- default_value: 0,
- values: [],
- approved: {},
- },
- },
- };
- sandbox.stub(element.$.relatedChanges, 'reload');
- sandbox.stub(element, '_reload').returns(Promise.resolve());
- sandbox.spy(element, '_paramsChanged');
- element.params = {view: 'change', changeNum: '1'};
- });
-
- test('tab switch works correctly', done => {
- assert.isTrue(element._paramsChanged.called);
- assert.equal(element.$.commentTabs.selected, CommentTabs.CHANGE_LOG);
- assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
-
- const commentTab = element.shadowRoot.querySelector(
- 'paper-tab.commentThreads'
- );
- // Switch to comment thread tab
- MockInteractions.tap(commentTab);
- const commentTabs = element.$.commentTabs;
- assert.equal(commentTabs.selected,
- CommentTabs.COMMENT_THREADS);
- assert.equal(element._currentView, CommentTabs.COMMENT_THREADS);
-
- // Switch back to 'Change Log' tab
- element._paramsChanged(element.params);
- flush(() => {
- assert.equal(commentTabs.selected,
- CommentTabs.CHANGE_LOG);
- assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
- done();
- });
- });
- });
-
- suite('Findings comment tab', () => {
- setup(done => {
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev2: {_number: 2, commit: {parents: []}},
- rev1: {_number: 1, commit: {parents: []}},
- rev13: {_number: 13, commit: {parents: []}},
- rev3: {_number: 3, commit: {parents: []}},
- rev4: {_number: 4, commit: {parents: []}},
- },
- current_revision: 'rev4',
- };
- element._commentThreads = THREADS;
- const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
- MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
- flush(() => {
- done();
- });
- });
-
- test('robot comments count per patchset', () => {
- const count = element._robotCommentCountPerPatchSet(THREADS);
- const expectedCount = {
- 2: 1,
- 3: 1,
- 4: 2,
- };
- assert.deepEqual(count, expectedCount);
- assert.equal(element._computeText({_number: 2}, THREADS),
- 'Patchset 2 (1 finding)');
- assert.equal(element._computeText({_number: 4}, THREADS),
- 'Patchset 4 (2 findings)');
- assert.equal(element._computeText({_number: 5}, THREADS),
- 'Patchset 5');
- });
-
- test('only robot comments are rendered', () => {
- assert.equal(element._robotCommentThreads.length, 2);
- assert.equal(element._robotCommentThreads[0].comments[0].robot_id,
- 'rc1');
- assert.equal(element._robotCommentThreads[1].comments[0].robot_id,
- 'rc2');
- });
-
- test('changing patchsets resets robot comments', done => {
- element.set('_change.current_revision', 'rev3');
- flush(() => {
- assert.equal(element._robotCommentThreads.length, 1);
- done();
- });
- });
-
- test('Show more button is hidden', () => {
- assert.isNull(element.shadowRoot.querySelector('.show-robot-comments'));
- });
-
- suite('robot comments show more button', () => {
- setup(done => {
- const arr = [];
- for (let i = 0; i <= 30; i++) {
- arr.push(...THREADS);
- }
- element._commentThreads = arr;
- flush(() => {
- done();
- });
- });
-
- test('Show more button is rendered', () => {
- assert.isOk(element.shadowRoot.querySelector('.show-robot-comments'));
- assert.equal(element._robotCommentThreads.length,
- ROBOT_COMMENTS_LIMIT);
- });
-
- test('Clicking show more button renders all comments', done => {
- MockInteractions.tap(element.shadowRoot.querySelector(
- '.show-robot-comments'));
- flush(() => {
- assert.equal(element._robotCommentThreads.length, 62);
- done();
- });
- });
- });
- });
-
- test('reply button is not visible when logged out', () => {
- assert.equal(getComputedStyle(element.$.replyBtn).display, 'none');
- element._loggedIn = true;
- assert.notEqual(getComputedStyle(element.$.replyBtn).display, 'none');
- });
-
- test('download tap calls _handleOpenDownloadDialog', () => {
- sandbox.stub(element, '_handleOpenDownloadDialog');
- element.$.actions.fire('download-tap');
- assert.isTrue(element._handleOpenDownloadDialog.called);
- });
-
- test('fetches the server config on attached', done => {
- flush(() => {
- assert.equal(element._serverConfig.test, 'config');
- done();
- });
- });
-
- test('_changeStatuses', () => {
- sandbox.stub(element, 'changeStatuses').returns(
- ['Merged', 'WIP']);
- element._loading = false;
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
- rev2: {_number: 2},
- rev1: {_number: 1},
- rev13: {_number: 13},
- rev3: {_number: 3},
- },
- current_revision: 'rev3',
- labels: {
- test: {
- all: [],
- default_value: 0,
- values: [],
- approved: {},
- },
- },
- };
- element._mergeable = true;
- const expectedStatuses = ['Merged', 'WIP'];
- assert.deepEqual(element._changeStatuses, expectedStatuses);
- assert.equal(element._changeStatus, expectedStatuses.join(', '));
- flushAsynchronousOperations();
- const statusChips = Polymer.dom(element.root)
- .querySelectorAll('gr-change-status');
- assert.equal(statusChips.length, 2);
- });
-
- test('diff preferences open when open-diff-prefs is fired', () => {
- const overlayOpenStub = sandbox.stub(element.$.fileList,
- 'openDiffPrefs');
- element.$.fileListHeader.fire('open-diff-prefs');
- assert.isTrue(overlayOpenStub.called);
- });
-
- test('_prepareCommitMsgForLinkify', () => {
- let commitMessage = 'R=test@google.com';
- let result = element._prepareCommitMsgForLinkify(commitMessage);
- assert.equal(result, 'R=\u200Btest@google.com');
-
- commitMessage = 'R=test@google.com\nR=test@google.com';
- result = element._prepareCommitMsgForLinkify(commitMessage);
- assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com');
-
- commitMessage = 'CC=test@google.com';
- result = element._prepareCommitMsgForLinkify(commitMessage);
- assert.equal(result, 'CC=\u200Btest@google.com');
- }),
-
- test('_isSubmitEnabled', () => {
- assert.isFalse(element._isSubmitEnabled({}));
- assert.isFalse(element._isSubmitEnabled({submit: {}}));
- assert.isTrue(element._isSubmitEnabled(
- {submit: {enabled: true}}));
- });
-
- test('_reload is called when an approved label is removed', () => {
- const vote = {_account_id: 1, name: 'bojack', value: 1};
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- owner: {email: 'abc@def'},
- revisions: {
rev2: {_number: 2, commit: {parents: []}},
rev1: {_number: 1, commit: {parents: []}},
rev13: {_number: 13, commit: {parents: []}},
@@ -934,1312 +720,1536 @@
status: 'NEW',
labels: {
test: {
- all: [vote],
+ all: [],
default_value: 0,
values: [],
approved: {},
},
},
};
- flushAsynchronousOperations();
- const reloadStub = sandbox.stub(element, '_reload');
- element.splice('_change.labels.test.all', 0, 1);
- assert.isFalse(reloadStub.called);
- element._change.labels.test.all.push(vote);
- element._change.labels.test.all.push(vote);
- element._change.labels.test.approved = vote;
- flushAsynchronousOperations();
- element.splice('_change.labels.test.all', 0, 2);
- assert.isTrue(reloadStub.called);
- assert.isTrue(reloadStub.calledOnce);
- });
-
- test('reply button has updated count when there are drafts', () => {
- const getLabel = element._computeReplyButtonLabel;
-
- assert.equal(getLabel(null, false), 'Reply');
- assert.equal(getLabel(null, true), 'Start review');
-
- const changeRecord = {base: null};
- assert.equal(getLabel(changeRecord, false), 'Reply');
-
- changeRecord.base = {};
- assert.equal(getLabel(changeRecord, false), 'Reply');
-
- changeRecord.base = {
- 'file1.txt': [{}],
- 'file2.txt': [{}, {}],
- };
- assert.equal(getLabel(changeRecord, false), 'Reply (3)');
- });
-
- test('start review button when owner of WIP change', () => {
- assert.equal(
- element._computeReplyButtonLabel(null, true),
- 'Start review');
- });
-
- test('comment events properly update diff drafts', () => {
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
- const draft = {
- __draft: true,
- id: 'id1',
- path: '/foo/bar.txt',
- text: 'hello',
- };
- element._handleCommentSave({detail: {comment: draft}});
- draft.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
- draft.patch_set = null;
- draft.text = 'hello, there';
- element._handleCommentSave({detail: {comment: draft}});
- draft.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
- const draft2 = {
- __draft: true,
- id: 'id2',
- path: '/foo/bar.txt',
- text: 'hola',
- };
- element._handleCommentSave({detail: {comment: draft2}});
- draft2.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]});
- draft.patch_set = null;
- element._handleCommentDiscard({detail: {comment: draft}});
- draft.patch_set = 2;
- assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]});
- element._handleCommentDiscard({detail: {comment: draft2}});
- assert.deepEqual(element._diffDrafts, {});
- });
-
- test('change num change', () => {
- element._changeNum = null;
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- labels: {},
- };
- element.viewState.changeNum = null;
- element.viewState.diffMode = 'UNIFIED';
- assert.equal(element.viewState.numFilesShown, 200);
- assert.equal(element._numFilesShown, 200);
- element._numFilesShown = 150;
- flushAsynchronousOperations();
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- assert.equal(element.viewState.numFilesShown, 150);
-
- element._changeNum = '1';
- element.params = {changeNum: '1'};
- element._change.newProp = '1';
- flushAsynchronousOperations();
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- assert.equal(element.viewState.changeNum, '1');
-
- element._changeNum = '2';
- element.params = {changeNum: '2'};
- element._change.newProp = '2';
- flushAsynchronousOperations();
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- assert.equal(element.viewState.changeNum, '2');
- assert.equal(element.viewState.numFilesShown, 200);
- assert.equal(element._numFilesShown, 200);
- });
-
- test('_setDiffViewMode is called with reset when new change is loaded',
- () => {
- sandbox.stub(element, '_setDiffViewMode');
- element.viewState = {changeNum: 1};
- element._changeNum = 2;
- element._resetFileListViewState();
- assert.isTrue(
- element._setDiffViewMode.lastCall.calledWithExactly(true));
- });
-
- test('diffViewMode is propagated from file list header', () => {
- element.viewState = {diffMode: 'UNIFIED'};
- element.$.fileListHeader.diffViewMode = 'SIDE_BY_SIDE';
- assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
- });
-
- test('diffMode defaults to side by side without preferences', done => {
- sandbox.stub(element.$.restAPI, 'getPreferences').returns(
- Promise.resolve({}));
- // No user prefs or diff view mode set.
-
- element._setDiffViewMode().then(() => {
- assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('diffMode defaults to preference when not already set', done => {
- sandbox.stub(element.$.restAPI, 'getPreferences').returns(
- Promise.resolve({default_diff_view: 'UNIFIED'}));
-
- element._setDiffViewMode().then(() => {
- assert.equal(element.viewState.diffMode, 'UNIFIED');
- done();
- });
- });
-
- test('existing diffMode overrides preference', done => {
- element.viewState.diffMode = 'SIDE_BY_SIDE';
- sandbox.stub(element.$.restAPI, 'getPreferences').returns(
- Promise.resolve({default_diff_view: 'UNIFIED'}));
- element._setDiffViewMode().then(() => {
- assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('don’t reload entire page when patchRange changes', () => {
- const reloadStub = sandbox.stub(element, '_reload',
- () => Promise.resolve());
- const reloadPatchDependentStub = sandbox.stub(element,
- '_reloadPatchNumDependentResources',
- () => Promise.resolve());
- const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
- const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
-
- const value = {
- view: Gerrit.Nav.View.CHANGE,
- patchNum: '1',
- };
- element._paramsChanged(value);
- assert.isTrue(reloadStub.calledOnce);
- assert.isTrue(relatedClearSpy.calledOnce);
-
- element._initialLoadComplete = true;
-
- value.basePatchNum = '1';
- value.patchNum = '2';
- element._paramsChanged(value);
- assert.isFalse(reloadStub.calledTwice);
- assert.isTrue(reloadPatchDependentStub.calledOnce);
- assert.isTrue(relatedClearSpy.calledOnce);
- assert.isTrue(collapseStub.calledTwice);
- });
-
- test('reload entire page when patchRange doesnt change', () => {
- const reloadStub = sandbox.stub(element, '_reload',
- () => Promise.resolve());
- const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
- const value = {
- view: Gerrit.Nav.View.CHANGE,
- };
- element._paramsChanged(value);
- assert.isTrue(reloadStub.calledOnce);
- element._initialLoadComplete = true;
- element._paramsChanged(value);
- assert.isTrue(reloadStub.calledTwice);
- assert.isTrue(collapseStub.calledTwice);
- });
-
- test('related changes are updated and new patch selected after rebase',
- done => {
- element._changeNum = '42';
- sandbox.stub(element, 'computeLatestPatchNum', () => 1);
- sandbox.stub(element, '_reload',
- () => Promise.resolve());
- const e = {detail: {action: 'rebase'}};
- element._handleReloadChange(e).then(() => {
- assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
- element._change));
- done();
- });
- });
-
- test('related changes are not updated after other action', done => {
- sandbox.stub(element, '_reload', () => Promise.resolve());
- sandbox.stub(element.$.relatedChanges, 'reload');
- const e = {detail: {action: 'abandon'}};
- element._handleReloadChange(e).then(() => {
- assert.isFalse(navigateToChangeStub.called);
- done();
- });
- });
-
- test('_computeMergedCommitInfo', () => {
- const dummyRevs = {
- 1: {commit: {commit: 1}},
- 2: {commit: {}},
- };
- assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {});
- assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs),
- dummyRevs[1].commit);
-
- // Regression test for issue 5337.
- const commit = element._computeMergedCommitInfo(2, dummyRevs);
- assert.notDeepEqual(commit, dummyRevs[2]);
- assert.deepEqual(commit, {commit: 2});
- });
-
- test('_computeCopyTextForTitle', () => {
- const change = {
- _number: 123,
- subject: 'test subject',
- revisions: {
- rev1: {_number: 1},
- rev3: {_number: 3},
- },
- current_revision: 'rev3',
- };
- sandbox.stub(Gerrit.Nav, 'getUrlForChange')
- .returns('/change/123');
- assert.equal(
- element._computeCopyTextForTitle(change),
- '123: test subject | https://localhost:8081/change/123'
- );
- });
-
- test('get latest revision', () => {
- let change = {
- revisions: {
- rev1: {_number: 1},
- rev3: {_number: 3},
- },
- current_revision: 'rev3',
- };
- assert.equal(element._getLatestRevisionSHA(change), 'rev3');
- change = {
- revisions: {
- rev1: {_number: 1},
- },
- };
- assert.equal(element._getLatestRevisionSHA(change), 'rev1');
- });
-
- test('show commit message edit button', () => {
- const _change = {
- status: element.ChangeStatus.MERGED,
- };
- assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
- assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
- assert.isTrue(element._computeHideEditCommitMessage(false, true, {}));
- assert.isFalse(element._computeHideEditCommitMessage(true, false, {}));
- assert.isTrue(element._computeHideEditCommitMessage(true, false,
- _change));
- assert.isTrue(element._computeHideEditCommitMessage(true, false, {},
- true));
- assert.isFalse(element._computeHideEditCommitMessage(true, false, {},
- false));
- });
-
- test('_handleCommitMessageSave trims trailing whitespace', () => {
- const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
- .returns(Promise.resolve({}));
-
- const mockEvent = content => { return {detail: {content}}; };
-
- element._handleCommitMessageSave(mockEvent('test \n test '));
- assert.equal(putStub.lastCall.args[1], 'test\n test');
-
- element._handleCommitMessageSave(mockEvent(' test\ntest'));
- assert.equal(putStub.lastCall.args[1], ' test\ntest');
-
- element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
- assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
- });
-
- test('_computeChangeIdCommitMessageError', () => {
- let commitMessage =
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
- let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- null);
-
- change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch');
-
- commitMessage = 'This is the greatest change.';
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'missing');
- });
-
- test('multiple change Ids in commit message picks last', () => {
- const commitMessage = [
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
- ].join('\n');
- let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- null);
- change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch');
- });
-
- test('does not count change Id that starts mid line', () => {
- const commitMessage = [
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
- 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
- ].join(' and ');
- let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- null);
- change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
- assert.equal(
- element._computeChangeIdCommitMessageError(commitMessage, change),
- 'mismatch');
- });
-
- test('_computeTitleAttributeWarning', () => {
- let changeIdCommitMessageError = 'missing';
- assert.equal(
- element._computeTitleAttributeWarning(changeIdCommitMessageError),
- 'No Change-Id in commit message');
-
- changeIdCommitMessageError = 'mismatch';
- assert.equal(
- element._computeTitleAttributeWarning(changeIdCommitMessageError),
- 'Change-Id mismatch');
- });
-
- test('_computeChangeIdClass', () => {
- let changeIdCommitMessageError = 'missing';
- assert.equal(
- element._computeChangeIdClass(changeIdCommitMessageError), '');
-
- changeIdCommitMessageError = 'mismatch';
- assert.equal(
- element._computeChangeIdClass(changeIdCommitMessageError), 'warning');
- });
-
- test('topic is coalesced to null', done => {
- sandbox.stub(element, '_changeChanged');
- sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
- id: '123456789',
- labels: {},
- current_revision: 'foo',
- revisions: {foo: {commit: {}}},
- }));
-
- element._getChangeDetail().then(() => {
- assert.isNull(element._change.topic);
- done();
- });
- });
-
- test('commit sha is populated from getChangeDetail', done => {
- sandbox.stub(element, '_changeChanged');
- sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
- id: '123456789',
- labels: {},
- current_revision: 'foo',
- revisions: {foo: {commit: {}}},
- }));
-
- element._getChangeDetail().then(() => {
- assert.equal('foo', element._commitInfo.commit);
- done();
- });
- });
-
- test('edit is added to change', () => {
- sandbox.stub(element, '_changeChanged');
- sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
- id: '123456789',
- labels: {},
- current_revision: 'foo',
- revisions: {foo: {commit: {}}},
- }));
- sandbox.stub(element, '_getEdit', () => Promise.resolve({
- base_patch_set_number: 1,
- commit: {commit: 'bar'},
- }));
- element._patchRange = {};
-
- return element._getChangeDetail().then(() => {
- const revs = element._change.revisions;
- assert.equal(Object.keys(revs).length, 2);
- assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}});
- assert.deepEqual(revs['bar'], {
- _number: element.EDIT_NAME,
- basePatchNum: 1,
- commit: {commit: 'bar'},
- fetch: undefined,
- });
- });
- });
-
- test('_getBasePatchNum', () => {
- const _change = {
- _number: 42,
- revisions: {
- '98da160735fb81604b4c40e93c368f380539dd0e': {
- _number: 1,
- commit: {
- parents: [],
- },
- },
- },
- };
- const _patchRange = {
- basePatchNum: 'PARENT',
- };
- assert.equal(element._getBasePatchNum(_change, _patchRange), 'PARENT');
-
- element._prefs = {
- default_base_for_merges: 'FIRST_PARENT',
- };
-
- const _change2 = {
- _number: 42,
- revisions: {
- '98da160735fb81604b4c40e93c368f380539dd0e': {
- _number: 1,
- commit: {
- parents: [
- {
- commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8',
- subject: 'test',
- },
- {
- commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841',
- subject: 'test3',
- },
- ],
- },
- },
- },
- };
- assert.equal(element._getBasePatchNum(_change2, _patchRange), -1);
-
- _patchRange.patchNum = 1;
- assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT');
- });
-
- test('_openReplyDialog called with `ANY` when coming from tap event',
- () => {
- const openStub = sandbox.stub(element, '_openReplyDialog');
- element._serverConfig = {};
- MockInteractions.tap(element.$.replyBtn);
- assert(openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY),
- '_openReplyDialog should have been passed ANY');
- assert.equal(openStub.callCount, 1);
- });
-
- test('_openReplyDialog called with `BODY` when coming from message reply' +
- 'event', done => {
- flush(() => {
- const openStub = sandbox.stub(element, '_openReplyDialog');
- element.messagesList.fire('reply',
- {message: {message: 'text'}});
- assert(openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.BODY),
- '_openReplyDialog should have been passed BODY');
- assert.equal(openStub.callCount, 1);
- done();
- });
- });
-
- test('reply dialog focus can be controlled', () => {
- const FocusTarget = element.$.replyDialog.FocusTarget;
- const openStub = sandbox.stub(element, '_openReplyDialog');
-
- const e = {detail: {}};
- element._handleShowReplyDialog(e);
- assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
- '_openReplyDialog should have been passed REVIEWERS');
- assert.equal(openStub.callCount, 1);
-
- e.detail.value = {ccsOnly: true};
- element._handleShowReplyDialog(e);
- assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
- '_openReplyDialog should have been passed CCS');
- assert.equal(openStub.callCount, 2);
- });
-
- test('getUrlParameter functionality', () => {
- const locationStub = sandbox.stub(element, '_getLocationSearch');
-
- locationStub.returns('?test');
- assert.equal(element._getUrlParameter('test'), 'test');
- locationStub.returns('?test2=12&test=3');
- assert.equal(element._getUrlParameter('test'), 'test');
- locationStub.returns('');
- assert.isNull(element._getUrlParameter('test'));
- locationStub.returns('?');
- assert.isNull(element._getUrlParameter('test'));
- locationStub.returns('?test2');
- assert.isNull(element._getUrlParameter('test'));
- });
-
- test('revert dialog opened with revert param', done => {
- sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true));
- sandbox.stub(Gerrit, 'awaitPluginsLoaded', () => Promise.resolve());
-
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
- element._change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1, commit: {parents: []}},
- rev2: {_number: 2, commit: {parents: []}},
- },
- current_revision: 'rev1',
- status: element.ChangeStatus.MERGED,
- labels: {},
- actions: {},
- };
-
- sandbox.stub(element, '_getUrlParameter',
- param => {
- assert.equal(param, 'revert');
- return param;
- });
-
- sandbox.stub(element.$.actions, 'showRevertDialog',
- done);
-
- element._maybeShowRevertDialog();
- assert.isTrue(Gerrit.awaitPluginsLoaded.called);
- });
-
- suite('scroll related tests', () => {
- test('document scrolling calls function to set scroll height', done => {
- const originalHeight = document.body.scrollHeight;
- const scrollStub = sandbox.stub(element, '_handleScroll',
- () => {
- assert.isTrue(scrollStub.called);
- document.body.style.height = originalHeight + 'px';
- scrollStub.restore();
- done();
- });
- document.body.style.height = '10000px';
- element._handleScroll();
- });
-
- test('scrollTop is set correctly', () => {
- element.viewState = {scrollTop: TEST_SCROLL_TOP_PX};
-
- sandbox.stub(element, '_reload', () => {
- // When element is reloaded, ensure that the history
- // state has the scrollTop set earlier. This will then
- // be reset.
- assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX);
- return Promise.resolve({});
- });
-
- // simulate reloading component, which is done when route
- // changes to match a regex of change view type.
- element._paramsChanged({view: Gerrit.Nav.View.CHANGE});
- });
-
- test('scrollTop is reset when new change is loaded', () => {
- element._resetFileListViewState();
- assert.equal(element.viewState.scrollTop, 0);
- });
- });
-
- suite('reply dialog tests', () => {
- setup(() => {
- sandbox.stub(element.$.replyDialog, '_draftChanged');
- sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
- element._change = {labels: {}};
- });
-
- test('reply from comment adds quote text', () => {
- const e = {detail: {message: {message: 'quote text'}}};
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from comment replaces quote text', () => {
- element.$.replyDialog.draft = '> old quote text\n\n some draft text';
- element.$.replyDialog.quote = '> old quote text\n\n';
- const e = {detail: {message: {message: 'quote text'}}};
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from same comment preserves quote text', () => {
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = {detail: {message: {message: 'quote text'}}};
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.draft,
- '> quote text\n\n some draft text');
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from top of page contains previous draft', () => {
- const div = document.createElement('div');
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = {target: div, preventDefault: sandbox.spy()};
- element._handleReplyTap(e);
- assert.equal(element.$.replyDialog.draft,
- '> quote text\n\n some draft text');
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
- });
-
- test('reply button is disabled until server config is loaded', () => {
- assert.isTrue(element._replyDisabled);
- element._serverConfig = {};
- assert.isFalse(element._replyDisabled);
- });
-
- suite('commit message expand/collapse', () => {
- setup(() => {
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: false}));
- });
-
- test('commitCollapseToggle hidden for short commit message', () => {
- element._latestCommitMessage = '';
- assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden'));
- });
-
- test('commitCollapseToggle shown for long commit message', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden'));
- });
-
- test('commitCollapseToggle functions', () => {
- element._latestCommitMessage = _.times(35, String).join('\n');
- assert.isTrue(element._commitCollapsed);
- assert.isTrue(element._commitCollapsible);
- assert.isTrue(
- element.$.commitMessageEditor.hasAttribute('collapsed'));
- MockInteractions.tap(element.$.commitCollapseToggleButton);
- assert.isFalse(element._commitCollapsed);
- assert.isTrue(element._commitCollapsible);
- assert.isFalse(
- element.$.commitMessageEditor.hasAttribute('collapsed'));
- });
- });
-
- suite('related changes expand/collapse', () => {
- let updateHeightSpy;
- setup(() => {
- updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight');
- });
-
- test('relatedChangesToggle shown height greater than changeInfo height',
- () => {
- assert.isFalse(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getScrollHeight', () => 60);
- sandbox.stub(element, '_getLineHeight', () => 5);
- sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
- element.$.relatedChanges.dispatchEvent(
- new CustomEvent('new-section-loaded'));
- assert.isTrue(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- assert.equal(updateHeightSpy.callCount, 1);
- });
-
- test('relatedChangesToggle hidden height less than changeInfo height',
- () => {
- assert.isFalse(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getScrollHeight', () => 40);
- sandbox.stub(element, '_getLineHeight', () => 5);
- sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
- element.$.relatedChanges.dispatchEvent(
- new CustomEvent('new-section-loaded'));
- assert.isFalse(element.$.relatedChangesToggle.classList
- .contains('showToggle'));
- assert.equal(updateHeightSpy.callCount, 1);
- });
-
- test('relatedChangesToggle functions', () => {
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
- element._relatedChangesLoading = false;
- assert.isTrue(element._relatedChangesCollapsed);
- assert.isTrue(
- element.$.relatedChanges.classList.contains('collapsed'));
- MockInteractions.tap(element.$.relatedChangesToggleButton);
- assert.isFalse(element._relatedChangesCollapsed);
- assert.isFalse(
- element.$.relatedChanges.classList.contains('collapsed'));
- });
-
- test('_updateRelatedChangeMaxHeight without commit toggle', () => {
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
-
- // 50 (existing height) - 30 (extra height) = 20 (adjusted height).
- // 20 (max existing height) % 12 (line height) = 6 (remainder).
- // 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
-
- element._updateRelatedChangeMaxHeight();
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '12px');
- assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
- '');
- });
-
- test('_updateRelatedChangeMaxHeight with commit toggle', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
-
- // 50 (existing height) % 12 (line height) = 2 (remainder).
- // 50 (existing height) - 2 (remainder) = 48 (max height to set).
-
- element._updateRelatedChangeMaxHeight();
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '48px');
- assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
- '2px');
- });
-
- test('_updateRelatedChangeMaxHeight in small screen mode', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
-
- element._updateRelatedChangeMaxHeight();
-
- // 400 (new height) % 12 (line height) = 4 (remainder).
- // 400 (new height) - 4 (remainder) = 396.
-
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '396px');
- });
-
- test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
- element._latestCommitMessage = _.times(31, String).join('\n');
- sandbox.stub(element, '_getOffsetHeight', () => 50);
- sandbox.stub(element, '_getLineHeight', () => 12);
- sandbox.stub(window, 'matchMedia', () => {
- if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') {
- return {matches: true};
- } else {
- return {matches: false};
- }
- });
-
- // 100 (new height) % 12 (line height) = 4 (remainder).
- // 100 (new height) - 4 (remainder) = 96.
- element._updateRelatedChangeMaxHeight();
- assert.equal(getCustomCssValue('--relation-chain-max-height'),
- '96px');
- });
-
- suite('update checks', () => {
- setup(() => {
- sandbox.spy(element, '_startUpdateCheckTimer');
- sandbox.stub(element, 'async', f => {
- // Only fire the async callback one time.
- if (element.async.callCount > 1) { return; }
- f.call(element);
- });
- });
-
- test('_startUpdateCheckTimer negative delay', () => {
- sandbox.stub(element, 'fetchChangeUpdates');
-
- element._serverConfig = {change: {update_delay: -1}};
-
- assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isFalse(element.fetchChangeUpdates.called);
- });
-
- test('_startUpdateCheckTimer up-to-date', () => {
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: true}));
-
- element._serverConfig = {change: {update_delay: 12345}};
-
- assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isTrue(element.fetchChangeUpdates.called);
- assert.equal(element.async.lastCall.args[1], 12345 * 1000);
- });
-
- test('_startUpdateCheckTimer out-of-date shows an alert', done => {
- sandbox.stub(element, 'fetchChangeUpdates',
- () => Promise.resolve({isLatest: false}));
- element.addEventListener('show-alert', e => {
- assert.equal(e.detail.message,
- 'A newer patch set has been uploaded');
- done();
- });
- element._serverConfig = {change: {update_delay: 12345}};
- });
-
- test('_startUpdateCheckTimer new status shows an alert', done => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({
- isLatest: true,
- newStatus: element.ChangeStatus.MERGED,
- }));
- element.addEventListener('show-alert', e => {
- assert.equal(e.detail.message, 'This change has been merged');
- done();
- });
- element._serverConfig = {change: {update_delay: 12345}};
- });
-
- test('_startUpdateCheckTimer new messages shows an alert', done => {
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({
- isLatest: true,
- newMessages: true,
- }));
- element.addEventListener('show-alert', e => {
- assert.equal(e.detail.message,
- 'There are new messages on this change');
- done();
- });
- element._serverConfig = {change: {update_delay: 12345}};
- });
- });
-
- test('canStartReview computation', () => {
- const change1 = {};
- const change2 = {
- actions: {
- ready: {
- enabled: true,
- },
- },
- };
- const change3 = {
- actions: {
- ready: {
- label: 'Ready for Review',
- },
- },
- };
- assert.isFalse(element._computeCanStartReview(change1));
- assert.isTrue(element._computeCanStartReview(change2));
- assert.isFalse(element._computeCanStartReview(change3));
- });
- });
-
- test('header class computation', () => {
- assert.equal(element._computeHeaderClass(), 'header');
- assert.equal(element._computeHeaderClass(true), 'header editMode');
- });
-
- test('_maybeScrollToMessage', done => {
- flush(() => {
- const scrollStub = sandbox.stub(element.messagesList,
- 'scrollToMessage');
-
- element._maybeScrollToMessage('');
- assert.isFalse(scrollStub.called);
- element._maybeScrollToMessage('message');
- assert.isFalse(scrollStub.called);
- element._maybeScrollToMessage('#message-TEST');
- assert.isTrue(scrollStub.called);
- assert.equal(scrollStub.lastCall.args[0], 'TEST');
- done();
- });
- });
-
- test('topic update reloads related changes', () => {
- sandbox.stub(element.$.relatedChanges, 'reload');
- element.dispatchEvent(new CustomEvent('topic-changed'));
- assert.isTrue(element.$.relatedChanges.reload.calledOnce);
- });
-
- test('_computeEditMode', () => {
- const callCompute = (range, params) =>
- element._computeEditMode({base: range}, {base: params});
- assert.isFalse(callCompute({}, {}));
- assert.isTrue(callCompute({}, {edit: true}));
- assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {}));
- assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}, {}));
- assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}, {}));
- });
-
- test('_processEdit', () => {
- element._patchRange = {};
- const change = {
- current_revision: 'foo',
- revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}},
- };
- let mockChange;
-
- // With no edit, mockChange should be unmodified.
- element._processEdit(mockChange = _.cloneDeep(change), null);
- assert.deepEqual(mockChange, change);
-
- // When edit is not based on the latest PS, current_revision should be
- // unmodified.
- const edit = {
- base_patch_set_number: 1,
- commit: {commit: 'bar'},
- fetch: true,
- };
- element._processEdit(mockChange = _.cloneDeep(change), edit);
- assert.notDeepEqual(mockChange, change);
- assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME);
- assert.equal(mockChange.current_revision, change.current_revision);
- assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'});
- assert.notOk(mockChange.revisions.bar.actions);
-
- edit.base_revision = 'foo';
- element._processEdit(mockChange = _.cloneDeep(change), edit);
- assert.notDeepEqual(mockChange, change);
- assert.equal(mockChange.current_revision, 'bar');
- assert.deepEqual(mockChange.revisions.bar.actions,
- mockChange.revisions.foo.actions);
-
- // If _patchRange.patchNum is defined, do not load edit.
- element._patchRange.patchNum = 'baz';
- change.current_revision = 'baz';
- element._processEdit(mockChange = _.cloneDeep(change), edit);
- assert.equal(element._patchRange.patchNum, 'baz');
- assert.notOk(mockChange.revisions.bar.actions);
- });
-
- test('file-action-tap handling', () => {
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- const fileList = element.$.fileList;
- const Actions = GrEditConstants.Actions;
- const controls = element.$.fileListHeader.$.editControls;
- sandbox.stub(controls, 'openDeleteDialog');
- sandbox.stub(controls, 'openRenameDialog');
- sandbox.stub(controls, 'openRestoreDialog');
- sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
-
- // Delete
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.DELETE.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(controls.openDeleteDialog.called);
- assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo');
-
- // Restore
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.RESTORE.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(controls.openRestoreDialog.called);
- assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo');
-
- // Rename
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.RENAME.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(controls.openRenameDialog.called);
- assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
-
- // Open
- fileList.dispatchEvent(new CustomEvent('file-action-tap', {
- detail: {action: Actions.OPEN.id, path: 'foo'},
- bubbles: true,
- composed: true,
- }));
- flushAsynchronousOperations();
-
- assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called);
- assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo');
- assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[2], '1');
- assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called);
- });
-
- test('_selectedRevision updates when patchNum is changed', () => {
- const revision1 = {_number: 1, commit: {parents: []}};
- const revision2 = {_number: 2, commit: {parents: []}};
- sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
- Promise.resolve({
- revisions: {
- aaa: revision1,
- bbb: revision2,
- },
- labels: {},
- actions: {},
- current_revision: 'bbb',
- change_id: 'loremipsumdolorsitamet',
- }));
- sandbox.stub(element, '_getEdit').returns(Promise.resolve());
- sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
- element._patchRange = {patchNum: '2'};
- return element._getChangeDetail().then(() => {
- assert.strictEqual(element._selectedRevision, revision2);
-
- element.set('_patchRange.patchNum', '1');
- assert.strictEqual(element._selectedRevision, revision1);
- });
- });
-
- test('_selectedRevision is assigned when patchNum is edit', () => {
- const revision1 = {_number: 1, commit: {parents: []}};
- const revision2 = {_number: 2, commit: {parents: []}};
- const revision3 = {_number: 'edit', commit: {parents: []}};
- sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
- Promise.resolve({
- revisions: {
- aaa: revision1,
- bbb: revision2,
- ccc: revision3,
- },
- labels: {},
- actions: {},
- current_revision: 'ccc',
- change_id: 'loremipsumdolorsitamet',
- }));
- sandbox.stub(element, '_getEdit').returns(Promise.resolve());
- sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
- element._patchRange = {patchNum: 'edit'};
- return element._getChangeDetail().then(() => {
- assert.strictEqual(element._selectedRevision, revision3);
- });
- });
-
- test('_sendShowChangeEvent', () => {
- element._change = {labels: {}};
- element._patchRange = {patchNum: 4};
- element._mergeable = true;
- const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent');
- element._sendShowChangeEvent();
- assert.isTrue(showStub.calledOnce);
- assert.equal(
- showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE);
- assert.deepEqual(showStub.lastCall.args[1], {
- change: {labels: {}},
- patchNum: 4,
- info: {mergeable: true},
- });
- });
-
- suite('_handleEditTap', () => {
- let fireEdit;
-
- setup(() => {
- fireEdit = () => {
- element.$.actions.dispatchEvent(new CustomEvent('edit-tap'));
- };
- navigateToChangeStub.restore();
-
- element._change = {revisions: {rev1: {_number: 1}}};
- });
-
- test('edit exists in revisions', done => {
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 2);
- assert.equal(args[1], element.EDIT_NAME); // patchNum
- done();
- });
-
- element.set('_change.revisions.rev2', {_number: element.EDIT_NAME});
- flushAsynchronousOperations();
-
- fireEdit();
- });
-
- test('no edit exists in revisions, non-latest patchset', done => {
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 4);
- assert.equal(args[1], 1); // patchNum
- assert.equal(args[3], true); // opt_isEdit
- done();
- });
-
- element.set('_change.revisions.rev2', {_number: 2});
- element._patchRange = {patchNum: 1};
- flushAsynchronousOperations();
-
- fireEdit();
- });
-
- test('no edit exists in revisions, latest patchset', done => {
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 4);
- // No patch should be specified when patchNum == latest.
- assert.isNotOk(args[1]); // patchNum
- assert.equal(args[3], true); // opt_isEdit
- done();
- });
-
- element.set('_change.revisions.rev2', {_number: 2});
- element._patchRange = {patchNum: 2};
- flushAsynchronousOperations();
-
- fireEdit();
- });
- });
-
- test('_handleStopEditTap', done => {
- sandbox.stub(element.$.metadata, '_computeLabelNames');
- navigateToChangeStub.restore();
- sandbox.stub(element, 'computeLatestPatchNum').returns(1);
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 2);
- assert.equal(args[1], 1); // patchNum
- done();
- });
-
- element._patchRange = {patchNum: 1};
- element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap',
- {bubbles: false}));
- });
-
- suite('plugin endpoints', () => {
- test('endpoint params', done => {
- element._change = {labels: {}};
- element._selectedRevision = {};
- let hookEl;
- let plugin;
- Gerrit.install(
- p => {
- plugin = p;
- plugin.hook('change-view-integration').getLastAttached()
- .then(
- el => hookEl = el);
- },
- '0.1',
- 'http://some/plugins/url.html');
- flush(() => {
- assert.strictEqual(hookEl.plugin, plugin);
- assert.strictEqual(hookEl.change, element._change);
- assert.strictEqual(hookEl.revision, element._selectedRevision);
- done();
- });
- });
- });
-
- suite('_getMergeability', () => {
- let getMergeableStub;
-
- setup(() => {
- element._change = {labels: {}};
- getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable')
- .returns(Promise.resolve({mergeable: true}));
- });
-
- test('merged change', () => {
- element._mergeable = null;
- element._change.status = element.ChangeStatus.MERGED;
- return element._getMergeability().then(() => {
- assert.isFalse(element._mergeable);
- assert.isFalse(getMergeableStub.called);
- });
- });
-
- test('abandoned change', () => {
- element._mergeable = null;
- element._change.status = element.ChangeStatus.ABANDONED;
- return element._getMergeability().then(() => {
- assert.isFalse(element._mergeable);
- assert.isFalse(getMergeableStub.called);
- });
- });
-
- test('open change', () => {
- element._mergeable = null;
- return element._getMergeability().then(() => {
- assert.isTrue(element._mergeable);
- assert.isTrue(getMergeableStub.called);
- });
- });
- });
-
- test('_paramsChanged sets in projectLookup', () => {
sandbox.stub(element.$.relatedChanges, 'reload');
sandbox.stub(element, '_reload').returns(Promise.resolve());
- const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- element._paramsChanged({
- view: Gerrit.Nav.View.CHANGE,
- changeNum: 101,
- project: 'test-project',
- });
- assert.isTrue(setStub.calledOnce);
- assert.isTrue(setStub.calledWith(101, 'test-project'));
+ sandbox.spy(element, '_paramsChanged');
+ element.params = {view: 'change', changeNum: '1'};
});
- test('_handleToggleStar called when star is tapped', () => {
+ test('tab switch works correctly', done => {
+ assert.isTrue(element._paramsChanged.called);
+ assert.equal(element.$.commentTabs.selected, CommentTabs.CHANGE_LOG);
+ assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
+
+ const commentTab = element.shadowRoot.querySelector(
+ 'paper-tab.commentThreads'
+ );
+ // Switch to comment thread tab
+ MockInteractions.tap(commentTab);
+ const commentTabs = element.$.commentTabs;
+ assert.equal(commentTabs.selected,
+ CommentTabs.COMMENT_THREADS);
+ assert.equal(element._currentView, CommentTabs.COMMENT_THREADS);
+
+ // Switch back to 'Change Log' tab
+ element._paramsChanged(element.params);
+ flush(() => {
+ assert.equal(commentTabs.selected,
+ CommentTabs.CHANGE_LOG);
+ assert.equal(element._currentView, CommentTabs.CHANGE_LOG);
+ done();
+ });
+ });
+ });
+
+ suite('Findings comment tab', () => {
+ setup(done => {
element._change = {
- owner: {_account_id: 1},
- starred: false,
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2, commit: {parents: []}},
+ rev1: {_number: 1, commit: {parents: []}},
+ rev13: {_number: 13, commit: {parents: []}},
+ rev3: {_number: 3, commit: {parents: []}},
+ rev4: {_number: 4, commit: {parents: []}},
+ },
+ current_revision: 'rev4',
};
- element._loggedIn = true;
- const stub = sandbox.stub(element, '_handleToggleStar');
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.$.changeStar.shadowRoot
- .querySelector('button'));
- assert.isTrue(stub.called);
+ element._commentThreads = THREADS;
+ const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
+ MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
+ flush(() => {
+ done();
+ });
});
- suite('gr-reporting tests', () => {
- setup(() => {
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 1,
- };
- sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve());
- sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve());
- sandbox.stub(element, '_reloadComments').returns(Promise.resolve());
- sandbox.stub(element, '_getMergeability').returns(Promise.resolve());
- sandbox.stub(element, '_getLatestCommitMessage')
- .returns(Promise.resolve());
- });
+ test('robot comments count per patchset', () => {
+ const count = element._robotCommentCountPerPatchSet(THREADS);
+ const expectedCount = {
+ 2: 1,
+ 3: 1,
+ 4: 2,
+ };
+ assert.deepEqual(count, expectedCount);
+ assert.equal(element._computeText({_number: 2}, THREADS),
+ 'Patchset 2 (1 finding)');
+ assert.equal(element._computeText({_number: 4}, THREADS),
+ 'Patchset 4 (2 findings)');
+ assert.equal(element._computeText({_number: 5}, THREADS),
+ 'Patchset 5');
+ });
- test('don\'t report changedDisplayed on reply', done => {
- const changeDisplayStub =
- sandbox.stub(element.$.reporting, 'changeDisplayed');
- const changeFullyLoadedStub =
- sandbox.stub(element.$.reporting, 'changeFullyLoaded');
- element._handleReplySent();
+ test('only robot comments are rendered', () => {
+ assert.equal(element._robotCommentThreads.length, 2);
+ assert.equal(element._robotCommentThreads[0].comments[0].robot_id,
+ 'rc1');
+ assert.equal(element._robotCommentThreads[1].comments[0].robot_id,
+ 'rc2');
+ });
+
+ test('changing patchsets resets robot comments', done => {
+ element.set('_change.current_revision', 'rev3');
+ flush(() => {
+ assert.equal(element._robotCommentThreads.length, 1);
+ done();
+ });
+ });
+
+ test('Show more button is hidden', () => {
+ assert.isNull(element.shadowRoot.querySelector('.show-robot-comments'));
+ });
+
+ suite('robot comments show more button', () => {
+ setup(done => {
+ const arr = [];
+ for (let i = 0; i <= 30; i++) {
+ arr.push(...THREADS);
+ }
+ element._commentThreads = arr;
flush(() => {
- assert.isFalse(changeDisplayStub.called);
- assert.isFalse(changeFullyLoadedStub.called);
done();
});
});
- test('report changedDisplayed on _paramsChanged', done => {
- const changeDisplayStub =
- sandbox.stub(element.$.reporting, 'changeDisplayed');
- const changeFullyLoadedStub =
- sandbox.stub(element.$.reporting, 'changeFullyLoaded');
- element._paramsChanged({
- view: Gerrit.Nav.View.CHANGE,
- changeNum: 101,
- project: 'test-project',
- });
+ test('Show more button is rendered', () => {
+ assert.isOk(element.shadowRoot.querySelector('.show-robot-comments'));
+ assert.equal(element._robotCommentThreads.length,
+ ROBOT_COMMENTS_LIMIT);
+ });
+
+ test('Clicking show more button renders all comments', done => {
+ MockInteractions.tap(element.shadowRoot.querySelector(
+ '.show-robot-comments'));
flush(() => {
- assert.isTrue(changeDisplayStub.called);
- assert.isTrue(changeFullyLoadedStub.called);
+ assert.equal(element._robotCommentThreads.length, 62);
done();
});
});
});
});
+
+ test('reply button is not visible when logged out', () => {
+ assert.equal(getComputedStyle(element.$.replyBtn).display, 'none');
+ element._loggedIn = true;
+ assert.notEqual(getComputedStyle(element.$.replyBtn).display, 'none');
+ });
+
+ test('download tap calls _handleOpenDownloadDialog', () => {
+ sandbox.stub(element, '_handleOpenDownloadDialog');
+ element.$.actions.fire('download-tap');
+ assert.isTrue(element._handleOpenDownloadDialog.called);
+ });
+
+ test('fetches the server config on attached', done => {
+ flush(() => {
+ assert.equal(element._serverConfig.test, 'config');
+ done();
+ });
+ });
+
+ test('_changeStatuses', () => {
+ sandbox.stub(element, 'changeStatuses').returns(
+ ['Merged', 'WIP']);
+ element._loading = false;
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2},
+ rev1: {_number: 1},
+ rev13: {_number: 13},
+ rev3: {_number: 3},
+ },
+ current_revision: 'rev3',
+ labels: {
+ test: {
+ all: [],
+ default_value: 0,
+ values: [],
+ approved: {},
+ },
+ },
+ };
+ element._mergeable = true;
+ const expectedStatuses = ['Merged', 'WIP'];
+ assert.deepEqual(element._changeStatuses, expectedStatuses);
+ assert.equal(element._changeStatus, expectedStatuses.join(', '));
+ flushAsynchronousOperations();
+ const statusChips = dom(element.root)
+ .querySelectorAll('gr-change-status');
+ assert.equal(statusChips.length, 2);
+ });
+
+ test('diff preferences open when open-diff-prefs is fired', () => {
+ const overlayOpenStub = sandbox.stub(element.$.fileList,
+ 'openDiffPrefs');
+ element.$.fileListHeader.fire('open-diff-prefs');
+ assert.isTrue(overlayOpenStub.called);
+ });
+
+ test('_prepareCommitMsgForLinkify', () => {
+ let commitMessage = 'R=test@google.com';
+ let result = element._prepareCommitMsgForLinkify(commitMessage);
+ assert.equal(result, 'R=\u200Btest@google.com');
+
+ commitMessage = 'R=test@google.com\nR=test@google.com';
+ result = element._prepareCommitMsgForLinkify(commitMessage);
+ assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com');
+
+ commitMessage = 'CC=test@google.com';
+ result = element._prepareCommitMsgForLinkify(commitMessage);
+ assert.equal(result, 'CC=\u200Btest@google.com');
+ }),
+
+ test('_isSubmitEnabled', () => {
+ assert.isFalse(element._isSubmitEnabled({}));
+ assert.isFalse(element._isSubmitEnabled({submit: {}}));
+ assert.isTrue(element._isSubmitEnabled(
+ {submit: {enabled: true}}));
+ });
+
+ test('_reload is called when an approved label is removed', () => {
+ const vote = {_account_id: 1, name: 'bojack', value: 1};
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ owner: {email: 'abc@def'},
+ revisions: {
+ rev2: {_number: 2, commit: {parents: []}},
+ rev1: {_number: 1, commit: {parents: []}},
+ rev13: {_number: 13, commit: {parents: []}},
+ rev3: {_number: 3, commit: {parents: []}},
+ },
+ current_revision: 'rev3',
+ status: 'NEW',
+ labels: {
+ test: {
+ all: [vote],
+ default_value: 0,
+ values: [],
+ approved: {},
+ },
+ },
+ };
+ flushAsynchronousOperations();
+ const reloadStub = sandbox.stub(element, '_reload');
+ element.splice('_change.labels.test.all', 0, 1);
+ assert.isFalse(reloadStub.called);
+ element._change.labels.test.all.push(vote);
+ element._change.labels.test.all.push(vote);
+ element._change.labels.test.approved = vote;
+ flushAsynchronousOperations();
+ element.splice('_change.labels.test.all', 0, 2);
+ assert.isTrue(reloadStub.called);
+ assert.isTrue(reloadStub.calledOnce);
+ });
+
+ test('reply button has updated count when there are drafts', () => {
+ const getLabel = element._computeReplyButtonLabel;
+
+ assert.equal(getLabel(null, false), 'Reply');
+ assert.equal(getLabel(null, true), 'Start review');
+
+ const changeRecord = {base: null};
+ assert.equal(getLabel(changeRecord, false), 'Reply');
+
+ changeRecord.base = {};
+ assert.equal(getLabel(changeRecord, false), 'Reply');
+
+ changeRecord.base = {
+ 'file1.txt': [{}],
+ 'file2.txt': [{}, {}],
+ };
+ assert.equal(getLabel(changeRecord, false), 'Reply (3)');
+ });
+
+ test('start review button when owner of WIP change', () => {
+ assert.equal(
+ element._computeReplyButtonLabel(null, true),
+ 'Start review');
+ });
+
+ test('comment events properly update diff drafts', () => {
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ const draft = {
+ __draft: true,
+ id: 'id1',
+ path: '/foo/bar.txt',
+ text: 'hello',
+ };
+ element._handleCommentSave({detail: {comment: draft}});
+ draft.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
+ draft.patch_set = null;
+ draft.text = 'hello, there';
+ element._handleCommentSave({detail: {comment: draft}});
+ draft.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
+ const draft2 = {
+ __draft: true,
+ id: 'id2',
+ path: '/foo/bar.txt',
+ text: 'hola',
+ };
+ element._handleCommentSave({detail: {comment: draft2}});
+ draft2.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]});
+ draft.patch_set = null;
+ element._handleCommentDiscard({detail: {comment: draft}});
+ draft.patch_set = 2;
+ assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]});
+ element._handleCommentDiscard({detail: {comment: draft2}});
+ assert.deepEqual(element._diffDrafts, {});
+ });
+
+ test('change num change', () => {
+ element._changeNum = null;
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ labels: {},
+ };
+ element.viewState.changeNum = null;
+ element.viewState.diffMode = 'UNIFIED';
+ assert.equal(element.viewState.numFilesShown, 200);
+ assert.equal(element._numFilesShown, 200);
+ element._numFilesShown = 150;
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ assert.equal(element.viewState.numFilesShown, 150);
+
+ element._changeNum = '1';
+ element.params = {changeNum: '1'};
+ element._change.newProp = '1';
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ assert.equal(element.viewState.changeNum, '1');
+
+ element._changeNum = '2';
+ element.params = {changeNum: '2'};
+ element._change.newProp = '2';
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ assert.equal(element.viewState.changeNum, '2');
+ assert.equal(element.viewState.numFilesShown, 200);
+ assert.equal(element._numFilesShown, 200);
+ });
+
+ test('_setDiffViewMode is called with reset when new change is loaded',
+ () => {
+ sandbox.stub(element, '_setDiffViewMode');
+ element.viewState = {changeNum: 1};
+ element._changeNum = 2;
+ element._resetFileListViewState();
+ assert.isTrue(
+ element._setDiffViewMode.lastCall.calledWithExactly(true));
+ });
+
+ test('diffViewMode is propagated from file list header', () => {
+ element.viewState = {diffMode: 'UNIFIED'};
+ element.$.fileListHeader.diffViewMode = 'SIDE_BY_SIDE';
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ });
+
+ test('diffMode defaults to side by side without preferences', done => {
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({}));
+ // No user prefs or diff view mode set.
+
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('diffMode defaults to preference when not already set', done => {
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({default_diff_view: 'UNIFIED'}));
+
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ done();
+ });
+ });
+
+ test('existing diffMode overrides preference', done => {
+ element.viewState.diffMode = 'SIDE_BY_SIDE';
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({default_diff_view: 'UNIFIED'}));
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('don’t reload entire page when patchRange changes', () => {
+ const reloadStub = sandbox.stub(element, '_reload',
+ () => Promise.resolve());
+ const reloadPatchDependentStub = sandbox.stub(element,
+ '_reloadPatchNumDependentResources',
+ () => Promise.resolve());
+ const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
+ const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+
+ const value = {
+ view: Gerrit.Nav.View.CHANGE,
+ patchNum: '1',
+ };
+ element._paramsChanged(value);
+ assert.isTrue(reloadStub.calledOnce);
+ assert.isTrue(relatedClearSpy.calledOnce);
+
+ element._initialLoadComplete = true;
+
+ value.basePatchNum = '1';
+ value.patchNum = '2';
+ element._paramsChanged(value);
+ assert.isFalse(reloadStub.calledTwice);
+ assert.isTrue(reloadPatchDependentStub.calledOnce);
+ assert.isTrue(relatedClearSpy.calledOnce);
+ assert.isTrue(collapseStub.calledTwice);
+ });
+
+ test('reload entire page when patchRange doesnt change', () => {
+ const reloadStub = sandbox.stub(element, '_reload',
+ () => Promise.resolve());
+ const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+ const value = {
+ view: Gerrit.Nav.View.CHANGE,
+ };
+ element._paramsChanged(value);
+ assert.isTrue(reloadStub.calledOnce);
+ element._initialLoadComplete = true;
+ element._paramsChanged(value);
+ assert.isTrue(reloadStub.calledTwice);
+ assert.isTrue(collapseStub.calledTwice);
+ });
+
+ test('related changes are updated and new patch selected after rebase',
+ done => {
+ element._changeNum = '42';
+ sandbox.stub(element, 'computeLatestPatchNum', () => 1);
+ sandbox.stub(element, '_reload',
+ () => Promise.resolve());
+ const e = {detail: {action: 'rebase'}};
+ element._handleReloadChange(e).then(() => {
+ assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
+ element._change));
+ done();
+ });
+ });
+
+ test('related changes are not updated after other action', done => {
+ sandbox.stub(element, '_reload', () => Promise.resolve());
+ sandbox.stub(element.$.relatedChanges, 'reload');
+ const e = {detail: {action: 'abandon'}};
+ element._handleReloadChange(e).then(() => {
+ assert.isFalse(navigateToChangeStub.called);
+ done();
+ });
+ });
+
+ test('_computeMergedCommitInfo', () => {
+ const dummyRevs = {
+ 1: {commit: {commit: 1}},
+ 2: {commit: {}},
+ };
+ assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {});
+ assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs),
+ dummyRevs[1].commit);
+
+ // Regression test for issue 5337.
+ const commit = element._computeMergedCommitInfo(2, dummyRevs);
+ assert.notDeepEqual(commit, dummyRevs[2]);
+ assert.deepEqual(commit, {commit: 2});
+ });
+
+ test('_computeCopyTextForTitle', () => {
+ const change = {
+ _number: 123,
+ subject: 'test subject',
+ revisions: {
+ rev1: {_number: 1},
+ rev3: {_number: 3},
+ },
+ current_revision: 'rev3',
+ };
+ sandbox.stub(Gerrit.Nav, 'getUrlForChange')
+ .returns('/change/123');
+ assert.equal(
+ element._computeCopyTextForTitle(change),
+ '123: test subject | https://localhost:8081/change/123'
+ );
+ });
+
+ test('get latest revision', () => {
+ let change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev3: {_number: 3},
+ },
+ current_revision: 'rev3',
+ };
+ assert.equal(element._getLatestRevisionSHA(change), 'rev3');
+ change = {
+ revisions: {
+ rev1: {_number: 1},
+ },
+ };
+ assert.equal(element._getLatestRevisionSHA(change), 'rev1');
+ });
+
+ test('show commit message edit button', () => {
+ const _change = {
+ status: element.ChangeStatus.MERGED,
+ };
+ assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
+ assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
+ assert.isTrue(element._computeHideEditCommitMessage(false, true, {}));
+ assert.isFalse(element._computeHideEditCommitMessage(true, false, {}));
+ assert.isTrue(element._computeHideEditCommitMessage(true, false,
+ _change));
+ assert.isTrue(element._computeHideEditCommitMessage(true, false, {},
+ true));
+ assert.isFalse(element._computeHideEditCommitMessage(true, false, {},
+ false));
+ });
+
+ test('_handleCommitMessageSave trims trailing whitespace', () => {
+ const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
+ .returns(Promise.resolve({}));
+
+ const mockEvent = content => { return {detail: {content}}; };
+
+ element._handleCommitMessageSave(mockEvent('test \n test '));
+ assert.equal(putStub.lastCall.args[1], 'test\n test');
+
+ element._handleCommitMessageSave(mockEvent(' test\ntest'));
+ assert.equal(putStub.lastCall.args[1], ' test\ntest');
+
+ element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
+ assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
+ });
+
+ test('_computeChangeIdCommitMessageError', () => {
+ let commitMessage =
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
+ let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ null);
+
+ change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'mismatch');
+
+ commitMessage = 'This is the greatest change.';
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'missing');
+ });
+
+ test('multiple change Ids in commit message picks last', () => {
+ const commitMessage = [
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
+ ].join('\n');
+ let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ null);
+ change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'mismatch');
+ });
+
+ test('does not count change Id that starts mid line', () => {
+ const commitMessage = [
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
+ 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
+ ].join(' and ');
+ let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ null);
+ change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
+ assert.equal(
+ element._computeChangeIdCommitMessageError(commitMessage, change),
+ 'mismatch');
+ });
+
+ test('_computeTitleAttributeWarning', () => {
+ let changeIdCommitMessageError = 'missing';
+ assert.equal(
+ element._computeTitleAttributeWarning(changeIdCommitMessageError),
+ 'No Change-Id in commit message');
+
+ changeIdCommitMessageError = 'mismatch';
+ assert.equal(
+ element._computeTitleAttributeWarning(changeIdCommitMessageError),
+ 'Change-Id mismatch');
+ });
+
+ test('_computeChangeIdClass', () => {
+ let changeIdCommitMessageError = 'missing';
+ assert.equal(
+ element._computeChangeIdClass(changeIdCommitMessageError), '');
+
+ changeIdCommitMessageError = 'mismatch';
+ assert.equal(
+ element._computeChangeIdClass(changeIdCommitMessageError), 'warning');
+ });
+
+ test('topic is coalesced to null', done => {
+ sandbox.stub(element, '_changeChanged');
+ sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
+ id: '123456789',
+ labels: {},
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}}},
+ }));
+
+ element._getChangeDetail().then(() => {
+ assert.isNull(element._change.topic);
+ done();
+ });
+ });
+
+ test('commit sha is populated from getChangeDetail', done => {
+ sandbox.stub(element, '_changeChanged');
+ sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
+ id: '123456789',
+ labels: {},
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}}},
+ }));
+
+ element._getChangeDetail().then(() => {
+ assert.equal('foo', element._commitInfo.commit);
+ done();
+ });
+ });
+
+ test('edit is added to change', () => {
+ sandbox.stub(element, '_changeChanged');
+ sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
+ id: '123456789',
+ labels: {},
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}}},
+ }));
+ sandbox.stub(element, '_getEdit', () => Promise.resolve({
+ base_patch_set_number: 1,
+ commit: {commit: 'bar'},
+ }));
+ element._patchRange = {};
+
+ return element._getChangeDetail().then(() => {
+ const revs = element._change.revisions;
+ assert.equal(Object.keys(revs).length, 2);
+ assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}});
+ assert.deepEqual(revs['bar'], {
+ _number: element.EDIT_NAME,
+ basePatchNum: 1,
+ commit: {commit: 'bar'},
+ fetch: undefined,
+ });
+ });
+ });
+
+ test('_getBasePatchNum', () => {
+ const _change = {
+ _number: 42,
+ revisions: {
+ '98da160735fb81604b4c40e93c368f380539dd0e': {
+ _number: 1,
+ commit: {
+ parents: [],
+ },
+ },
+ },
+ };
+ const _patchRange = {
+ basePatchNum: 'PARENT',
+ };
+ assert.equal(element._getBasePatchNum(_change, _patchRange), 'PARENT');
+
+ element._prefs = {
+ default_base_for_merges: 'FIRST_PARENT',
+ };
+
+ const _change2 = {
+ _number: 42,
+ revisions: {
+ '98da160735fb81604b4c40e93c368f380539dd0e': {
+ _number: 1,
+ commit: {
+ parents: [
+ {
+ commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8',
+ subject: 'test',
+ },
+ {
+ commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841',
+ subject: 'test3',
+ },
+ ],
+ },
+ },
+ },
+ };
+ assert.equal(element._getBasePatchNum(_change2, _patchRange), -1);
+
+ _patchRange.patchNum = 1;
+ assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT');
+ });
+
+ test('_openReplyDialog called with `ANY` when coming from tap event',
+ () => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element._serverConfig = {};
+ MockInteractions.tap(element.$.replyBtn);
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openStub.callCount, 1);
+ });
+
+ test('_openReplyDialog called with `BODY` when coming from message reply' +
+ 'event', done => {
+ flush(() => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element.messagesList.fire('reply',
+ {message: {message: 'text'}});
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.BODY),
+ '_openReplyDialog should have been passed BODY');
+ assert.equal(openStub.callCount, 1);
+ done();
+ });
+ });
+
+ test('reply dialog focus can be controlled', () => {
+ const FocusTarget = element.$.replyDialog.FocusTarget;
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+
+ const e = {detail: {}};
+ element._handleShowReplyDialog(e);
+ assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
+ '_openReplyDialog should have been passed REVIEWERS');
+ assert.equal(openStub.callCount, 1);
+
+ e.detail.value = {ccsOnly: true};
+ element._handleShowReplyDialog(e);
+ assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
+ '_openReplyDialog should have been passed CCS');
+ assert.equal(openStub.callCount, 2);
+ });
+
+ test('getUrlParameter functionality', () => {
+ const locationStub = sandbox.stub(element, '_getLocationSearch');
+
+ locationStub.returns('?test');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('?test2=12&test=3');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?test2');
+ assert.isNull(element._getUrlParameter('test'));
+ });
+
+ test('revert dialog opened with revert param', done => {
+ sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true));
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded', () => Promise.resolve());
+
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1, commit: {parents: []}},
+ rev2: {_number: 2, commit: {parents: []}},
+ },
+ current_revision: 'rev1',
+ status: element.ChangeStatus.MERGED,
+ labels: {},
+ actions: {},
+ };
+
+ sandbox.stub(element, '_getUrlParameter',
+ param => {
+ assert.equal(param, 'revert');
+ return param;
+ });
+
+ sandbox.stub(element.$.actions, 'showRevertDialog',
+ done);
+
+ element._maybeShowRevertDialog();
+ assert.isTrue(Gerrit.awaitPluginsLoaded.called);
+ });
+
+ suite('scroll related tests', () => {
+ test('document scrolling calls function to set scroll height', done => {
+ const originalHeight = document.body.scrollHeight;
+ const scrollStub = sandbox.stub(element, '_handleScroll',
+ () => {
+ assert.isTrue(scrollStub.called);
+ document.body.style.height = originalHeight + 'px';
+ scrollStub.restore();
+ done();
+ });
+ document.body.style.height = '10000px';
+ element._handleScroll();
+ });
+
+ test('scrollTop is set correctly', () => {
+ element.viewState = {scrollTop: TEST_SCROLL_TOP_PX};
+
+ sandbox.stub(element, '_reload', () => {
+ // When element is reloaded, ensure that the history
+ // state has the scrollTop set earlier. This will then
+ // be reset.
+ assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX);
+ return Promise.resolve({});
+ });
+
+ // simulate reloading component, which is done when route
+ // changes to match a regex of change view type.
+ element._paramsChanged({view: Gerrit.Nav.View.CHANGE});
+ });
+
+ test('scrollTop is reset when new change is loaded', () => {
+ element._resetFileListViewState();
+ assert.equal(element.viewState.scrollTop, 0);
+ });
+ });
+
+ suite('reply dialog tests', () => {
+ setup(() => {
+ sandbox.stub(element.$.replyDialog, '_draftChanged');
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+ element._change = {labels: {}};
+ });
+
+ test('reply from comment adds quote text', () => {
+ const e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from comment replaces quote text', () => {
+ element.$.replyDialog.draft = '> old quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> old quote text\n\n';
+ const e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from same comment preserves quote text', () => {
+ element.$.replyDialog.draft = '> quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> quote text\n\n';
+ const e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.draft,
+ '> quote text\n\n some draft text');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from top of page contains previous draft', () => {
+ const div = document.createElement('div');
+ element.$.replyDialog.draft = '> quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> quote text\n\n';
+ const e = {target: div, preventDefault: sandbox.spy()};
+ element._handleReplyTap(e);
+ assert.equal(element.$.replyDialog.draft,
+ '> quote text\n\n some draft text');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+ });
+
+ test('reply button is disabled until server config is loaded', () => {
+ assert.isTrue(element._replyDisabled);
+ element._serverConfig = {};
+ assert.isFalse(element._replyDisabled);
+ });
+
+ suite('commit message expand/collapse', () => {
+ setup(() => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: false}));
+ });
+
+ test('commitCollapseToggle hidden for short commit message', () => {
+ element._latestCommitMessage = '';
+ assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden'));
+ });
+
+ test('commitCollapseToggle shown for long commit message', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden'));
+ });
+
+ test('commitCollapseToggle functions', () => {
+ element._latestCommitMessage = _.times(35, String).join('\n');
+ assert.isTrue(element._commitCollapsed);
+ assert.isTrue(element._commitCollapsible);
+ assert.isTrue(
+ element.$.commitMessageEditor.hasAttribute('collapsed'));
+ MockInteractions.tap(element.$.commitCollapseToggleButton);
+ assert.isFalse(element._commitCollapsed);
+ assert.isTrue(element._commitCollapsible);
+ assert.isFalse(
+ element.$.commitMessageEditor.hasAttribute('collapsed'));
+ });
+ });
+
+ suite('related changes expand/collapse', () => {
+ let updateHeightSpy;
+ setup(() => {
+ updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight');
+ });
+
+ test('relatedChangesToggle shown height greater than changeInfo height',
+ () => {
+ assert.isFalse(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getScrollHeight', () => 60);
+ sandbox.stub(element, '_getLineHeight', () => 5);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+ element.$.relatedChanges.dispatchEvent(
+ new CustomEvent('new-section-loaded'));
+ assert.isTrue(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ assert.equal(updateHeightSpy.callCount, 1);
+ });
+
+ test('relatedChangesToggle hidden height less than changeInfo height',
+ () => {
+ assert.isFalse(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getScrollHeight', () => 40);
+ sandbox.stub(element, '_getLineHeight', () => 5);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+ element.$.relatedChanges.dispatchEvent(
+ new CustomEvent('new-section-loaded'));
+ assert.isFalse(element.$.relatedChangesToggle.classList
+ .contains('showToggle'));
+ assert.equal(updateHeightSpy.callCount, 1);
+ });
+
+ test('relatedChangesToggle functions', () => {
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+ element._relatedChangesLoading = false;
+ assert.isTrue(element._relatedChangesCollapsed);
+ assert.isTrue(
+ element.$.relatedChanges.classList.contains('collapsed'));
+ MockInteractions.tap(element.$.relatedChangesToggleButton);
+ assert.isFalse(element._relatedChangesCollapsed);
+ assert.isFalse(
+ element.$.relatedChanges.classList.contains('collapsed'));
+ });
+
+ test('_updateRelatedChangeMaxHeight without commit toggle', () => {
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+
+ // 50 (existing height) - 30 (extra height) = 20 (adjusted height).
+ // 20 (max existing height) % 12 (line height) = 6 (remainder).
+ // 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
+
+ element._updateRelatedChangeMaxHeight();
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '12px');
+ assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
+ '');
+ });
+
+ test('_updateRelatedChangeMaxHeight with commit toggle', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
+
+ // 50 (existing height) % 12 (line height) = 2 (remainder).
+ // 50 (existing height) - 2 (remainder) = 48 (max height to set).
+
+ element._updateRelatedChangeMaxHeight();
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '48px');
+ assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
+ '2px');
+ });
+
+ test('_updateRelatedChangeMaxHeight in small screen mode', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
+
+ element._updateRelatedChangeMaxHeight();
+
+ // 400 (new height) % 12 (line height) = 4 (remainder).
+ // 400 (new height) - 4 (remainder) = 396.
+
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '396px');
+ });
+
+ test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
+ element._latestCommitMessage = _.times(31, String).join('\n');
+ sandbox.stub(element, '_getOffsetHeight', () => 50);
+ sandbox.stub(element, '_getLineHeight', () => 12);
+ sandbox.stub(window, 'matchMedia', () => {
+ if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') {
+ return {matches: true};
+ } else {
+ return {matches: false};
+ }
+ });
+
+ // 100 (new height) % 12 (line height) = 4 (remainder).
+ // 100 (new height) - 4 (remainder) = 96.
+ element._updateRelatedChangeMaxHeight();
+ assert.equal(getCustomCssValue('--relation-chain-max-height'),
+ '96px');
+ });
+
+ suite('update checks', () => {
+ setup(() => {
+ sandbox.spy(element, '_startUpdateCheckTimer');
+ sandbox.stub(element, 'async', f => {
+ // Only fire the async callback one time.
+ if (element.async.callCount > 1) { return; }
+ f.call(element);
+ });
+ });
+
+ test('_startUpdateCheckTimer negative delay', () => {
+ sandbox.stub(element, 'fetchChangeUpdates');
+
+ element._serverConfig = {change: {update_delay: -1}};
+
+ assert.isTrue(element._startUpdateCheckTimer.called);
+ assert.isFalse(element.fetchChangeUpdates.called);
+ });
+
+ test('_startUpdateCheckTimer up-to-date', () => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: true}));
+
+ element._serverConfig = {change: {update_delay: 12345}};
+
+ assert.isTrue(element._startUpdateCheckTimer.called);
+ assert.isTrue(element.fetchChangeUpdates.called);
+ assert.equal(element.async.lastCall.args[1], 12345 * 1000);
+ });
+
+ test('_startUpdateCheckTimer out-of-date shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => Promise.resolve({isLatest: false}));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'A newer patch set has been uploaded');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new status shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newStatus: element.ChangeStatus.MERGED,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message, 'This change has been merged');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new messages shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newMessages: true,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'There are new messages on this change');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+ });
+
+ test('canStartReview computation', () => {
+ const change1 = {};
+ const change2 = {
+ actions: {
+ ready: {
+ enabled: true,
+ },
+ },
+ };
+ const change3 = {
+ actions: {
+ ready: {
+ label: 'Ready for Review',
+ },
+ },
+ };
+ assert.isFalse(element._computeCanStartReview(change1));
+ assert.isTrue(element._computeCanStartReview(change2));
+ assert.isFalse(element._computeCanStartReview(change3));
+ });
+ });
+
+ test('header class computation', () => {
+ assert.equal(element._computeHeaderClass(), 'header');
+ assert.equal(element._computeHeaderClass(true), 'header editMode');
+ });
+
+ test('_maybeScrollToMessage', done => {
+ flush(() => {
+ const scrollStub = sandbox.stub(element.messagesList,
+ 'scrollToMessage');
+
+ element._maybeScrollToMessage('');
+ assert.isFalse(scrollStub.called);
+ element._maybeScrollToMessage('message');
+ assert.isFalse(scrollStub.called);
+ element._maybeScrollToMessage('#message-TEST');
+ assert.isTrue(scrollStub.called);
+ assert.equal(scrollStub.lastCall.args[0], 'TEST');
+ done();
+ });
+ });
+
+ test('topic update reloads related changes', () => {
+ sandbox.stub(element.$.relatedChanges, 'reload');
+ element.dispatchEvent(new CustomEvent('topic-changed'));
+ assert.isTrue(element.$.relatedChanges.reload.calledOnce);
+ });
+
+ test('_computeEditMode', () => {
+ const callCompute = (range, params) =>
+ element._computeEditMode({base: range}, {base: params});
+ assert.isFalse(callCompute({}, {}));
+ assert.isTrue(callCompute({}, {edit: true}));
+ assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {}));
+ assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}, {}));
+ assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}, {}));
+ });
+
+ test('_processEdit', () => {
+ element._patchRange = {};
+ const change = {
+ current_revision: 'foo',
+ revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}},
+ };
+ let mockChange;
+
+ // With no edit, mockChange should be unmodified.
+ element._processEdit(mockChange = _.cloneDeep(change), null);
+ assert.deepEqual(mockChange, change);
+
+ // When edit is not based on the latest PS, current_revision should be
+ // unmodified.
+ const edit = {
+ base_patch_set_number: 1,
+ commit: {commit: 'bar'},
+ fetch: true,
+ };
+ element._processEdit(mockChange = _.cloneDeep(change), edit);
+ assert.notDeepEqual(mockChange, change);
+ assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME);
+ assert.equal(mockChange.current_revision, change.current_revision);
+ assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'});
+ assert.notOk(mockChange.revisions.bar.actions);
+
+ edit.base_revision = 'foo';
+ element._processEdit(mockChange = _.cloneDeep(change), edit);
+ assert.notDeepEqual(mockChange, change);
+ assert.equal(mockChange.current_revision, 'bar');
+ assert.deepEqual(mockChange.revisions.bar.actions,
+ mockChange.revisions.foo.actions);
+
+ // If _patchRange.patchNum is defined, do not load edit.
+ element._patchRange.patchNum = 'baz';
+ change.current_revision = 'baz';
+ element._processEdit(mockChange = _.cloneDeep(change), edit);
+ assert.equal(element._patchRange.patchNum, 'baz');
+ assert.notOk(mockChange.revisions.bar.actions);
+ });
+
+ test('file-action-tap handling', () => {
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ const fileList = element.$.fileList;
+ const Actions = GrEditConstants.Actions;
+ const controls = element.$.fileListHeader.$.editControls;
+ sandbox.stub(controls, 'openDeleteDialog');
+ sandbox.stub(controls, 'openRenameDialog');
+ sandbox.stub(controls, 'openRestoreDialog');
+ sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+
+ // Delete
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.DELETE.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(controls.openDeleteDialog.called);
+ assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo');
+
+ // Restore
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.RESTORE.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(controls.openRestoreDialog.called);
+ assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo');
+
+ // Rename
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.RENAME.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(controls.openRenameDialog.called);
+ assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
+
+ // Open
+ fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+ detail: {action: Actions.OPEN.id, path: 'foo'},
+ bubbles: true,
+ composed: true,
+ }));
+ flushAsynchronousOperations();
+
+ assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called);
+ assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo');
+ assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[2], '1');
+ assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called);
+ });
+
+ test('_selectedRevision updates when patchNum is changed', () => {
+ const revision1 = {_number: 1, commit: {parents: []}};
+ const revision2 = {_number: 2, commit: {parents: []}};
+ sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
+ Promise.resolve({
+ revisions: {
+ aaa: revision1,
+ bbb: revision2,
+ },
+ labels: {},
+ actions: {},
+ current_revision: 'bbb',
+ change_id: 'loremipsumdolorsitamet',
+ }));
+ sandbox.stub(element, '_getEdit').returns(Promise.resolve());
+ sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
+ element._patchRange = {patchNum: '2'};
+ return element._getChangeDetail().then(() => {
+ assert.strictEqual(element._selectedRevision, revision2);
+
+ element.set('_patchRange.patchNum', '1');
+ assert.strictEqual(element._selectedRevision, revision1);
+ });
+ });
+
+ test('_selectedRevision is assigned when patchNum is edit', () => {
+ const revision1 = {_number: 1, commit: {parents: []}};
+ const revision2 = {_number: 2, commit: {parents: []}};
+ const revision3 = {_number: 'edit', commit: {parents: []}};
+ sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
+ Promise.resolve({
+ revisions: {
+ aaa: revision1,
+ bbb: revision2,
+ ccc: revision3,
+ },
+ labels: {},
+ actions: {},
+ current_revision: 'ccc',
+ change_id: 'loremipsumdolorsitamet',
+ }));
+ sandbox.stub(element, '_getEdit').returns(Promise.resolve());
+ sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
+ element._patchRange = {patchNum: 'edit'};
+ return element._getChangeDetail().then(() => {
+ assert.strictEqual(element._selectedRevision, revision3);
+ });
+ });
+
+ test('_sendShowChangeEvent', () => {
+ element._change = {labels: {}};
+ element._patchRange = {patchNum: 4};
+ element._mergeable = true;
+ const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent');
+ element._sendShowChangeEvent();
+ assert.isTrue(showStub.calledOnce);
+ assert.equal(
+ showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE);
+ assert.deepEqual(showStub.lastCall.args[1], {
+ change: {labels: {}},
+ patchNum: 4,
+ info: {mergeable: true},
+ });
+ });
+
+ suite('_handleEditTap', () => {
+ let fireEdit;
+
+ setup(() => {
+ fireEdit = () => {
+ element.$.actions.dispatchEvent(new CustomEvent('edit-tap'));
+ };
+ navigateToChangeStub.restore();
+
+ element._change = {revisions: {rev1: {_number: 1}}};
+ });
+
+ test('edit exists in revisions', done => {
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 2);
+ assert.equal(args[1], element.EDIT_NAME); // patchNum
+ done();
+ });
+
+ element.set('_change.revisions.rev2', {_number: element.EDIT_NAME});
+ flushAsynchronousOperations();
+
+ fireEdit();
+ });
+
+ test('no edit exists in revisions, non-latest patchset', done => {
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 4);
+ assert.equal(args[1], 1); // patchNum
+ assert.equal(args[3], true); // opt_isEdit
+ done();
+ });
+
+ element.set('_change.revisions.rev2', {_number: 2});
+ element._patchRange = {patchNum: 1};
+ flushAsynchronousOperations();
+
+ fireEdit();
+ });
+
+ test('no edit exists in revisions, latest patchset', done => {
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 4);
+ // No patch should be specified when patchNum == latest.
+ assert.isNotOk(args[1]); // patchNum
+ assert.equal(args[3], true); // opt_isEdit
+ done();
+ });
+
+ element.set('_change.revisions.rev2', {_number: 2});
+ element._patchRange = {patchNum: 2};
+ flushAsynchronousOperations();
+
+ fireEdit();
+ });
+ });
+
+ test('_handleStopEditTap', done => {
+ sandbox.stub(element.$.metadata, '_computeLabelNames');
+ navigateToChangeStub.restore();
+ sandbox.stub(element, 'computeLatestPatchNum').returns(1);
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 2);
+ assert.equal(args[1], 1); // patchNum
+ done();
+ });
+
+ element._patchRange = {patchNum: 1};
+ element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap',
+ {bubbles: false}));
+ });
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element._change = {labels: {}};
+ element._selectedRevision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-view-integration').getLastAttached()
+ .then(
+ el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element._change);
+ assert.strictEqual(hookEl.revision, element._selectedRevision);
+ done();
+ });
+ });
+ });
+
+ suite('_getMergeability', () => {
+ let getMergeableStub;
+
+ setup(() => {
+ element._change = {labels: {}};
+ getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable')
+ .returns(Promise.resolve({mergeable: true}));
+ });
+
+ test('merged change', () => {
+ element._mergeable = null;
+ element._change.status = element.ChangeStatus.MERGED;
+ return element._getMergeability().then(() => {
+ assert.isFalse(element._mergeable);
+ assert.isFalse(getMergeableStub.called);
+ });
+ });
+
+ test('abandoned change', () => {
+ element._mergeable = null;
+ element._change.status = element.ChangeStatus.ABANDONED;
+ return element._getMergeability().then(() => {
+ assert.isFalse(element._mergeable);
+ assert.isFalse(getMergeableStub.called);
+ });
+ });
+
+ test('open change', () => {
+ element._mergeable = null;
+ return element._getMergeability().then(() => {
+ assert.isTrue(element._mergeable);
+ assert.isTrue(getMergeableStub.called);
+ });
+ });
+ });
+
+ test('_paramsChanged sets in projectLookup', () => {
+ sandbox.stub(element.$.relatedChanges, 'reload');
+ sandbox.stub(element, '_reload').returns(Promise.resolve());
+ const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: 101,
+ project: 'test-project',
+ });
+ assert.isTrue(setStub.calledOnce);
+ assert.isTrue(setStub.calledWith(101, 'test-project'));
+ });
+
+ test('_handleToggleStar called when star is tapped', () => {
+ element._change = {
+ owner: {_account_id: 1},
+ starred: false,
+ };
+ element._loggedIn = true;
+ const stub = sandbox.stub(element, '_handleToggleStar');
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.$.changeStar.shadowRoot
+ .querySelector('button'));
+ assert.isTrue(stub.called);
+ });
+
+ suite('gr-reporting tests', () => {
+ setup(() => {
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve());
+ sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve());
+ sandbox.stub(element, '_reloadComments').returns(Promise.resolve());
+ sandbox.stub(element, '_getMergeability').returns(Promise.resolve());
+ sandbox.stub(element, '_getLatestCommitMessage')
+ .returns(Promise.resolve());
+ });
+
+ test('don\'t report changedDisplayed on reply', done => {
+ const changeDisplayStub =
+ sandbox.stub(element.$.reporting, 'changeDisplayed');
+ const changeFullyLoadedStub =
+ sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+ element._handleReplySent();
+ flush(() => {
+ assert.isFalse(changeDisplayStub.called);
+ assert.isFalse(changeFullyLoadedStub.called);
+ done();
+ });
+ });
+
+ test('report changedDisplayed on _paramsChanged', done => {
+ const changeDisplayStub =
+ sandbox.stub(element.$.reporting, 'changeDisplayed');
+ const changeFullyLoadedStub =
+ sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: 101,
+ project: 'test-project',
+ });
+ flush(() => {
+ assert.isTrue(changeDisplayStub.called);
+ assert.isTrue(changeFullyLoadedStub.called);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 16c55cd..2649733 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.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,79 +14,100 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+/*
+ The custom CSS property `--gr-formatted-text-prose-max-width` controls the max
+ width of formatted text blocks that are not code.
+*/
+/*
+ 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';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrCommentList extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.PathListBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment-list'; }
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-formatted-text/gr-formatted-text.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-comment-list_html.js';
- static get properties() {
- return {
- changeNum: Number,
- comments: Object,
- patchNum: Number,
- projectName: String,
- /** @type {?} */
- projectConfig: Object,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrCommentList extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _computeFilesFromComments(comments) {
- const arr = Object.keys(comments || {});
- return arr.sort(this.specialFilePathCompare);
- }
+ static get is() { return 'gr-comment-list'; }
- _isOnParent(comment) {
- return comment.side === 'PARENT';
- }
-
- _computeDiffURL(filePath, changeNum, allComments) {
- if ([filePath, changeNum, allComments].some(arg => arg === undefined)) {
- return;
- }
- const fileComments = this._computeCommentsForFile(allComments, filePath);
- // This can happen for files that don't exist anymore in the current ps.
- if (fileComments.length === 0) return;
- return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
- filePath, fileComments[0].patch_set);
- }
-
- _computeDiffLineURL(filePath, changeNum, patchNum, comment) {
- const basePatchNum = comment.hasOwnProperty('parent') ?
- -comment.parent : null;
- return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
- filePath, patchNum, basePatchNum, comment.line,
- this._isOnParent(comment));
- }
-
- _computeCommentsForFile(comments, filePath) {
- // Changes are not picked up by the dom-repeat due to the array instance
- // identity not changing even when it has elements added/removed from it.
- return (comments[filePath] || []).slice();
- }
-
- _computePatchDisplayName(comment) {
- if (this._isOnParent(comment)) {
- return 'Base, ';
- }
- if (comment.patch_set != this.patchNum) {
- return `PS${comment.patch_set}, `;
- }
- return '';
- }
+ static get properties() {
+ return {
+ changeNum: Number,
+ comments: Object,
+ patchNum: Number,
+ projectName: String,
+ /** @type {?} */
+ projectConfig: Object,
+ };
}
- customElements.define(GrCommentList.is, GrCommentList);
-})();
+ _computeFilesFromComments(comments) {
+ const arr = Object.keys(comments || {});
+ return arr.sort(this.specialFilePathCompare);
+ }
+
+ _isOnParent(comment) {
+ return comment.side === 'PARENT';
+ }
+
+ _computeDiffURL(filePath, changeNum, allComments) {
+ if ([filePath, changeNum, allComments].some(arg => arg === undefined)) {
+ return;
+ }
+ const fileComments = this._computeCommentsForFile(allComments, filePath);
+ // This can happen for files that don't exist anymore in the current ps.
+ if (fileComments.length === 0) return;
+ return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
+ filePath, fileComments[0].patch_set);
+ }
+
+ _computeDiffLineURL(filePath, changeNum, patchNum, comment) {
+ const basePatchNum = comment.hasOwnProperty('parent') ?
+ -comment.parent : null;
+ return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName,
+ filePath, patchNum, basePatchNum, comment.line,
+ this._isOnParent(comment));
+ }
+
+ _computeCommentsForFile(comments, filePath) {
+ // Changes are not picked up by the dom-repeat due to the array instance
+ // identity not changing even when it has elements added/removed from it.
+ return (comments[filePath] || []).slice();
+ }
+
+ _computePatchDisplayName(comment) {
+ if (this._isOnParent(comment)) {
+ return 'Base, ';
+ }
+ if (comment.patch_set != this.patchNum) {
+ return `PS${comment.patch_set}, `;
+ }
+ return '';
+ }
+}
+
+customElements.define(GrCommentList.is, GrCommentList);
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
index 52c0c89..d50ba6a 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
@@ -1,35 +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/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<!--
- The custom CSS property `--gr-formatted-text-prose-max-width` controls the max
- width of formatted text blocks that are not code.
--->
-
-<dom-module id="gr-comment-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -64,27 +51,19 @@
</style>
<template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
<div class="file"><a class="fileLink" href="[[_computeDiffURL(file, changeNum, comments)]]">[[computeDisplayPath(file)]]</a></div>
- <template is="dom-repeat"
- items="[[_computeCommentsForFile(comments, file)]]" as="comment">
+ <template is="dom-repeat" items="[[_computeCommentsForFile(comments, file)]]" as="comment">
<div class="container">
- <a class="lineNum"
- href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
- <span hidden$="[[!comment.line]]">
+ <a class="lineNum" href\$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
+ <span hidden\$="[[!comment.line]]">
<span>[[_computePatchDisplayName(comment)]]</span>
Line <span>[[comment.line]]</span>
</span>
- <span hidden$="[[comment.line]]">
+ <span hidden\$="[[comment.line]]">
File comment:
</span>
</a>
- <gr-formatted-text
- class="message"
- no-trailing-margin
- content="[[comment.message]]"
- config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+ <gr-formatted-text class="message" no-trailing-margin="" content="[[comment.message]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text>
</div>
</template>
</template>
- </template>
- <script src="gr-comment-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index a91ec0e..5e8c3ab 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_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-comment-list</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-comment-list.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-comment-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-comment-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,98 +40,101 @@
</template>
</test-fixture>
-<script>
- suite('gr-comment-list 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-comment-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-comment-list tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('_computeFilesFromComments w/ special file path sorting', () => {
- const comments = {
- 'file_b.html': [],
- 'file_c.css': [],
- 'file_a.js': [],
- 'test.cc': [],
- 'test.h': [],
- };
- const expected = [
- 'file_a.js',
- 'file_b.html',
- 'file_c.css',
- 'test.h',
- 'test.cc',
- ];
- const actual = element._computeFilesFromComments(comments);
- assert.deepEqual(actual, expected);
-
- assert.deepEqual(element._computeFilesFromComments(null), []);
- });
-
- test('_computePatchDisplayName', () => {
- const comment = {line: 123, side: 'REVISION', patch_set: 10};
-
- element.patchNum = 10;
- assert.equal(element._computePatchDisplayName(comment), '');
-
- element.patchNum = 9;
- assert.equal(element._computePatchDisplayName(comment), 'PS10, ');
-
- comment.side = 'PARENT';
- assert.equal(element._computePatchDisplayName(comment), 'Base, ');
- });
-
- test('config commentlinks propagate to formatted text', () => {
- element.comments = {
- 'test.h': [{
- author: {name: 'foo'},
- patch_set: 4,
- line: 10,
- updated: '2017-10-30 20:48:40.000000000',
- message: 'Ideadbeefdeadbeef',
- unresolved: true,
- }],
- };
- element.projectConfig = {
- commentlinks: {foo: {link: '#/q/$1', match: '(I[0-9a-f]{8,40})'}},
- };
- flushAsynchronousOperations();
- const formattedText = Polymer.dom(element.root).querySelector(
- 'gr-formatted-text.message');
- assert.isOk(formattedText.config);
- assert.deepEqual(formattedText.config,
- element.projectConfig.commentlinks);
- });
-
- test('_computeDiffLineURL', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- element.projectName = 'proj';
- element.changeNum = 123;
-
- const comment = {line: 456};
- element._computeDiffLineURL('foo.cc', 123, 4, comment);
- assert.isTrue(getUrlStub.calledOnce);
- assert.deepEqual(getUrlStub.lastCall.args,
- [123, 'proj', 'foo.cc', 4, null, 456, false]);
-
- comment.side = 'PARENT';
- element._computeDiffLineURL('foo.cc', 123, 4, comment);
- assert.isTrue(getUrlStub.calledTwice);
- assert.deepEqual(getUrlStub.lastCall.args,
- [123, 'proj', 'foo.cc', 4, null, 456, true]);
-
- comment.parent = 12;
- element._computeDiffLineURL('foo.cc', 123, 4, comment);
- assert.isTrue(getUrlStub.calledThrice);
- assert.deepEqual(getUrlStub.lastCall.args,
- [123, 'proj', 'foo.cc', 4, -12, 456, true]);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('_computeFilesFromComments w/ special file path sorting', () => {
+ const comments = {
+ 'file_b.html': [],
+ 'file_c.css': [],
+ 'file_a.js': [],
+ 'test.cc': [],
+ 'test.h': [],
+ };
+ const expected = [
+ 'file_a.js',
+ 'file_b.html',
+ 'file_c.css',
+ 'test.h',
+ 'test.cc',
+ ];
+ const actual = element._computeFilesFromComments(comments);
+ assert.deepEqual(actual, expected);
+
+ assert.deepEqual(element._computeFilesFromComments(null), []);
+ });
+
+ test('_computePatchDisplayName', () => {
+ const comment = {line: 123, side: 'REVISION', patch_set: 10};
+
+ element.patchNum = 10;
+ assert.equal(element._computePatchDisplayName(comment), '');
+
+ element.patchNum = 9;
+ assert.equal(element._computePatchDisplayName(comment), 'PS10, ');
+
+ comment.side = 'PARENT';
+ assert.equal(element._computePatchDisplayName(comment), 'Base, ');
+ });
+
+ test('config commentlinks propagate to formatted text', () => {
+ element.comments = {
+ 'test.h': [{
+ author: {name: 'foo'},
+ patch_set: 4,
+ line: 10,
+ updated: '2017-10-30 20:48:40.000000000',
+ message: 'Ideadbeefdeadbeef',
+ unresolved: true,
+ }],
+ };
+ element.projectConfig = {
+ commentlinks: {foo: {link: '#/q/$1', match: '(I[0-9a-f]{8,40})'}},
+ };
+ flushAsynchronousOperations();
+ const formattedText = dom(element.root).querySelector(
+ 'gr-formatted-text.message');
+ assert.isOk(formattedText.config);
+ assert.deepEqual(formattedText.config,
+ element.projectConfig.commentlinks);
+ });
+
+ test('_computeDiffLineURL', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ element.projectName = 'proj';
+ element.changeNum = 123;
+
+ const comment = {line: 456};
+ element._computeDiffLineURL('foo.cc', 123, 4, comment);
+ assert.isTrue(getUrlStub.calledOnce);
+ assert.deepEqual(getUrlStub.lastCall.args,
+ [123, 'proj', 'foo.cc', 4, null, 456, false]);
+
+ comment.side = 'PARENT';
+ element._computeDiffLineURL('foo.cc', 123, 4, comment);
+ assert.isTrue(getUrlStub.calledTwice);
+ assert.deepEqual(getUrlStub.lastCall.args,
+ [123, 'proj', 'foo.cc', 4, null, 456, true]);
+
+ comment.parent = 12;
+ element._computeDiffLineURL('foo.cc', 123, 4, comment);
+ assert.isTrue(getUrlStub.calledThrice);
+ assert.deepEqual(getUrlStub.lastCall.args,
+ [123, 'proj', 'foo.cc', 4, -12, 456, true]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
index a339865..79a3692 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -14,68 +14,75 @@
* 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 GrCommitInfo extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-commit-info'; }
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.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-commit-info_html.js';
- static get properties() {
- return {
- change: Object,
- /** @type {?} */
- commitInfo: Object,
- serverConfig: Object,
- _showWebLink: {
- type: Boolean,
- computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
- },
- _webLink: {
- type: String,
- computed: '_computeWebLink(change, commitInfo, serverConfig)',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrCommitInfo extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _getWeblink(change, commitInfo, config) {
- return Gerrit.Nav.getPatchSetWeblink(
- change.project,
- commitInfo.commit,
- {
- weblinks: commitInfo.web_links,
- config,
- });
- }
+ static get is() { return 'gr-commit-info'; }
- _computeShowWebLink(change, commitInfo, serverConfig) {
- // Polymer 2: check for undefined
- if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const weblink = this._getWeblink(change, commitInfo, serverConfig);
- return !!weblink && !!weblink.url;
- }
-
- _computeWebLink(change, commitInfo, serverConfig) {
- // Polymer 2: check for undefined
- if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const {url} = this._getWeblink(change, commitInfo, serverConfig) || {};
- return url;
- }
-
- _computeShortHash(commitInfo) {
- const {name} =
- this._getWeblink(this.change, commitInfo, this.serverConfig) || {};
- return name;
- }
+ static get properties() {
+ return {
+ change: Object,
+ /** @type {?} */
+ commitInfo: Object,
+ serverConfig: Object,
+ _showWebLink: {
+ type: Boolean,
+ computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
+ },
+ _webLink: {
+ type: String,
+ computed: '_computeWebLink(change, commitInfo, serverConfig)',
+ },
+ };
}
- customElements.define(GrCommitInfo.is, GrCommitInfo);
-})();
+ _getWeblink(change, commitInfo, config) {
+ return Gerrit.Nav.getPatchSetWeblink(
+ change.project,
+ commitInfo.commit,
+ {
+ weblinks: commitInfo.web_links,
+ config,
+ });
+ }
+
+ _computeShowWebLink(change, commitInfo, serverConfig) {
+ // Polymer 2: check for undefined
+ if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const weblink = this._getWeblink(change, commitInfo, serverConfig);
+ return !!weblink && !!weblink.url;
+ }
+
+ _computeWebLink(change, commitInfo, serverConfig) {
+ // Polymer 2: check for undefined
+ if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const {url} = this._getWeblink(change, commitInfo, serverConfig) || {};
+ return url;
+ }
+
+ _computeShortHash(commitInfo) {
+ const {name} =
+ this._getWeblink(this.change, commitInfo, this.serverConfig) || {};
+ return name;
+ }
+}
+
+customElements.define(GrCommitInfo.is, GrCommitInfo);
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
index 902bf41..ffd36f4 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
@@ -1,26 +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="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-
-<dom-module id="gr-commit-info">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.container {
align-items: center;
@@ -29,19 +25,12 @@
</style>
<div class="container">
<template is="dom-if" if="[[_showWebLink]]">
- <a target="_blank" rel="noopener"
- href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
+ <a target="_blank" rel="noopener" href\$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
</template>
<template is="dom-if" if="[[!_showWebLink]]">
[[_computeShortHash(commitInfo)]]
</template>
- <gr-copy-clipboard
- has-tooltip
- button-title="Copy full SHA to clipboard"
- hide-input
- text="[[commitInfo.commit]]">
+ <gr-copy-clipboard has-tooltip="" button-title="Copy full SHA to clipboard" hide-input="" text="[[commitInfo.commit]]">
</gr-copy-clipboard>
</div>
- </template>
- <script src="gr-commit-info.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
index b063561..f2fdbe0 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-commit-info</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="../../core/gr-router/gr-router.html">
-<link rel="import" href="gr-commit-info.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="../../core/gr-router/gr-router.js"></script>
+<script type="module" src="./gr-commit-info.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../core/gr-router/gr-router.js';
+import './gr-commit-info.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,105 +42,108 @@
</template>
</test-fixture>
-<script>
- suite('gr-commit-info tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../core/gr-router/gr-router.js';
+import './gr-commit-info.js';
+suite('gr-commit-info tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('weblinks use Gerrit.Nav interface', () => {
- const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
- .returns([{name: 'stubb', url: '#s'}]);
- element.change = {};
- element.commitInfo = {};
- element.serverConfig = {};
- assert.isTrue(weblinksStub.called);
- });
-
- test('no web link when unavailable', () => {
- element.commitInfo = {};
- element.serverConfig = {};
- element.change = {labels: [], project: ''};
-
- assert.isNotOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- });
-
- test('use web link when available', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.change = {labels: [], project: ''};
- element.commitInfo =
- {commit: 'commitsha', web_links: [{name: 'gitweb', url: 'link-url'}]};
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'link-url');
- });
-
- test('does not relativize web links that begin with scheme', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.change = {labels: [], project: ''};
- element.commitInfo = {
- commit: 'commitsha',
- web_links: [{name: 'gitweb', url: 'https://link-url'}],
- };
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'https://link-url');
- });
-
- test('ignore web links that are neither gitweb nor gitiles', () => {
- const router = document.createElement('gr-router');
- sandbox.stub(Gerrit.Nav, '_generateWeblinks',
- router._generateWeblinks.bind(router));
-
- element.change = {project: 'project-name'};
- element.commitInfo = {
- commit: 'commit-sha',
- web_links: [
- {
- name: 'ignore',
- url: 'ignore',
- },
- {
- name: 'gitiles',
- url: 'https://link-url',
- },
- ],
- };
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'https://link-url');
-
- // Remove gitiles link.
- element.commitInfo.web_links.splice(1, 1);
- assert.isNotOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.isNotOk(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('weblinks use Gerrit.Nav interface', () => {
+ const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
+ .returns([{name: 'stubb', url: '#s'}]);
+ element.change = {};
+ element.commitInfo = {};
+ element.serverConfig = {};
+ assert.isTrue(weblinksStub.called);
+ });
+
+ test('no web link when unavailable', () => {
+ element.commitInfo = {};
+ element.serverConfig = {};
+ element.change = {labels: [], project: ''};
+
+ assert.isNotOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ });
+
+ test('use web link when available', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.change = {labels: [], project: ''};
+ element.commitInfo =
+ {commit: 'commitsha', web_links: [{name: 'gitweb', url: 'link-url'}]};
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'link-url');
+ });
+
+ test('does not relativize web links that begin with scheme', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.change = {labels: [], project: ''};
+ element.commitInfo = {
+ commit: 'commitsha',
+ web_links: [{name: 'gitweb', url: 'https://link-url'}],
+ };
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'https://link-url');
+ });
+
+ test('ignore web links that are neither gitweb nor gitiles', () => {
+ const router = document.createElement('gr-router');
+ sandbox.stub(Gerrit.Nav, '_generateWeblinks',
+ router._generateWeblinks.bind(router));
+
+ element.change = {project: 'project-name'};
+ element.commitInfo = {
+ commit: 'commit-sha',
+ web_links: [
+ {
+ name: 'ignore',
+ url: 'ignore',
+ },
+ {
+ name: 'gitiles',
+ url: 'https://link-url',
+ },
+ ],
+ };
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'https://link-url');
+
+ // Remove gitiles link.
+ element.commitInfo.web_links.splice(1, 1);
+ assert.isNotOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.isNotOk(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
index 555c605..d950988 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -14,69 +14,79 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.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-confirm-abandon-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmAbandonDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-abandon-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmAbandonDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-abandon-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- message: String,
- };
- }
-
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter': '_handleEnterKey',
- };
- }
-
- resetFocus() {
- this.$.messageInput.textarea.focus();
- }
-
- _handleEnterKey(e) {
- this._confirm();
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this._confirm();
- }
-
- _confirm() {
- this.fire('confirm', {reason: this.message}, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ static get properties() {
+ return {
+ message: String,
+ };
}
- customElements.define(GrConfirmAbandonDialog.is, GrConfirmAbandonDialog);
-})();
+ get keyBindings() {
+ return {
+ 'ctrl+enter meta+enter': '_handleEnterKey',
+ };
+ }
+
+ resetFocus() {
+ this.$.messageInput.textarea.focus();
+ }
+
+ _handleEnterKey(e) {
+ this._confirm();
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this._confirm();
+ }
+
+ _confirm() {
+ this.fire('confirm', {reason: this.message}, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmAbandonDialog.is, GrConfirmAbandonDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
index 9e7857c4..e8b530b 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
@@ -1,28 +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/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-abandon-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -48,21 +42,11 @@
width: 73ch; /* Add a char to account for the border. */
}
</style>
- <gr-dialog
- confirm-label="Abandon"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Abandon" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">Abandon Change</div>
<div class="main" slot="main">
<label for="messageInput">Abandon Message</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- placeholder="<Insert reasoning here>"
- bind-value="{{message}}"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" placeholder="<Insert reasoning here>" bind-value="{{message}}"></iron-autogrow-textarea>
</div>
</gr-dialog>
- </template>
- <script src="gr-confirm-abandon-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
index 3786174..522b290 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-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-confirm-abandon-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-confirm-abandon-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-confirm-abandon-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-abandon-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,46 +40,48 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-abandon-dialog 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-confirm-abandon-dialog.js';
+suite('gr-confirm-abandon-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_handleConfirmTap', () => {
- const confirmHandler = sandbox.stub();
- element.addEventListener('confirm', confirmHandler);
- sandbox.spy(element, '_handleConfirmTap');
- sandbox.spy(element, '_confirm');
- element.shadowRoot
- .querySelector('gr-dialog').fire('confirm');
- assert.isTrue(confirmHandler.called);
- assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(element._handleConfirmTap.called);
- assert.isTrue(element._confirm.called);
- assert.isTrue(element._confirm.called);
- assert.isTrue(element._confirm.calledOnce);
- });
-
- test('_handleCancelTap', () => {
- const cancelHandler = sandbox.stub();
- element.addEventListener('cancel', cancelHandler);
- sandbox.spy(element, '_handleCancelTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('cancel');
- assert.isTrue(cancelHandler.called);
- assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(element._handleCancelTap.called);
- assert.isTrue(element._handleCancelTap.calledOnce);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sandbox.stub();
+ element.addEventListener('confirm', confirmHandler);
+ sandbox.spy(element, '_handleConfirmTap');
+ sandbox.spy(element, '_confirm');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('confirm');
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(confirmHandler.calledOnce);
+ assert.isTrue(element._handleConfirmTap.called);
+ assert.isTrue(element._confirm.called);
+ assert.isTrue(element._confirm.called);
+ assert.isTrue(element._confirm.calledOnce);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sandbox.stub();
+ element.addEventListener('cancel', cancelHandler);
+ sandbox.spy(element, '_handleCancelTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('cancel');
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(cancelHandler.calledOnce);
+ assert.isTrue(element._handleCancelTap.called);
+ assert.isTrue(element._handleCancelTap.calledOnce);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
index 35e9afb..9c5ddf4 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -14,45 +14,54 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dialog/gr-dialog.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-confirm-cherrypick-conflict-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmCherrypickConflictDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-cherrypick-conflict-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
*/
- class GrConfirmCherrypickConflictDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-cherrypick-conflict-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
}
- customElements.define(GrConfirmCherrypickConflictDialog.is,
- GrConfirmCherrypickConflictDialog);
-})();
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmCherrypickConflictDialog.is,
+ GrConfirmCherrypickConflictDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
index b9e9155..c03c246 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
@@ -1,27 +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="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-
-<dom-module id="gr-confirm-cherrypick-conflict-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -36,10 +31,7 @@
width: 100%;
}
</style>
- <gr-dialog
- confirm-label="Continue"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Continue" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">Cherry Pick Conflict!</div>
<div class="main" slot="main">
<span>Cherry Pick failed! (merge conflicts)</span>
@@ -47,6 +39,4 @@
<span>Please select "Continue" to continue with conflicts or select "cancel" to close the dialog.</span>
</div>
</gr-dialog>
- </template>
- <script src="gr-confirm-cherrypick-conflict-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
index 7c9896a..58b1182 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-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-confirm-cherrypick-conflict-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-confirm-cherrypick-conflict-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-confirm-cherrypick-conflict-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-cherrypick-conflict-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,41 +40,43 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-cherrypick-conflict-dialog 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-confirm-cherrypick-conflict-dialog.js';
+suite('gr-confirm-cherrypick-conflict-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('_handleConfirmTap', () => {
- const confirmHandler = sandbox.stub();
- element.addEventListener('confirm', confirmHandler);
- sandbox.spy(element, '_handleConfirmTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('confirm');
- assert.isTrue(confirmHandler.called);
- assert.isTrue(confirmHandler.calledOnce);
- assert.isTrue(element._handleConfirmTap.called);
- assert.isTrue(element._handleConfirmTap.calledOnce);
- });
-
- test('_handleCancelTap', () => {
- const cancelHandler = sandbox.stub();
- element.addEventListener('cancel', cancelHandler);
- sandbox.spy(element, '_handleCancelTap');
- element.shadowRoot
- .querySelector('gr-dialog').fire('cancel');
- assert.isTrue(cancelHandler.called);
- assert.isTrue(cancelHandler.calledOnce);
- assert.isTrue(element._handleCancelTap.called);
- assert.isTrue(element._handleCancelTap.calledOnce);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sandbox.stub();
+ element.addEventListener('confirm', confirmHandler);
+ sandbox.spy(element, '_handleConfirmTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('confirm');
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(confirmHandler.calledOnce);
+ assert.isTrue(element._handleConfirmTap.called);
+ assert.isTrue(element._handleConfirmTap.calledOnce);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sandbox.stub();
+ element.addEventListener('cancel', cancelHandler);
+ sandbox.spy(element, '_handleCancelTap');
+ element.shadowRoot
+ .querySelector('gr-dialog').fire('cancel');
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(cancelHandler.calledOnce);
+ assert.isTrue(element._handleCancelTap.called);
+ assert.isTrue(element._handleCancelTap.calledOnce);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index 2b10a97..7405a30 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -14,115 +14,128 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const SUGGESTIONS_LIMIT = 15;
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-confirm-cherrypick-dialog_html.js';
+
+const SUGGESTIONS_LIMIT = 15;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmCherrypickDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-cherrypick-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmCherrypickDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-cherrypick-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- branch: String,
- baseCommit: String,
- changeStatus: String,
- commitMessage: String,
- commitNum: String,
- message: String,
- project: String,
- _query: {
- type: Function,
- value() {
- return this._getProjectBranchesSuggestions.bind(this);
- },
+ static get properties() {
+ return {
+ branch: String,
+ baseCommit: String,
+ changeStatus: String,
+ commitMessage: String,
+ commitNum: String,
+ message: String,
+ project: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getProjectBranchesSuggestions.bind(this);
},
- };
- }
-
- static get observers() {
- return [
- '_computeMessage(changeStatus, commitNum, commitMessage)',
- ];
- }
-
- _computeMessage(changeStatus, commitNum, commitMessage) {
- // Polymer 2: check for undefined
- if ([
- changeStatus,
- commitNum,
- commitMessage,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- let newMessage = commitMessage;
-
- if (changeStatus === 'MERGED') {
- newMessage += '(cherry picked from commit ' + commitNum + ')';
- }
- this.message = newMessage;
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- resetFocus() {
- this.$.branchInput.focus();
- }
-
- _getProjectBranchesSuggestions(input) {
- if (input.startsWith('refs/heads/')) {
- input = input.substring('refs/heads/'.length);
- }
- return this.$.restAPI.getRepoBranches(
- input, this.project, SUGGESTIONS_LIMIT).then(response => {
- const branches = [];
- let branch;
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- if (response[key].ref.startsWith('refs/heads/')) {
- branch = response[key].ref.substring('refs/heads/'.length);
- } else {
- branch = response[key].ref;
- }
- branches.push({
- name: branch,
- });
- }
- return branches;
- });
- }
+ },
+ };
}
- customElements.define(GrConfirmCherrypickDialog.is,
- GrConfirmCherrypickDialog);
-})();
+ static get observers() {
+ return [
+ '_computeMessage(changeStatus, commitNum, commitMessage)',
+ ];
+ }
+
+ _computeMessage(changeStatus, commitNum, commitMessage) {
+ // Polymer 2: check for undefined
+ if ([
+ changeStatus,
+ commitNum,
+ commitMessage,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ let newMessage = commitMessage;
+
+ if (changeStatus === 'MERGED') {
+ newMessage += '(cherry picked from commit ' + commitNum + ')';
+ }
+ this.message = newMessage;
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ resetFocus() {
+ this.$.branchInput.focus();
+ }
+
+ _getProjectBranchesSuggestions(input) {
+ if (input.startsWith('refs/heads/')) {
+ input = input.substring('refs/heads/'.length);
+ }
+ return this.$.restAPI.getRepoBranches(
+ input, this.project, SUGGESTIONS_LIMIT).then(response => {
+ const branches = [];
+ let branch;
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ if (response[key].ref.startsWith('refs/heads/')) {
+ branch = response[key].ref.substring('refs/heads/'.length);
+ } else {
+ branch = response[key].ref;
+ }
+ branches.push({
+ name: branch,
+ });
+ }
+ return branches;
+ });
+ }
+}
+
+customElements.define(GrConfirmCherrypickDialog.is,
+ GrConfirmCherrypickDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
index cab9fd6..ee6e55d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
@@ -1,31 +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="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-confirm-cherrypick-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -54,48 +45,25 @@
width: 73ch; /* Add a char to account for the border. */
}
</style>
- <gr-dialog
- confirm-label="Cherry Pick"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Cherry Pick" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">Cherry Pick Change to Another Branch</div>
<div class="main" slot="main">
<label for="branchInput">
Cherry Pick to branch
</label>
- <gr-autocomplete
- id="branchInput"
- text="{{branch}}"
- query="[[_query]]"
- placeholder="Destination branch">
+ <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch">
</gr-autocomplete>
<label for="baseInput">
Provide base commit sha1 for cherry-pick
</label>
- <iron-input
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
- <input
- is="iron-input"
- id="baseCommitInput"
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}">
+ <iron-input maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
+ <input is="iron-input" id="baseCommitInput" maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
</iron-input>
<label for="messageInput">
Cherry Pick Commit Message
</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- rows="4"
- max-rows="15"
- bind-value="{{message}}"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea>
</div>
</gr-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-cherrypick-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
index 12e6252..fa9531f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-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-confirm-cherrypick-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-confirm-cherrypick-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-confirm-cherrypick-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-cherrypick-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,85 +40,87 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-cherrypick-dialog 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-confirm-cherrypick-dialog.js';
+suite('gr-confirm-cherrypick-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getRepoBranches(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- ref: 'refs/heads/test-branch',
- revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
- can_delete: true,
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- });
- element = fixture('basic');
- element.project = 'test-project';
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getRepoBranches(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ ref: 'refs/heads/test-branch',
+ revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+ can_delete: true,
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
});
+ element = fixture('basic');
+ element.project = 'test-project';
+ });
- teardown(() => { sandbox.restore(); });
+ teardown(() => { sandbox.restore(); });
- test('with merged change', () => {
- element.changeStatus = 'MERGED';
- element.commitMessage = 'message\n';
- element.commitNum = '123';
- element.branch = 'master';
- flushAsynchronousOperations();
- const expectedMessage = 'message\n(cherry picked from commit 123)';
- assert.equal(element.message, expectedMessage);
- });
+ test('with merged change', () => {
+ element.changeStatus = 'MERGED';
+ element.commitMessage = 'message\n';
+ element.commitNum = '123';
+ element.branch = 'master';
+ flushAsynchronousOperations();
+ const expectedMessage = 'message\n(cherry picked from commit 123)';
+ assert.equal(element.message, expectedMessage);
+ });
- test('with unmerged change', () => {
- element.changeStatus = 'OPEN';
- element.commitMessage = 'message\n';
- element.commitNum = '123';
- element.branch = 'master';
- flushAsynchronousOperations();
- const expectedMessage = 'message\n';
- assert.equal(element.message, expectedMessage);
- });
+ test('with unmerged change', () => {
+ element.changeStatus = 'OPEN';
+ element.commitMessage = 'message\n';
+ element.commitNum = '123';
+ element.branch = 'master';
+ flushAsynchronousOperations();
+ const expectedMessage = 'message\n';
+ assert.equal(element.message, expectedMessage);
+ });
- test('with updated commit message', () => {
- element.changeStatus = 'OPEN';
- element.commitMessage = 'message\n';
- element.commitNum = '123';
- element.branch = 'master';
- const myNewMessage = 'updated commit message';
- element.message = myNewMessage;
- flushAsynchronousOperations();
- assert.equal(element.message, myNewMessage);
- });
+ test('with updated commit message', () => {
+ element.changeStatus = 'OPEN';
+ element.commitMessage = 'message\n';
+ element.commitNum = '123';
+ element.branch = 'master';
+ const myNewMessage = 'updated commit message';
+ element.message = myNewMessage;
+ flushAsynchronousOperations();
+ assert.equal(element.message, myNewMessage);
+ });
- test('_getProjectBranchesSuggestions empty', done => {
- element._getProjectBranchesSuggestions('nonexistent').then(branches => {
- assert.equal(branches.length, 0);
- done();
- });
- });
-
- test('resetFocus', () => {
- const focusStub = sandbox.stub(element.$.branchInput, 'focus');
- element.resetFocus();
- assert.isTrue(focusStub.called);
- });
-
- test('_getProjectBranchesSuggestions non-empty', done => {
- element._getProjectBranchesSuggestions('test-branch').then(branches => {
- assert.equal(branches.length, 1);
- assert.equal(branches[0].name, 'test-branch');
- done();
- });
+ test('_getProjectBranchesSuggestions empty', done => {
+ element._getProjectBranchesSuggestions('nonexistent').then(branches => {
+ assert.equal(branches.length, 0);
+ done();
});
});
+
+ test('resetFocus', () => {
+ const focusStub = sandbox.stub(element.$.branchInput, 'focus');
+ element.resetFocus();
+ assert.isTrue(focusStub.called);
+ });
+
+ test('_getProjectBranchesSuggestions non-empty', done => {
+ element._getProjectBranchesSuggestions('test-branch').then(branches => {
+ assert.equal(branches.length, 1);
+ assert.equal(branches[0].name, 'test-branch');
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
index 8932af7..8316951 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -14,90 +14,102 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const SUGGESTIONS_LIMIT = 15;
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-confirm-move-dialog_html.js';
+
+const SUGGESTIONS_LIMIT = 15;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmMoveDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-move-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmMoveDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-move-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- branch: String,
- message: String,
- project: String,
- _query: {
- type: Function,
- value() {
- return this._getProjectBranchesSuggestions.bind(this);
- },
+ static get properties() {
+ return {
+ branch: String,
+ message: String,
+ project: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getProjectBranchesSuggestions.bind(this);
},
- };
- }
-
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter': '_handleConfirmTap',
- };
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- _getProjectBranchesSuggestions(input) {
- if (input.startsWith('refs/heads/')) {
- input = input.substring('refs/heads/'.length);
- }
- return this.$.restAPI.getRepoBranches(
- input, this.project, SUGGESTIONS_LIMIT).then(response => {
- const branches = [];
- let branch;
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- if (response[key].ref.startsWith('refs/heads/')) {
- branch = response[key].ref.substring('refs/heads/'.length);
- } else {
- branch = response[key].ref;
- }
- branches.push({
- name: branch,
- });
- }
- return branches;
- });
- }
+ },
+ };
}
- customElements.define(GrConfirmMoveDialog.is, GrConfirmMoveDialog);
-})();
+ get keyBindings() {
+ return {
+ 'ctrl+enter meta+enter': '_handleConfirmTap',
+ };
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ _getProjectBranchesSuggestions(input) {
+ if (input.startsWith('refs/heads/')) {
+ input = input.substring('refs/heads/'.length);
+ }
+ return this.$.restAPI.getRepoBranches(
+ input, this.project, SUGGESTIONS_LIMIT).then(response => {
+ const branches = [];
+ let branch;
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ if (response[key].ref.startsWith('refs/heads/')) {
+ branch = response[key].ref.substring('refs/heads/'.length);
+ } else {
+ branch = response[key].ref;
+ }
+ branches.push({
+ name: branch,
+ });
+ }
+ return branches;
+ });
+ }
+}
+
+customElements.define(GrConfirmMoveDialog.is, GrConfirmMoveDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
index f65ec03..b8f3336 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
@@ -1,30 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-confirm-move-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -54,10 +46,7 @@
color: var(--error-text-color);
}
</style>
- <gr-dialog
- confirm-label="Move Change"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Move Change" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">Move Change to Another Branch</div>
<div class="main" slot="main">
<p class="warning">
@@ -66,25 +55,13 @@
<label for="branchInput">
Move change to branch
</label>
- <gr-autocomplete
- id="branchInput"
- text="{{branch}}"
- query="[[_query]]"
- placeholder="Destination branch">
+ <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch">
</gr-autocomplete>
<label for="messageInput">
Move Change Message
</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- rows="4"
- max-rows="15"
- bind-value="{{message}}"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea>
</div>
</gr-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-move-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
index 465ce73..27c9934 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-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-confirm-move-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-confirm-move-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-confirm-move-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-move-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,52 +40,54 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-move-dialog tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-move-dialog.js';
+suite('gr-confirm-move-dialog tests', () => {
+ let element;
- setup(() => {
- stub('gr-rest-api-interface', {
- getRepoBranches(input) {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- ref: 'refs/heads/test-branch',
- revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
- can_delete: true,
- },
- ]);
- } else {
- return Promise.resolve({});
- }
- },
- });
- element = fixture('basic');
- element.project = 'test-project';
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getRepoBranches(input) {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ ref: 'refs/heads/test-branch',
+ revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+ can_delete: true,
+ },
+ ]);
+ } else {
+ return Promise.resolve({});
+ }
+ },
});
+ element = fixture('basic');
+ element.project = 'test-project';
+ });
- test('with updated commit message', () => {
- element.branch = 'master';
- const myNewMessage = 'updated commit message';
- element.message = myNewMessage;
- flushAsynchronousOperations();
- assert.equal(element.message, myNewMessage);
- });
+ test('with updated commit message', () => {
+ element.branch = 'master';
+ const myNewMessage = 'updated commit message';
+ element.message = myNewMessage;
+ flushAsynchronousOperations();
+ assert.equal(element.message, myNewMessage);
+ });
- test('_getProjectBranchesSuggestions empty', done => {
- element._getProjectBranchesSuggestions('nonexistent').then(branches => {
- assert.equal(branches.length, 0);
- done();
- });
- });
-
- test('_getProjectBranchesSuggestions non-empty', done => {
- element._getProjectBranchesSuggestions('test-branch').then(branches => {
- assert.equal(branches.length, 1);
- assert.equal(branches[0].name, 'test-branch');
- done();
- });
+ test('_getProjectBranchesSuggestions empty', done => {
+ element._getProjectBranchesSuggestions('nonexistent').then(branches => {
+ assert.equal(branches.length, 0);
+ done();
});
});
+
+ test('_getProjectBranchesSuggestions non-empty', done => {
+ element._getProjectBranchesSuggestions('test-branch').then(branches => {
+ assert.equal(branches.length, 1);
+ assert.equal(branches[0].name, 'test-branch');
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index 607f587..e451034 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -14,157 +14,166 @@
* 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 GrConfirmRebaseDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-confirm-rebase-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.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-confirm-rebase-dialog_html.js';
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
+/** @extends Polymer.Element */
+class GrConfirmRebaseDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- branch: String,
- changeNumber: Number,
- hasParent: Boolean,
- rebaseOnCurrent: Boolean,
- _text: String,
- _query: {
- type: Function,
- value() {
- return this._getChangeSuggestions.bind(this);
- },
+ static get is() { return 'gr-confirm-rebase-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
+
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
+
+ static get properties() {
+ return {
+ branch: String,
+ changeNumber: Number,
+ hasParent: Boolean,
+ rebaseOnCurrent: Boolean,
+ _text: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getChangeSuggestions.bind(this);
},
- _recentChanges: Array,
- };
- }
-
- static get observers() {
- return [
- '_updateSelectedOption(rebaseOnCurrent, hasParent)',
- ];
- }
-
- // This is called by gr-change-actions every time the rebase dialog is
- // re-opened. Unlike other autocompletes that make a request with each
- // updated input, this one gets all recent changes once and then filters
- // them by the input. The query is re-run each time the dialog is opened
- // in case there are new/updated changes in the generic query since the
- // last time it was run.
- fetchRecentChanges() {
- return this.$.restAPI.getChanges(null, `is:open -age:90d`)
- .then(response => {
- const changes = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- changes.push({
- name: `${response[key]._number}: ${response[key].subject}`,
- value: response[key]._number,
- });
- }
- this._recentChanges = changes;
- return this._recentChanges;
- });
- }
-
- _getRecentChanges() {
- if (this._recentChanges) {
- return Promise.resolve(this._recentChanges);
- }
- return this.fetchRecentChanges();
- }
-
- _getChangeSuggestions(input) {
- return this._getRecentChanges().then(changes =>
- this._filterChanges(input, changes));
- }
-
- _filterChanges(input, changes) {
- return changes.filter(change => change.name.includes(input) &&
- change.value !== this.changeNumber);
- }
-
- _displayParentOption(rebaseOnCurrent, hasParent) {
- return hasParent && rebaseOnCurrent;
- }
-
- _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) {
- return hasParent && !rebaseOnCurrent;
- }
-
- _displayTipOption(rebaseOnCurrent, hasParent) {
- return !(!rebaseOnCurrent && !hasParent);
- }
-
- /**
- * There is a subtle but important difference between setting the base to an
- * empty string and omitting it entirely from the payload. An empty string
- * implies that the parent should be cleared and the change should be
- * rebased on top of the target branch. Leaving out the base implies that it
- * should be rebased on top of its current parent.
- */
- _getSelectedBase() {
- if (this.$.rebaseOnParentInput.checked) { return null; }
- if (this.$.rebaseOnTipInput.checked) { return ''; }
- // Change numbers will have their description appended by the
- // autocomplete.
- return this._text.split(':')[0];
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm',
- {detail: {base: this._getSelectedBase()}}));
- this._text = '';
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('cancel'));
- this._text = '';
- }
-
- _handleRebaseOnOther() {
- this.$.parentInput.focus();
- }
-
- _handleEnterChangeNumberClick() {
- this.$.rebaseOnOtherInput.checked = true;
- }
-
- /**
- * Sets the default radio button based on the state of the app and
- * the corresponding value to be submitted.
- */
- _updateSelectedOption(rebaseOnCurrent, hasParent) {
- // Polymer 2: check for undefined
- if ([rebaseOnCurrent, hasParent].some(arg => arg === undefined)) {
- return;
- }
-
- if (this._displayParentOption(rebaseOnCurrent, hasParent)) {
- this.$.rebaseOnParentInput.checked = true;
- } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) {
- this.$.rebaseOnTipInput.checked = true;
- } else {
- this.$.rebaseOnOtherInput.checked = true;
- }
- }
+ },
+ _recentChanges: Array,
+ };
}
- customElements.define(GrConfirmRebaseDialog.is, GrConfirmRebaseDialog);
-})();
+ static get observers() {
+ return [
+ '_updateSelectedOption(rebaseOnCurrent, hasParent)',
+ ];
+ }
+
+ // This is called by gr-change-actions every time the rebase dialog is
+ // re-opened. Unlike other autocompletes that make a request with each
+ // updated input, this one gets all recent changes once and then filters
+ // them by the input. The query is re-run each time the dialog is opened
+ // in case there are new/updated changes in the generic query since the
+ // last time it was run.
+ fetchRecentChanges() {
+ return this.$.restAPI.getChanges(null, `is:open -age:90d`)
+ .then(response => {
+ const changes = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ changes.push({
+ name: `${response[key]._number}: ${response[key].subject}`,
+ value: response[key]._number,
+ });
+ }
+ this._recentChanges = changes;
+ return this._recentChanges;
+ });
+ }
+
+ _getRecentChanges() {
+ if (this._recentChanges) {
+ return Promise.resolve(this._recentChanges);
+ }
+ return this.fetchRecentChanges();
+ }
+
+ _getChangeSuggestions(input) {
+ return this._getRecentChanges().then(changes =>
+ this._filterChanges(input, changes));
+ }
+
+ _filterChanges(input, changes) {
+ return changes.filter(change => change.name.includes(input) &&
+ change.value !== this.changeNumber);
+ }
+
+ _displayParentOption(rebaseOnCurrent, hasParent) {
+ return hasParent && rebaseOnCurrent;
+ }
+
+ _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) {
+ return hasParent && !rebaseOnCurrent;
+ }
+
+ _displayTipOption(rebaseOnCurrent, hasParent) {
+ return !(!rebaseOnCurrent && !hasParent);
+ }
+
+ /**
+ * There is a subtle but important difference between setting the base to an
+ * empty string and omitting it entirely from the payload. An empty string
+ * implies that the parent should be cleared and the change should be
+ * rebased on top of the target branch. Leaving out the base implies that it
+ * should be rebased on top of its current parent.
+ */
+ _getSelectedBase() {
+ if (this.$.rebaseOnParentInput.checked) { return null; }
+ if (this.$.rebaseOnTipInput.checked) { return ''; }
+ // Change numbers will have their description appended by the
+ // autocomplete.
+ return this._text.split(':')[0];
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('confirm',
+ {detail: {base: this._getSelectedBase()}}));
+ this._text = '';
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('cancel'));
+ this._text = '';
+ }
+
+ _handleRebaseOnOther() {
+ this.$.parentInput.focus();
+ }
+
+ _handleEnterChangeNumberClick() {
+ this.$.rebaseOnOtherInput.checked = true;
+ }
+
+ /**
+ * Sets the default radio button based on the state of the app and
+ * the corresponding value to be submitted.
+ */
+ _updateSelectedOption(rebaseOnCurrent, hasParent) {
+ // Polymer 2: check for undefined
+ if ([rebaseOnCurrent, hasParent].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (this._displayParentOption(rebaseOnCurrent, hasParent)) {
+ this.$.rebaseOnParentInput.checked = true;
+ } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) {
+ this.$.rebaseOnTipInput.checked = true;
+ } else {
+ this.$.rebaseOnOtherInput.checked = true;
+ }
+ }
+}
+
+customElements.define(GrConfirmRebaseDialog.is, GrConfirmRebaseDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
index cf2721a..20872bc 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
@@ -1,28 +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="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.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-confirm-rebase-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -50,70 +44,43 @@
margin: var(--spacing-m) 0;
}
</style>
- <gr-dialog
- id="confirmDialog"
- confirm-label="Rebase"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog id="confirmDialog" confirm-label="Rebase" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">Confirm rebase</div>
<div class="main" slot="main">
- <div id="rebaseOnParent" class="rebaseOption"
- hidden$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]">
- <input id="rebaseOnParentInput"
- name="rebaseOptions"
- type="radio"
- on-click="_handleRebaseOnParent">
+ <div id="rebaseOnParent" class="rebaseOption" hidden\$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]">
+ <input id="rebaseOnParentInput" name="rebaseOptions" type="radio" on-click="_handleRebaseOnParent">
<label id="rebaseOnParentLabel" for="rebaseOnParentInput">
Rebase on parent change
</label>
</div>
- <div id="parentUpToDateMsg" class="message"
- hidden$="[[!_displayParentUpToDateMsg(rebaseOnCurrent, hasParent)]]">
+ <div id="parentUpToDateMsg" class="message" hidden\$="[[!_displayParentUpToDateMsg(rebaseOnCurrent, hasParent)]]">
This change is up to date with its parent.
</div>
- <div id="rebaseOnTip" class="rebaseOption"
- hidden$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]">
- <input id="rebaseOnTipInput"
- name="rebaseOptions"
- type="radio"
- disabled$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"
- on-click="_handleRebaseOnTip">
+ <div id="rebaseOnTip" class="rebaseOption" hidden\$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]">
+ <input id="rebaseOnTipInput" name="rebaseOptions" type="radio" disabled\$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]" on-click="_handleRebaseOnTip">
<label id="rebaseOnTipLabel" for="rebaseOnTipInput">
Rebase on top of the [[branch]]
- branch<span hidden$="[[!hasParent]]">
+ branch<span hidden\$="[[!hasParent]]">
(breaks relation chain)
</span>
</label>
</div>
- <div id="tipUpToDateMsg" class="message"
- hidden$="[[_displayTipOption(rebaseOnCurrent, hasParent)]]">
+ <div id="tipUpToDateMsg" class="message" hidden\$="[[_displayTipOption(rebaseOnCurrent, hasParent)]]">
Change is up to date with the target branch already ([[branch]])
</div>
<div id="rebaseOnOther" class="rebaseOption">
- <input id="rebaseOnOtherInput"
- name="rebaseOptions"
- type="radio"
- on-click="_handleRebaseOnOther">
+ <input id="rebaseOnOtherInput" name="rebaseOptions" type="radio" on-click="_handleRebaseOnOther">
<label id="rebaseOnOtherLabel" for="rebaseOnOtherInput">
- Rebase on a specific change, ref, or commit <span hidden$="[[!hasParent]]">
+ Rebase on a specific change, ref, or commit <span hidden\$="[[!hasParent]]">
(breaks relation chain)
</span>
</label>
</div>
<div class="parentRevisionContainer">
- <gr-autocomplete
- id="parentInput"
- query="[[_query]]"
- no-debounce
- text="{{_text}}"
- on-click="_handleEnterChangeNumberClick"
- allow-non-suggested-values
- placeholder="Change number, ref, or commit hash">
+ <gr-autocomplete id="parentInput" query="[[_query]]" no-debounce="" text="{{_text}}" on-click="_handleEnterChangeNumberClick" allow-non-suggested-values="" placeholder="Change number, ref, or commit hash">
</gr-autocomplete>
</div>
</div>
</gr-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-rebase-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index fe42cac..d715fbe3 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-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-confirm-rebase-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-confirm-rebase-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-confirm-rebase-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-rebase-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,167 +40,169 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-rebase-dialog 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-confirm-rebase-dialog.js';
+suite('gr-confirm-rebase-dialog tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('controls with parent and rebase on current available', () => {
+ element.rebaseOnCurrent = true;
+ element.hasParent = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.rebaseOnParentInput.checked);
+ assert.isFalse(element.$.rebaseOnParent.hasAttribute('hidden'));
+ assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+ assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
+ assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ });
+
+ test('controls with parent rebase on current not available', () => {
+ element.rebaseOnCurrent = false;
+ element.hasParent = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.rebaseOnTipInput.checked);
+ assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
+ assert.isFalse(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+ assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
+ assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ });
+
+ test('controls without parent and rebase on current available', () => {
+ element.rebaseOnCurrent = true;
+ element.hasParent = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.rebaseOnTipInput.checked);
+ assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
+ assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+ assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
+ assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ });
+
+ test('controls without parent rebase on current not available', () => {
+ element.rebaseOnCurrent = false;
+ element.hasParent = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.rebaseOnOtherInput.checked);
+ assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
+ assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
+ assert.isTrue(element.$.rebaseOnTip.hasAttribute('hidden'));
+ assert.isFalse(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ });
+
+ test('input cleared on cancel or submit', () => {
+ element._text = '123';
+ element.$.confirmDialog.fire('confirm');
+ assert.equal(element._text, '');
+
+ element._text = '123';
+ element.$.confirmDialog.fire('cancel');
+ assert.equal(element._text, '');
+ });
+
+ test('_getSelectedBase', () => {
+ element._text = '5fab321c';
+ element.$.rebaseOnParentInput.checked = true;
+ assert.equal(element._getSelectedBase(), null);
+ element.$.rebaseOnParentInput.checked = false;
+ element.$.rebaseOnTipInput.checked = true;
+ assert.equal(element._getSelectedBase(), '');
+ element.$.rebaseOnTipInput.checked = false;
+ assert.equal(element._getSelectedBase(), element._text);
+ element._text = '101: Test';
+ assert.equal(element._getSelectedBase(), '101');
+ });
+
+ suite('parent suggestions', () => {
+ let recentChanges;
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ recentChanges = [
+ {
+ name: '123: my first awesome change',
+ value: 123,
+ },
+ {
+ name: '124: my second awesome change',
+ value: 124,
+ },
+ {
+ name: '245: my third awesome change',
+ value: 245,
+ },
+ ];
+
+ sandbox.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve(
+ [
+ {
+ _number: 123,
+ subject: 'my first awesome change',
+ },
+ {
+ _number: 124,
+ subject: 'my second awesome change',
+ },
+ {
+ _number: 245,
+ subject: 'my third awesome change',
+ },
+ ]
+ ));
});
- teardown(() => {
- sandbox.restore();
+ test('_getRecentChanges', () => {
+ sandbox.spy(element, '_getRecentChanges');
+ return element._getRecentChanges()
+ .then(() => {
+ assert.deepEqual(element._recentChanges, recentChanges);
+ assert.equal(element.$.restAPI.getChanges.callCount, 1);
+ // When called a second time, should not re-request recent changes.
+ element._getRecentChanges();
+ })
+ .then(() => {
+ assert.equal(element._getRecentChanges.callCount, 2);
+ assert.equal(element.$.restAPI.getChanges.callCount, 1);
+ });
});
- test('controls with parent and rebase on current available', () => {
- element.rebaseOnCurrent = true;
- element.hasParent = true;
- flushAsynchronousOperations();
- assert.isTrue(element.$.rebaseOnParentInput.checked);
- assert.isFalse(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
+ test('_filterChanges', () => {
+ assert.equal(element._filterChanges('123', recentChanges).length, 1);
+ assert.equal(element._filterChanges('12', recentChanges).length, 2);
+ assert.equal(element._filterChanges('awesome', recentChanges).length,
+ 3);
+ assert.equal(element._filterChanges('third', recentChanges).length,
+ 1);
+
+ element.changeNumber = 123;
+ assert.equal(element._filterChanges('123', recentChanges).length, 0);
+ assert.equal(element._filterChanges('124', recentChanges).length, 1);
+ assert.equal(element._filterChanges('awesome', recentChanges).length,
+ 2);
});
- test('controls with parent rebase on current not available', () => {
- element.rebaseOnCurrent = false;
- element.hasParent = true;
- flushAsynchronousOperations();
- assert.isTrue(element.$.rebaseOnTipInput.checked);
- assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isFalse(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
- });
-
- test('controls without parent and rebase on current available', () => {
- element.rebaseOnCurrent = true;
- element.hasParent = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.rebaseOnTipInput.checked);
- assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden'));
- });
-
- test('controls without parent rebase on current not available', () => {
- element.rebaseOnCurrent = false;
- element.hasParent = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.rebaseOnOtherInput.checked);
- assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden'));
- assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden'));
- assert.isTrue(element.$.rebaseOnTip.hasAttribute('hidden'));
- assert.isFalse(element.$.tipUpToDateMsg.hasAttribute('hidden'));
- });
-
- test('input cleared on cancel or submit', () => {
- element._text = '123';
- element.$.confirmDialog.fire('confirm');
- assert.equal(element._text, '');
-
- element._text = '123';
- element.$.confirmDialog.fire('cancel');
- assert.equal(element._text, '');
- });
-
- test('_getSelectedBase', () => {
- element._text = '5fab321c';
- element.$.rebaseOnParentInput.checked = true;
- assert.equal(element._getSelectedBase(), null);
- element.$.rebaseOnParentInput.checked = false;
- element.$.rebaseOnTipInput.checked = true;
- assert.equal(element._getSelectedBase(), '');
- element.$.rebaseOnTipInput.checked = false;
- assert.equal(element._getSelectedBase(), element._text);
- element._text = '101: Test';
- assert.equal(element._getSelectedBase(), '101');
- });
-
- suite('parent suggestions', () => {
- let recentChanges;
- setup(() => {
- recentChanges = [
- {
- name: '123: my first awesome change',
- value: 123,
- },
- {
- name: '124: my second awesome change',
- value: 124,
- },
- {
- name: '245: my third awesome change',
- value: 245,
- },
- ];
-
- sandbox.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve(
- [
- {
- _number: 123,
- subject: 'my first awesome change',
- },
- {
- _number: 124,
- subject: 'my second awesome change',
- },
- {
- _number: 245,
- subject: 'my third awesome change',
- },
- ]
- ));
- });
-
- test('_getRecentChanges', () => {
- sandbox.spy(element, '_getRecentChanges');
- return element._getRecentChanges()
- .then(() => {
- assert.deepEqual(element._recentChanges, recentChanges);
- assert.equal(element.$.restAPI.getChanges.callCount, 1);
- // When called a second time, should not re-request recent changes.
- element._getRecentChanges();
- })
- .then(() => {
- assert.equal(element._getRecentChanges.callCount, 2);
- assert.equal(element.$.restAPI.getChanges.callCount, 1);
- });
- });
-
- test('_filterChanges', () => {
- assert.equal(element._filterChanges('123', recentChanges).length, 1);
- assert.equal(element._filterChanges('12', recentChanges).length, 2);
- assert.equal(element._filterChanges('awesome', recentChanges).length,
- 3);
- assert.equal(element._filterChanges('third', recentChanges).length,
- 1);
-
- element.changeNumber = 123;
- assert.equal(element._filterChanges('123', recentChanges).length, 0);
- assert.equal(element._filterChanges('124', recentChanges).length, 1);
- assert.equal(element._filterChanges('awesome', recentChanges).length,
- 2);
- });
-
- test('input text change triggers function', () => {
- sandbox.spy(element, '_getRecentChanges');
- element.$.parentInput.noDebounce = true;
- MockInteractions.pressAndReleaseKeyOn(
- element.$.parentInput.$.input,
- 13,
- null,
- 'enter');
- element._text = '1';
- assert.isTrue(element._getRecentChanges.calledOnce);
- element._text = '12';
- assert.isTrue(element._getRecentChanges.calledTwice);
- });
+ test('input text change triggers function', () => {
+ sandbox.spy(element, '_getRecentChanges');
+ element.$.parentInput.noDebounce = true;
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.parentInput.$.input,
+ 13,
+ null,
+ 'enter');
+ element._text = '1';
+ assert.isTrue(element._getRecentChanges.calledOnce);
+ element._text = '12';
+ assert.isTrue(element._getRecentChanges.calledTwice);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 05660bf..9bc0f8d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -14,184 +14,196 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const ERR_COMMIT_NOT_FOUND =
- 'Unable to find the commit hash of this change.';
- const CHANGE_SUBJECT_LIMIT = 50;
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.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-confirm-revert-dialog_html.js';
- // TODO(dhruvsri): clean up repeated definitions after moving to js modules
- const REVERT_TYPES = {
- REVERT_SINGLE_CHANGE: 1,
- REVERT_SUBMISSION: 2,
- };
+const ERR_COMMIT_NOT_FOUND =
+ 'Unable to find the commit hash of this change.';
+const CHANGE_SUBJECT_LIMIT = 50;
+
+// TODO(dhruvsri): clean up repeated definitions after moving to js modules
+const REVERT_TYPES = {
+ REVERT_SINGLE_CHANGE: 1,
+ REVERT_SUBMISSION: 2,
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmRevertDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-revert-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmRevertDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-revert-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- /* The revert message updated by the user
- The default value is set by the dialog */
- _message: String,
- _revertType: {
- type: Number,
- value: REVERT_TYPES.REVERT_SINGLE_CHANGE,
- },
- _showRevertSubmission: {
- type: Boolean,
- value: false,
- },
- _changesCount: Number,
- _showErrorMessage: {
- type: Boolean,
- value: false,
- },
- /* store the default revert messages per revert type so that we can
- check if user has edited the revert message or not
- Set when populate() is called */
- _originalRevertMessages: {
- type: Array,
- value() { return []; },
- },
- // Store the actual messages that the user has edited
- _revertMessages: {
- type: Array,
- value() { return []; },
- },
- };
- }
-
- _computeIfSingleRevert(revertType) {
- return revertType === REVERT_TYPES.REVERT_SINGLE_CHANGE;
- }
-
- _computeIfRevertSubmission(revertType) {
- return revertType === REVERT_TYPES.REVERT_SUBMISSION;
- }
-
- _modifyRevertMsg(change, commitMessage, message) {
- return this.$.jsAPI.modifyRevertMsg(change,
- message, commitMessage);
- }
-
- populate(change, commitMessage, changes) {
- this._changesCount = changes.length;
- // The option to revert a single change is always available
- this._populateRevertSingleChangeMessage(
- change, commitMessage, change.current_revision);
- this._populateRevertSubmissionMessage(change, changes, commitMessage);
- }
-
- _populateRevertSingleChangeMessage(change, commitMessage, commitHash) {
- // Figure out what the revert title should be.
- const originalTitle = (commitMessage || '').split('\n')[0];
- const revertTitle = `Revert "${originalTitle}"`;
- if (!commitHash) {
- this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
- return;
- }
- const revertCommitText = `This reverts commit ${commitHash}.`;
-
- this._message = `${revertTitle}\n\n${revertCommitText}\n\n` +
- `Reason for revert: <INSERT REASONING HERE>\n`;
- // This is to give plugins a chance to update message
- this._message = this._modifyRevertMsg(change, commitMessage,
- this._message);
- this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
- this._showRevertSubmission = false;
- this._revertMessages[this._revertType] = this._message;
- this._originalRevertMessages[this._revertType] = this._message;
- }
-
- _getTrimmedChangeSubject(subject) {
- if (!subject) return '';
- if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
- return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
- }
-
- _modifyRevertSubmissionMsg(change, msg, commitMessage) {
- return this.$.jsAPI.modifyRevertSubmissionMsg(change, msg,
- commitMessage);
- }
-
- _populateRevertSubmissionMessage(change, changes, commitMessage) {
- // Follow the same convention of the revert
- const commitHash = change.current_revision;
- if (!commitHash) {
- this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
- return;
- }
- if (!changes || changes.length <= 1) return;
- const submissionId = change.submission_id;
- const revertTitle = 'Revert submission ' + submissionId;
- this._message = revertTitle + '\n\n' + 'Reason for revert: <INSERT ' +
- 'REASONING HERE>\n';
- this._message += 'Reverted Changes:\n';
- changes.forEach(change => {
- this._message += change.change_id.substring(0, 10) + ':'
- + this._getTrimmedChangeSubject(change.subject) + '\n';
- });
- this._message = this._modifyRevertSubmissionMsg(change, this._message,
- commitMessage);
- this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
- this._revertMessages[this._revertType] = this._message;
- this._originalRevertMessages[this._revertType] = this._message;
- this._showRevertSubmission = true;
- }
-
- _handleRevertSingleChangeClicked() {
- this._showErrorMessage = false;
- this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION] = this._message;
- this._message = this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE];
- this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
- }
-
- _handleRevertSubmissionClicked() {
- this._showErrorMessage = false;
- this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
- this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE] = this._message;
- this._message = this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION];
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- if (this._message === this._originalRevertMessages[this._revertType]) {
- this._showErrorMessage = true;
- return;
- }
- this.fire('confirm', {revertType: this._revertType,
- message: this._message}, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', {revertType: this._revertType},
- {bubbles: false});
- }
+ static get properties() {
+ return {
+ /* The revert message updated by the user
+ The default value is set by the dialog */
+ _message: String,
+ _revertType: {
+ type: Number,
+ value: REVERT_TYPES.REVERT_SINGLE_CHANGE,
+ },
+ _showRevertSubmission: {
+ type: Boolean,
+ value: false,
+ },
+ _changesCount: Number,
+ _showErrorMessage: {
+ type: Boolean,
+ value: false,
+ },
+ /* store the default revert messages per revert type so that we can
+ check if user has edited the revert message or not
+ Set when populate() is called */
+ _originalRevertMessages: {
+ type: Array,
+ value() { return []; },
+ },
+ // Store the actual messages that the user has edited
+ _revertMessages: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrConfirmRevertDialog.is, GrConfirmRevertDialog);
-})();
+ _computeIfSingleRevert(revertType) {
+ return revertType === REVERT_TYPES.REVERT_SINGLE_CHANGE;
+ }
+
+ _computeIfRevertSubmission(revertType) {
+ return revertType === REVERT_TYPES.REVERT_SUBMISSION;
+ }
+
+ _modifyRevertMsg(change, commitMessage, message) {
+ return this.$.jsAPI.modifyRevertMsg(change,
+ message, commitMessage);
+ }
+
+ populate(change, commitMessage, changes) {
+ this._changesCount = changes.length;
+ // The option to revert a single change is always available
+ this._populateRevertSingleChangeMessage(
+ change, commitMessage, change.current_revision);
+ this._populateRevertSubmissionMessage(change, changes, commitMessage);
+ }
+
+ _populateRevertSingleChangeMessage(change, commitMessage, commitHash) {
+ // Figure out what the revert title should be.
+ const originalTitle = (commitMessage || '').split('\n')[0];
+ const revertTitle = `Revert "${originalTitle}"`;
+ if (!commitHash) {
+ this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
+ return;
+ }
+ const revertCommitText = `This reverts commit ${commitHash}.`;
+
+ this._message = `${revertTitle}\n\n${revertCommitText}\n\n` +
+ `Reason for revert: <INSERT REASONING HERE>\n`;
+ // This is to give plugins a chance to update message
+ this._message = this._modifyRevertMsg(change, commitMessage,
+ this._message);
+ this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
+ this._showRevertSubmission = false;
+ this._revertMessages[this._revertType] = this._message;
+ this._originalRevertMessages[this._revertType] = this._message;
+ }
+
+ _getTrimmedChangeSubject(subject) {
+ if (!subject) return '';
+ if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
+ return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
+ }
+
+ _modifyRevertSubmissionMsg(change, msg, commitMessage) {
+ return this.$.jsAPI.modifyRevertSubmissionMsg(change, msg,
+ commitMessage);
+ }
+
+ _populateRevertSubmissionMessage(change, changes, commitMessage) {
+ // Follow the same convention of the revert
+ const commitHash = change.current_revision;
+ if (!commitHash) {
+ this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
+ return;
+ }
+ if (!changes || changes.length <= 1) return;
+ const submissionId = change.submission_id;
+ const revertTitle = 'Revert submission ' + submissionId;
+ this._message = revertTitle + '\n\n' + 'Reason for revert: <INSERT ' +
+ 'REASONING HERE>\n';
+ this._message += 'Reverted Changes:\n';
+ changes.forEach(change => {
+ this._message += change.change_id.substring(0, 10) + ':'
+ + this._getTrimmedChangeSubject(change.subject) + '\n';
+ });
+ this._message = this._modifyRevertSubmissionMsg(change, this._message,
+ commitMessage);
+ this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
+ this._revertMessages[this._revertType] = this._message;
+ this._originalRevertMessages[this._revertType] = this._message;
+ this._showRevertSubmission = true;
+ }
+
+ _handleRevertSingleChangeClicked() {
+ this._showErrorMessage = false;
+ this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION] = this._message;
+ this._message = this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE];
+ this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE;
+ }
+
+ _handleRevertSubmissionClicked() {
+ this._showErrorMessage = false;
+ this._revertType = REVERT_TYPES.REVERT_SUBMISSION;
+ this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE] = this._message;
+ this._message = this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION];
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._message === this._originalRevertMessages[this._revertType]) {
+ this._showErrorMessage = true;
+ return;
+ }
+ this.fire('confirm', {revertType: this._revertType,
+ message: this._message}, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', {revertType: this._revertType},
+ {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmRevertDialog.is, GrConfirmRevertDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
index 144cf20..3f293cf 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
@@ -1,30 +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/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-confirm-revert-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -56,54 +48,34 @@
margin-bottom: var(--spacing-m);
}
</style>
- <gr-dialog
- confirm-label="Revert"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Revert" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">
Revert Merged Change
</div>
<div class="main" slot="main">
- <div class="error" hidden$="[[!_showErrorMessage]]">
+ <div class="error" hidden\$="[[!_showErrorMessage]]">
<span> A reason is required </span>
</div>
<template is="dom-if" if="[[_showRevertSubmission]]">
<div class="revertSubmissionLayout">
- <input
- name="revertOptions"
- type="radio"
- id="revertSingleChange"
- on-change="_handleRevertSingleChangeClicked"
- checked="[[_computeIfSingleRevert(_revertType)]]">
+ <input name="revertOptions" type="radio" id="revertSingleChange" on-change="_handleRevertSingleChangeClicked" checked="[[_computeIfSingleRevert(_revertType)]]">
<label for="revertSingleChange" class="label revertSingleChange">
Revert single change
</label>
</div>
<div class="revertSubmissionLayout">
- <input
- name="revertOptions"
- type="radio"
- id="revertSubmission"
- on-change="_handleRevertSubmissionClicked"
- checked="[[_computeIfRevertSubmission(_revertType)]]">
+ <input name="revertOptions" type="radio" id="revertSubmission" on-change="_handleRevertSubmissionClicked" checked="[[_computeIfRevertSubmission(_revertType)]]">
<label for="revertSubmission" class="label revertSubmission">
Revert entire submission ([[_changesCount]] Changes)
</label>
- </template>
+ </div></template>
<gr-endpoint-decorator name="confirm-revert-change">
<label for="messageInput">
Revert Commit Message
</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- max-rows="15"
- bind-value="{{_message}}"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" max-rows="15" bind-value="{{_message}}"></iron-autogrow-textarea>
</gr-endpoint-decorator>
</div>
</gr-dialog>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-confirm-revert-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index 29886dd..1f8837b 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-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-confirm-revert-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-confirm-revert-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-confirm-revert-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-revert-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,70 +40,72 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-revert-dialog 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-confirm-revert-dialog.js';
+suite('gr-confirm-revert-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox =sinon.sandbox.create();
- });
-
- teardown(() => sandbox.restore());
-
- test('no match', () => {
- assert.isNotOk(element._message);
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- element._populateRevertSingleChangeMessage({},
- 'not a commitHash in sight', undefined);
- assert.isTrue(alertStub.calledOnce);
- });
-
- test('single line', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'one line commit\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert "one line commit"\n\n' +
- 'This reverts commit abcd123.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
- });
-
- test('multi line', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert "many lines"\n\n' +
- 'This reverts commit abcd123.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
- });
-
- test('issue above change id', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert "much lines"\n\n' +
- 'This reverts commit abcd123.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
- });
-
- test('revert a revert', () => {
- assert.isNotOk(element._message);
- element._populateRevertSingleChangeMessage({},
- 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert "Revert "one line commit""\n\n' +
- 'This reverts commit abcd123.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element._message, expected);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox =sinon.sandbox.create();
});
+
+ teardown(() => sandbox.restore());
+
+ test('no match', () => {
+ assert.isNotOk(element._message);
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+ element._populateRevertSingleChangeMessage({},
+ 'not a commitHash in sight', undefined);
+ assert.isTrue(alertStub.calledOnce);
+ });
+
+ test('single line', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'one line commit\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert "one line commit"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element._message, expected);
+ });
+
+ test('multi line', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert "many lines"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element._message, expected);
+ });
+
+ test('issue above change id', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert "much lines"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element._message, expected);
+ });
+
+ test('revert a revert', () => {
+ assert.isNotOk(element._message);
+ element._populateRevertSingleChangeMessage({},
+ 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert "Revert "one line commit""\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element._message, expected);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
index ae8dfa5..3cae44a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
@@ -14,87 +14,98 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
- const ERR_COMMIT_NOT_FOUND =
- 'Unable to find the commit hash of this change.';
- const CHANGE_SUBJECT_LIMIT = 50;
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../../styles/shared-styles.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-confirm-revert-submission-dialog_html.js';
+
+const ERR_COMMIT_NOT_FOUND =
+ 'Unable to find the commit hash of this change.';
+const CHANGE_SUBJECT_LIMIT = 50;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmRevertSubmissionDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-revert-submission-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmRevertSubmissionDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-revert-submission-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- message: String,
- commitMessage: String,
- };
- }
-
- _getTrimmedChangeSubject(subject) {
- if (!subject) return '';
- if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
- return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
- }
-
- _modifyRevertSubmissionMsg(change) {
- return this.$.jsAPI.modifyRevertSubmissionMsg(change,
- this.message, this.commitMessage);
- }
-
- _populateRevertSubmissionMessage(message, change, changes) {
- // Follow the same convention of the revert
- const commitHash = change.current_revision;
- if (!commitHash) {
- this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
- return;
- }
- const submissionId = change.submission_id;
- const revertTitle = 'Revert submission ' + submissionId;
- this.changes = changes;
- this.message = revertTitle + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- this.message += 'Reverted Changes:\n';
- changes = changes || [];
- changes.forEach(change => {
- this.message += change.change_id.substring(0, 10) + ': ' +
- this._getTrimmedChangeSubject(change.subject) + '\n';
- });
- this.message = this._modifyRevertSubmissionMsg(change);
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ static get properties() {
+ return {
+ message: String,
+ commitMessage: String,
+ };
}
- customElements.define(GrConfirmRevertSubmissionDialog.is,
- GrConfirmRevertSubmissionDialog);
-})();
+ _getTrimmedChangeSubject(subject) {
+ if (!subject) return '';
+ if (subject.length < CHANGE_SUBJECT_LIMIT) return subject;
+ return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...';
+ }
+
+ _modifyRevertSubmissionMsg(change) {
+ return this.$.jsAPI.modifyRevertSubmissionMsg(change,
+ this.message, this.commitMessage);
+ }
+
+ _populateRevertSubmissionMessage(message, change, changes) {
+ // Follow the same convention of the revert
+ const commitHash = change.current_revision;
+ if (!commitHash) {
+ this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND});
+ return;
+ }
+ const submissionId = change.submission_id;
+ const revertTitle = 'Revert submission ' + submissionId;
+ this.changes = changes;
+ this.message = revertTitle + '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ this.message += 'Reverted Changes:\n';
+ changes = changes || [];
+ changes.forEach(change => {
+ this.message += change.change_id.substring(0, 10) + ': ' +
+ this._getTrimmedChangeSubject(change.subject) + '\n';
+ });
+ this.message = this._modifyRevertSubmissionMsg(change);
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmRevertSubmissionDialog.is,
+ GrConfirmRevertSubmissionDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
index f2cfef8..a68920c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
@@ -1,29 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 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/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-confirm-revert-submission-dialog">
- <template>
+export const htmlTemplate = html`
<!-- TODO(taoalpha): move all shared styles to a style module. -->
<style include="shared-styles">
:host {
@@ -45,24 +38,14 @@
width: 73ch; /* Add a char to account for the border. */
}
</style>
- <gr-dialog
- confirm-label="Revert Submission"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Revert Submission" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">Revert Submission</div>
<div class="main" slot="main">
<label for="messageInput">
Revert Commit Message
</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- max-rows="15"
- bind-value="{{message}}"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea>
</div>
</gr-dialog>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-confirm-revert-submission-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
index 2513986..d84aa4d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-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-confirm-revert-submission-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-confirm-revert-submission-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-confirm-revert-submission-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-revert-submission-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,67 +41,69 @@
</template>
</test-fixture>
-<script>
- suite('gr-confirm-revert-submission-dialog 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-confirm-revert-submission-dialog.js';
+suite('gr-confirm-revert-submission-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox =sinon.sandbox.create();
- });
-
- teardown(() => sandbox.restore());
-
- test('no match', () => {
- assert.isNotOk(element.message);
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
- element._populateRevertSubmissionMessage(
- 'not a commitHash in sight'
- );
- assert.isTrue(alertStub.calledOnce);
- });
-
- test('single line', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'one line commit\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
-
- test('multi line', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
-
- test('issue above change id', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
-
- test('revert a revert', () => {
- assert.isNotOk(element.message);
- element._populateRevertSubmissionMessage(
- 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
- 'abcd123');
- const expected = 'Revert submission\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n';
- assert.equal(element.message, expected);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox =sinon.sandbox.create();
});
+
+ teardown(() => sandbox.restore());
+
+ test('no match', () => {
+ assert.isNotOk(element.message);
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+ element._populateRevertSubmissionMessage(
+ 'not a commitHash in sight'
+ );
+ assert.isTrue(alertStub.calledOnce);
+ });
+
+ test('single line', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'one line commit\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+
+ test('multi line', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+
+ test('issue above change id', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+
+ test('revert a revert', () => {
+ assert.isNotOk(element.message);
+ element._populateRevertSubmissionMessage(
+ 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ const expected = 'Revert submission\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index aa26681..037d53d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -14,67 +14,80 @@
* 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 GrConfirmSubmitDialog extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-confirm-submit-dialog'; }
+import '@polymer/iron-icon/iron-icon.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.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-confirm-submit-dialog_html.js';
+
+/** @extends Polymer.Element */
+class GrConfirmSubmitDialog extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-submit-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
+
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
+
+ static get properties() {
+ return {
/**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
+ * @type {{
+ * is_private: boolean,
+ * subject: string,
+ * }}
*/
+ change: Object,
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
/**
* @type {{
- * is_private: boolean,
- * subject: string,
+ * label: string,
* }}
*/
- change: Object,
-
- /**
- * @type {{
- * label: string,
- * }}
- */
- action: Object,
- };
- }
-
- resetFocus(e) {
- this.$.dialog.resetFocus();
- }
-
- _computeUnresolvedCommentsWarning(change) {
- const unresolvedCount = change.unresolved_comment_count;
- const plural = unresolvedCount > 1 ? 's' : '';
- return `Heads Up! ${unresolvedCount} unresolved comment${plural}.`;
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
- }
+ action: Object,
+ };
}
- customElements.define(GrConfirmSubmitDialog.is, GrConfirmSubmitDialog);
-})();
+ resetFocus(e) {
+ this.$.dialog.resetFocus();
+ }
+
+ _computeUnresolvedCommentsWarning(change) {
+ const unresolvedCount = change.unresolved_comment_count;
+ const plural = unresolvedCount > 1 ? 's' : '';
+ return `Heads Up! ${unresolvedCount} unresolved comment${plural}.`;
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
+ }
+}
+
+customElements.define(GrConfirmSubmitDialog.is, GrConfirmSubmitDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
index a845ed4..03e0f17 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
@@ -1,34 +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="/bower_components/iron-icon/iron-icon.html">
-
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-submit-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
#dialog {
min-width: 40em;
@@ -48,18 +36,13 @@
}
}
</style>
- <gr-dialog
- id="dialog"
- confirm-label="Continue"
- confirm-on-enter
- on-cancel="_handleCancelTap"
- on-confirm="_handleConfirmTap">
+ <gr-dialog id="dialog" confirm-label="Continue" confirm-on-enter="" on-cancel="_handleCancelTap" on-confirm="_handleConfirmTap">
<div class="header" slot="header">
[[action.label]]
</div>
<div class="main" slot="main">
<gr-endpoint-decorator name="confirm-submit-change">
- <p>Ready to submit “<strong>[[change.subject]]</strong>”?</p>
+ <p>Ready to submit “<strong>[[change.subject]]</strong>”?</p>
<template is="dom-if" if="[[change.is_private]]">
<p>
<iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon>
@@ -79,6 +62,4 @@
</div>
</gr-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-confirm-submit-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
index a5dffa8..5acbbde 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
@@ -19,17 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-submit-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"/>
-<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-confirm-submit-dialog.html">
+<script type="module" src="./gr-confirm-submit-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-confirm-submit-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,43 +42,45 @@
</template>
</test-fixture>
-<script>
- suite('gr-file-list-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-confirm-submit-dialog.js';
+suite('gr-file-list-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('display', () => {
- element.action = {label: 'my-label'};
- element.change = {subject: 'my-subject'};
- flushAsynchronousOperations();
- const header = element.shadowRoot
- .querySelector('.header');
- assert.equal(header.textContent.trim(), 'my-label');
-
- const message = element.shadowRoot
- .querySelector('.main p');
- assert.notEqual(message.textContent.length, 0);
- assert.notEqual(message.textContent.indexOf('my-subject'), -1);
- });
-
- test('_computeUnresolvedCommentsWarning', () => {
- const change = {unresolved_comment_count: 1};
- assert.equal(element._computeUnresolvedCommentsWarning(change),
- 'Heads Up! 1 unresolved comment.');
-
- const change2 = {unresolved_comment_count: 2};
- assert.equal(element._computeUnresolvedCommentsWarning(change2),
- 'Heads Up! 2 unresolved comments.');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('display', () => {
+ element.action = {label: 'my-label'};
+ element.change = {subject: 'my-subject'};
+ flushAsynchronousOperations();
+ const header = element.shadowRoot
+ .querySelector('.header');
+ assert.equal(header.textContent.trim(), 'my-label');
+
+ const message = element.shadowRoot
+ .querySelector('.main p');
+ assert.notEqual(message.textContent.length, 0);
+ assert.notEqual(message.textContent.indexOf('my-subject'), -1);
+ });
+
+ test('_computeUnresolvedCommentsWarning', () => {
+ const change = {unresolved_comment_count: 1};
+ assert.equal(element._computeUnresolvedCommentsWarning(change),
+ 'Heads Up! 1 unresolved comment.');
+
+ const change2 = {unresolved_comment_count: 2};
+ assert.equal(element._computeUnresolvedCommentsWarning(change2),
+ 'Heads Up! 2 unresolved comments.');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 17c6f50..1b6e521 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -14,214 +14,225 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-download-commands/gr-download-commands.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-download-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDownloadDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-download-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when the user presses the close button.
+ *
+ * @event close
*/
- class GrDownloadDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-download-dialog'; }
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
- static get properties() {
- return {
- /** @type {{ revisions: Array }} */
- change: Object,
- patchNum: String,
- /** @type {?} */
- config: Object,
+ static get properties() {
+ return {
+ /** @type {{ revisions: Array }} */
+ change: Object,
+ patchNum: String,
+ /** @type {?} */
+ config: Object,
- _schemes: {
- type: Array,
- value() { return []; },
- computed: '_computeSchemes(change, patchNum)',
- observer: '_schemesChanged',
- },
- _selectedScheme: String,
- };
- }
+ _schemes: {
+ type: Array,
+ value() { return []; },
+ computed: '_computeSchemes(change, patchNum)',
+ observer: '_schemesChanged',
+ },
+ _selectedScheme: String,
+ };
+ }
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
- focus() {
- if (this._schemes.length) {
- this.$.downloadCommands.focusOnCopy();
- } else {
- this.$.download.focus();
- }
- }
-
- getFocusStops() {
- const links = this.shadowRoot
- .querySelector('#archives').querySelectorAll('a');
- return {
- start: this.$.closeButton,
- end: links[links.length - 1],
- };
- }
-
- _computeDownloadCommands(change, patchNum, _selectedScheme) {
- let commandObj;
- if (!change) return [];
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum) &&
- rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) {
- commandObj = rev.fetch[_selectedScheme].commands;
- break;
- }
- }
- const commands = [];
- for (const title in commandObj) {
- if (!commandObj || !commandObj.hasOwnProperty(title)) { continue; }
- commands.push({
- title,
- command: commandObj[title],
- });
- }
- return commands;
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- *
- * @return {string}
- */
- _computeZipDownloadLink(change, patchNum) {
- return this._computeDownloadLink(change, patchNum, true);
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- *
- * @return {string}
- */
- _computeZipDownloadFilename(change, patchNum) {
- return this._computeDownloadFilename(change, patchNum, true);
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- * @param {boolean=} opt_zip
- *
- * @return {string} Not sure why there was a mismatch
- */
- _computeDownloadLink(change, patchNum, opt_zip) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return '';
- }
- return this.changeBaseURL(change.project, change._number, patchNum) +
- '/patch?' + (opt_zip ? 'zip' : 'download');
- }
-
- /**
- * @param {!Object} change
- * @param {number|string} patchNum
- * @param {boolean=} opt_zip
- *
- * @return {string}
- */
- _computeDownloadFilename(change, patchNum, opt_zip) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return '';
- }
-
- let shortRev = '';
- for (const rev in change.revisions) {
- if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
- shortRev = rev.substr(0, 7);
- break;
- }
- }
- return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64');
- }
-
- _computeHidePatchFile(change, patchNum) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return false;
- }
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- const parentLength = rev.commit && rev.commit.parents ?
- rev.commit.parents.length : 0;
- return parentLength == 0;
- }
- }
- return false;
- }
-
- _computeArchiveDownloadLink(change, patchNum, format) {
- // Polymer 2: check for undefined
- if ([change, patchNum, format].some(arg => arg === undefined)) {
- return '';
- }
- return this.changeBaseURL(change.project, change._number, patchNum) +
- '/archive?format=' + format;
- }
-
- _computeSchemes(change, patchNum) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return [];
- }
-
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- const fetch = rev.fetch;
- if (fetch) {
- return Object.keys(fetch).sort();
- }
- break;
- }
- }
- return [];
- }
-
- _computePatchSetQuantity(revisions) {
- if (!revisions) { return 0; }
- return Object.keys(revisions).length;
- }
-
- _handleCloseTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('close', null, {bubbles: false});
- }
-
- _schemesChanged(schemes) {
- if (schemes.length === 0) { return; }
- if (!schemes.includes(this._selectedScheme)) {
- this._selectedScheme = schemes.sort()[0];
- }
- }
-
- _computeShowDownloadCommands(schemes) {
- return schemes.length ? '' : 'hidden';
+ focus() {
+ if (this._schemes.length) {
+ this.$.downloadCommands.focusOnCopy();
+ } else {
+ this.$.download.focus();
}
}
- customElements.define(GrDownloadDialog.is, GrDownloadDialog);
-})();
+ getFocusStops() {
+ const links = this.shadowRoot
+ .querySelector('#archives').querySelectorAll('a');
+ return {
+ start: this.$.closeButton,
+ end: links[links.length - 1],
+ };
+ }
+
+ _computeDownloadCommands(change, patchNum, _selectedScheme) {
+ let commandObj;
+ if (!change) return [];
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum) &&
+ rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) {
+ commandObj = rev.fetch[_selectedScheme].commands;
+ break;
+ }
+ }
+ const commands = [];
+ for (const title in commandObj) {
+ if (!commandObj || !commandObj.hasOwnProperty(title)) { continue; }
+ commands.push({
+ title,
+ command: commandObj[title],
+ });
+ }
+ return commands;
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ *
+ * @return {string}
+ */
+ _computeZipDownloadLink(change, patchNum) {
+ return this._computeDownloadLink(change, patchNum, true);
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ *
+ * @return {string}
+ */
+ _computeZipDownloadFilename(change, patchNum) {
+ return this._computeDownloadFilename(change, patchNum, true);
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ * @param {boolean=} opt_zip
+ *
+ * @return {string} Not sure why there was a mismatch
+ */
+ _computeDownloadLink(change, patchNum, opt_zip) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return '';
+ }
+ return this.changeBaseURL(change.project, change._number, patchNum) +
+ '/patch?' + (opt_zip ? 'zip' : 'download');
+ }
+
+ /**
+ * @param {!Object} change
+ * @param {number|string} patchNum
+ * @param {boolean=} opt_zip
+ *
+ * @return {string}
+ */
+ _computeDownloadFilename(change, patchNum, opt_zip) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return '';
+ }
+
+ let shortRev = '';
+ for (const rev in change.revisions) {
+ if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
+ shortRev = rev.substr(0, 7);
+ break;
+ }
+ }
+ return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64');
+ }
+
+ _computeHidePatchFile(change, patchNum) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return false;
+ }
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ const parentLength = rev.commit && rev.commit.parents ?
+ rev.commit.parents.length : 0;
+ return parentLength == 0;
+ }
+ }
+ return false;
+ }
+
+ _computeArchiveDownloadLink(change, patchNum, format) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum, format].some(arg => arg === undefined)) {
+ return '';
+ }
+ return this.changeBaseURL(change.project, change._number, patchNum) +
+ '/archive?format=' + format;
+ }
+
+ _computeSchemes(change, patchNum) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return [];
+ }
+
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ const fetch = rev.fetch;
+ if (fetch) {
+ return Object.keys(fetch).sort();
+ }
+ break;
+ }
+ }
+ return [];
+ }
+
+ _computePatchSetQuantity(revisions) {
+ if (!revisions) { return 0; }
+ return Object.keys(revisions).length;
+ }
+
+ _handleCloseTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('close', null, {bubbles: false});
+ }
+
+ _schemesChanged(schemes) {
+ if (schemes.length === 0) { return; }
+ if (!schemes.includes(this._selectedScheme)) {
+ this._selectedScheme = schemes.sort()[0];
+ }
+ }
+
+ _computeShowDownloadCommands(schemes) {
+ return schemes.length ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrDownloadDialog.is, GrDownloadDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
index 4ddc876..324f9f4 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
@@ -1,30 +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/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
-
-<dom-module id="gr-download-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -77,37 +69,26 @@
Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]]
</h3>
</section>
- <section class$="[[_computeShowDownloadCommands(_schemes)]]">
- <gr-download-commands
- id="downloadCommands"
- commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]"
- schemes="[[_schemes]]"
- selected-scheme="{{_selectedScheme}}"></gr-download-commands>
+ <section class\$="[[_computeShowDownloadCommands(_schemes)]]">
+ <gr-download-commands id="downloadCommands" commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" schemes="[[_schemes]]" selected-scheme="{{_selectedScheme}}"></gr-download-commands>
</section>
<section class="flexContainer">
- <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]" hidden>
+ <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]">
<label>Patch file</label>
<div>
- <a
- id="download"
- href$="[[_computeDownloadLink(change, patchNum)]]"
- download>
+ <a id="download" href\$="[[_computeDownloadLink(change, patchNum)]]" download="">
[[_computeDownloadFilename(change, patchNum)]]
</a>
- <a
- href$="[[_computeZipDownloadLink(change, patchNum)]]"
- download>
+ <a href\$="[[_computeZipDownloadLink(change, patchNum)]]" download="">
[[_computeZipDownloadFilename(change, patchNum)]]
</a>
</div>
</div>
- <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden>
+ <div class="archivesContainer" hidden\$="[[!config.archives.length]]" hidden="">
<label>Archive</label>
<div id="archives" class="archives">
<template is="dom-repeat" items="[[config.archives]]" as="format">
- <a
- href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]"
- download>
+ <a href\$="[[_computeArchiveDownloadLink(change, patchNum, format)]]" download="">
[[format]]
</a>
</template>
@@ -116,11 +97,7 @@
</section>
<section class="footer">
<span class="closeButtonContainer">
- <gr-button id="closeButton"
- link
- on-click="_handleCloseTap">Close</gr-button>
+ <gr-button id="closeButton" link="" on-click="_handleCloseTap">Close</gr-button>
</span>
</section>
- </template>
- <script src="gr-download-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index a3755ba..8a55c21 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-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-download-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-download-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-download-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-download-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -41,184 +46,187 @@
</template>
</test-fixture>
-<script>
- function getChangeObject() {
- return {
- current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
- revisions: {
- '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
- _number: 1,
- commit: {
- parents: [],
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-download-dialog.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+function getChangeObject() {
+ return {
+ current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
+ revisions: {
+ '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
+ _number: 1,
+ commit: {
+ parents: [],
+ },
+ fetch: {
+ repo: {
+ commands: {
+ repo: 'repo download test-project 5/1',
+ },
},
- fetch: {
- repo: {
- commands: {
- repo: 'repo download test-project 5/1',
- },
+ ssh: {
+ commands: {
+ 'Checkout':
+ 'git fetch ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
+ 'Cherry Pick':
+ 'git fetch ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
+ 'Format Patch':
+ 'git fetch ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1 ' +
+ '&& git format-patch -1 --stdout FETCH_HEAD',
+ 'Pull':
+ 'git pull ' +
+ 'ssh://andybons@localhost:29418/test-project ' +
+ 'refs/changes/05/5/1',
},
- ssh: {
- commands: {
- 'Checkout':
- 'git fetch ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
- 'Cherry Pick':
- 'git fetch ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
- 'Format Patch':
- 'git fetch ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1 ' +
- '&& git format-patch -1 --stdout FETCH_HEAD',
- 'Pull':
- 'git pull ' +
- 'ssh://andybons@localhost:29418/test-project ' +
- 'refs/changes/05/5/1',
- },
- },
- http: {
- commands: {
- 'Checkout':
- 'git fetch ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
- 'Cherry Pick':
- 'git fetch ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
- 'Format Patch':
- 'git fetch ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1 && ' +
- 'git format-patch -1 --stdout FETCH_HEAD',
- 'Pull':
- 'git pull ' +
- 'http://andybons@localhost:8080/a/test-project ' +
- 'refs/changes/05/5/1',
- },
+ },
+ http: {
+ commands: {
+ 'Checkout':
+ 'git fetch ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1 && git checkout FETCH_HEAD',
+ 'Cherry Pick':
+ 'git fetch ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD',
+ 'Format Patch':
+ 'git fetch ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1 && ' +
+ 'git format-patch -1 --stdout FETCH_HEAD',
+ 'Pull':
+ 'git pull ' +
+ 'http://andybons@localhost:8080/a/test-project ' +
+ 'refs/changes/05/5/1',
},
},
},
},
- };
- }
+ },
+ };
+}
- function getChangeObjectNoFetch() {
- return {
- current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
- revisions: {
- '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
- _number: 1,
- commit: {
- parents: [],
- },
- fetch: {},
+function getChangeObjectNoFetch() {
+ return {
+ current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72',
+ revisions: {
+ '34685798fe548b6d17d1e8e5edc43a26d055cc72': {
+ _number: 1,
+ commit: {
+ parents: [],
},
+ fetch: {},
},
+ },
+ };
+}
+
+suite('gr-download-dialog', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+
+ element = fixture('basic');
+ element.patchNum = '1';
+ element.config = {
+ schemes: {
+ 'anonymous http': {},
+ 'http': {},
+ 'repo': {},
+ 'ssh': {},
+ },
+ archives: ['tgz', 'tar', 'tbz2', 'txz'],
};
- }
- suite('gr-download-dialog', async () => {
- await readyToTest();
- let element;
- let sandbox;
+ flushAsynchronousOperations();
+ });
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('anchors use download attribute', () => {
+ const anchors = Array.from(
+ dom(element.root).querySelectorAll('a'));
+ assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
+ });
+
+ suite('gr-download-dialog tests with no fetch options', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
-
- element = fixture('basic');
- element.patchNum = '1';
- element.config = {
- schemes: {
- 'anonymous http': {},
- 'http': {},
- 'repo': {},
- 'ssh': {},
- },
- archives: ['tgz', 'tar', 'tbz2', 'txz'],
- };
-
+ element.change = getChangeObjectNoFetch();
flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
- });
-
- test('anchors use download attribute', () => {
- const anchors = Array.from(
- Polymer.dom(element.root).querySelectorAll('a'));
- assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
- });
-
- suite('gr-download-dialog tests with no fetch options', () => {
- setup(() => {
- element.change = getChangeObjectNoFetch();
- flushAsynchronousOperations();
- });
-
- test('focuses on first download link if no copy links', () => {
- const focusStub = sandbox.stub(element.$.download, 'focus');
- element.focus();
- assert.isTrue(focusStub.called);
- focusStub.restore();
- });
- });
-
- suite('gr-download-dialog with fetch options', () => {
- setup(() => {
- element.change = getChangeObject();
- flushAsynchronousOperations();
- });
-
- test('focuses on first copy link', () => {
- const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy');
- element.focus();
- flushAsynchronousOperations();
- assert.isTrue(focusStub.called);
- focusStub.restore();
- });
-
- test('computed fields', () => {
- assert.equal(element._computeArchiveDownloadLink(
- {project: 'test/project', _number: 123}, 2, 'tgz'),
- '/changes/test%2Fproject~123/revisions/2/archive?format=tgz');
- });
-
- test('close event', done => {
- element.addEventListener('close', () => {
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('.closeButtonContainer gr-button'));
- });
- });
-
- test('_computeShowDownloadCommands', () => {
- assert.equal(element._computeShowDownloadCommands([]), 'hidden');
- assert.equal(element._computeShowDownloadCommands(['test']), '');
- });
-
- test('_computeHidePatchFile', () => {
- const patchNum = '1';
-
- const change1 = {
- revisions: {
- r1: {_number: 1, commit: {parents: []}},
- },
- };
- assert.isTrue(element._computeHidePatchFile(change1, patchNum));
-
- const change2 = {
- revisions: {
- r1: {_number: 1, commit: {parents: [
- {commit: 'p1'},
- ]}},
- },
- };
- assert.isFalse(element._computeHidePatchFile(change2, patchNum));
+ test('focuses on first download link if no copy links', () => {
+ const focusStub = sandbox.stub(element.$.download, 'focus');
+ element.focus();
+ assert.isTrue(focusStub.called);
+ focusStub.restore();
});
});
+
+ suite('gr-download-dialog with fetch options', () => {
+ setup(() => {
+ element.change = getChangeObject();
+ flushAsynchronousOperations();
+ });
+
+ test('focuses on first copy link', () => {
+ const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy');
+ element.focus();
+ flushAsynchronousOperations();
+ assert.isTrue(focusStub.called);
+ focusStub.restore();
+ });
+
+ test('computed fields', () => {
+ assert.equal(element._computeArchiveDownloadLink(
+ {project: 'test/project', _number: 123}, 2, 'tgz'),
+ '/changes/test%2Fproject~123/revisions/2/archive?format=tgz');
+ });
+
+ test('close event', done => {
+ element.addEventListener('close', () => {
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.closeButtonContainer gr-button'));
+ });
+ });
+
+ test('_computeShowDownloadCommands', () => {
+ assert.equal(element._computeShowDownloadCommands([]), 'hidden');
+ assert.equal(element._computeShowDownloadCommands(['test']), '');
+ });
+
+ test('_computeHidePatchFile', () => {
+ const patchNum = '1';
+
+ const change1 = {
+ revisions: {
+ r1: {_number: 1, commit: {parents: []}},
+ },
+ };
+ assert.isTrue(element._computeHidePatchFile(change1, patchNum));
+
+ const change2 = {
+ revisions: {
+ r1: {_number: 1, commit: {parents: [
+ {commit: 'p1'},
+ ]}},
+ },
+ };
+ assert.isFalse(element._computeHidePatchFile(change2, patchNum));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-constants.js b/polygerrit-ui/app/elements/change/gr-file-list-constants.js
index 8bdcf7a..0f93b52 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-constants.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-constants.js
@@ -1,31 +1,29 @@
-<!--
-@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
+ const GrFileListConstants = window.GrFileListConstants || {};
-http://www.apache.org/licenses/LICENSE-2.0
+ GrFileListConstants.FilesExpandedState = {
+ ALL: 'all',
+ NONE: 'none',
+ SOME: 'some',
+ };
-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';
-
- const GrFileListConstants = window.GrFileListConstants || {};
-
- GrFileListConstants.FilesExpandedState = {
- ALL: 'all',
- NONE: 'none',
- SOME: 'some',
- };
-
- window.GrFileListConstants = GrFileListConstants;
- })(window);
-</script>
+ window.GrFileListConstants = GrFileListConstants;
+})(window);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index 34e1cfa..eef436b 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -14,264 +14,286 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Maximum length for patch set descriptions.
- const PATCH_DESC_MAX_LENGTH = 500;
- const MERGED_STATUS = 'MERGED';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../diff/gr-diff-mode-selector/gr-diff-mode-selector.js';
+import '../../diff/gr-patch-range-select/gr-patch-range-select.js';
+import '../../edit/gr-edit-controls/gr-edit-controls.js';
+import '../../shared/gr-editable-label/gr-editable-label.js';
+import '../../shared/gr-linked-chip/gr-linked-chip.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../gr-file-list-constants.js';
+import '../gr-commit-info/gr-commit-info.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-file-list-header_html.js';
+
+// Maximum length for patch set descriptions.
+const PATCH_DESC_MAX_LENGTH = 500;
+const MERGED_STATUS = 'MERGED';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrFileListHeader extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-file-list-header'; }
+ /**
+ * @event expand-diffs
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * @event collapse-diffs
*/
- class GrFileListHeader extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-file-list-header'; }
- /**
- * @event expand-diffs
- */
- /**
- * @event collapse-diffs
- */
+ /**
+ * @event open-diff-prefs
+ */
- /**
- * @event open-diff-prefs
- */
+ /**
+ * @event open-included-in-dialog
+ */
- /**
- * @event open-included-in-dialog
- */
+ /**
+ * @event open-download-dialog
+ */
- /**
- * @event open-download-dialog
- */
+ /**
+ * @event open-upload-help-dialog
+ */
- /**
- * @event open-upload-help-dialog
- */
+ static get properties() {
+ return {
+ account: Object,
+ allPatchSets: Array,
+ /** @type {?} */
+ change: Object,
+ changeNum: String,
+ changeUrl: String,
+ changeComments: Object,
+ commitInfo: Object,
+ editMode: Boolean,
+ loggedIn: Boolean,
+ serverConfig: Object,
+ shownFileCount: Number,
+ diffPrefs: Object,
+ diffPrefsDisabled: Boolean,
+ diffViewMode: {
+ type: String,
+ notify: true,
+ },
+ patchNum: String,
+ basePatchNum: String,
+ filesExpanded: String,
+ // Caps the number of files that can be shown and have the 'show diffs' /
+ // 'hide diffs' buttons still be functional.
+ _maxFilesForBulkActions: {
+ type: Number,
+ readOnly: true,
+ value: 225,
+ },
+ _patchsetDescription: {
+ type: String,
+ value: '',
+ },
+ _descriptionReadOnly: {
+ type: Boolean,
+ computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
+ },
+ revisionInfo: Object,
+ };
+ }
- static get properties() {
- return {
- account: Object,
- allPatchSets: Array,
- /** @type {?} */
- change: Object,
- changeNum: String,
- changeUrl: String,
- changeComments: Object,
- commitInfo: Object,
- editMode: Boolean,
- loggedIn: Boolean,
- serverConfig: Object,
- shownFileCount: Number,
- diffPrefs: Object,
- diffPrefsDisabled: Boolean,
- diffViewMode: {
- type: String,
- notify: true,
- },
- patchNum: String,
- basePatchNum: String,
- filesExpanded: String,
- // Caps the number of files that can be shown and have the 'show diffs' /
- // 'hide diffs' buttons still be functional.
- _maxFilesForBulkActions: {
- type: Number,
- readOnly: true,
- value: 225,
- },
- _patchsetDescription: {
- type: String,
- value: '',
- },
- _descriptionReadOnly: {
- type: Boolean,
- computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
- },
- revisionInfo: Object,
- };
+ static get observers() {
+ return [
+ '_computePatchSetDescription(change, patchNum)',
+ ];
+ }
+
+ setDiffViewMode(mode) {
+ this.$.modeSelect.setMode(mode);
+ }
+
+ _expandAllDiffs() {
+ this._expanded = true;
+ this.fire('expand-diffs');
+ }
+
+ _collapseAllDiffs() {
+ this._expanded = false;
+ this.fire('collapse-diffs');
+ }
+
+ _computeExpandedClass(filesExpanded) {
+ const classes = [];
+ if (filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
+ classes.push('expanded');
+ }
+ if (filesExpanded === GrFileListConstants.FilesExpandedState.SOME ||
+ filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
+ classes.push('openFile');
+ }
+ return classes.join(' ');
+ }
+
+ _computeDescriptionPlaceholder(readOnly) {
+ return (readOnly ? 'No' : 'Add') + ' patchset description';
+ }
+
+ _computeDescriptionReadOnly(loggedIn, change, account) {
+ // Polymer 2: check for undefined
+ if ([loggedIn, change, account].some(arg => arg === undefined)) {
+ return undefined;
}
- static get observers() {
- return [
- '_computePatchSetDescription(change, patchNum)',
- ];
+ return !(loggedIn && (account._account_id === change.owner._account_id));
+ }
+
+ _computePatchSetDescription(change, patchNum) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum].some(arg => arg === undefined)) {
+ return;
}
- setDiffViewMode(mode) {
- this.$.modeSelect.setMode(mode);
- }
+ const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
+ this._patchsetDescription = (rev && rev.description) ?
+ rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
+ }
- _expandAllDiffs() {
- this._expanded = true;
- this.fire('expand-diffs');
- }
+ _handleDescriptionRemoved(e) {
+ return this._updateDescription('', e);
+ }
- _collapseAllDiffs() {
- this._expanded = false;
- this.fire('collapse-diffs');
- }
-
- _computeExpandedClass(filesExpanded) {
- const classes = [];
- if (filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
- classes.push('expanded');
+ /**
+ * @param {!Object} revisions The revisions object keyed by revision hashes
+ * @param {?Object} patchSet A revision already fetched from {revisions}
+ * @return {string|undefined} the SHA hash corresponding to the revision.
+ */
+ _getPatchsetHash(revisions, patchSet) {
+ for (const rev in revisions) {
+ if (revisions.hasOwnProperty(rev) &&
+ revisions[rev] === patchSet) {
+ return rev;
}
- if (filesExpanded === GrFileListConstants.FilesExpandedState.SOME ||
- filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
- classes.push('openFile');
- }
- return classes.join(' ');
- }
-
- _computeDescriptionPlaceholder(readOnly) {
- return (readOnly ? 'No' : 'Add') + ' patchset description';
- }
-
- _computeDescriptionReadOnly(loggedIn, change, account) {
- // Polymer 2: check for undefined
- if ([loggedIn, change, account].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return !(loggedIn && (account._account_id === change.owner._account_id));
- }
-
- _computePatchSetDescription(change, patchNum) {
- // Polymer 2: check for undefined
- if ([change, patchNum].some(arg => arg === undefined)) {
- return;
- }
-
- const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
- this._patchsetDescription = (rev && rev.description) ?
- rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
- }
-
- _handleDescriptionRemoved(e) {
- return this._updateDescription('', e);
- }
-
- /**
- * @param {!Object} revisions The revisions object keyed by revision hashes
- * @param {?Object} patchSet A revision already fetched from {revisions}
- * @return {string|undefined} the SHA hash corresponding to the revision.
- */
- _getPatchsetHash(revisions, patchSet) {
- for (const rev in revisions) {
- if (revisions.hasOwnProperty(rev) &&
- revisions[rev] === patchSet) {
- return rev;
- }
- }
- }
-
- _handleDescriptionChanged(e) {
- const desc = e.detail.trim();
- this._updateDescription(desc, e);
- }
-
- /**
- * Update the patchset description with the rest API.
- *
- * @param {string} desc
- * @param {?(Event|Node)} e
- * @return {!Promise}
- */
- _updateDescription(desc, e) {
- const target = Polymer.dom(e).rootTarget;
- if (target) { target.disabled = true; }
- const rev = this.getRevisionByPatchNum(this.change.revisions,
- this.patchNum);
- const sha = this._getPatchsetHash(this.change.revisions, rev);
- return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
- .then(res => {
- if (res.ok) {
- if (target) { target.disabled = false; }
- this.set(['change', 'revisions', sha, 'description'], desc);
- this._patchsetDescription = desc;
- }
- })
- .catch(err => {
- if (target) { target.disabled = false; }
- return;
- });
- }
-
- _computePrefsButtonHidden(prefs, diffPrefsDisabled) {
- return diffPrefsDisabled || !prefs;
- }
-
- _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
- return shownFileCount <= maxFilesForBulkActions;
- }
-
- _handlePatchChange(e) {
- const {basePatchNum, patchNum} = e.detail;
- if (this.patchNumEquals(basePatchNum, this.basePatchNum) &&
- this.patchNumEquals(patchNum, this.patchNum)) { return; }
- Gerrit.Nav.navigateToChange(this.change, patchNum, basePatchNum);
- }
-
- _handlePrefsTap(e) {
- e.preventDefault();
- this.fire('open-diff-prefs');
- }
-
- _handleIncludedInTap(e) {
- e.preventDefault();
- this.fire('open-included-in-dialog');
- }
-
- _handleDownloadTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('open-download-dialog', {bubbles: false}));
- }
-
- _computeEditModeClass(editMode) {
- return editMode ? 'editMode' : '';
- }
-
- _computePatchInfoClass(patchNum, allPatchSets) {
- const latestNum = this.computeLatestPatchNum(allPatchSets);
- if (this.patchNumEquals(patchNum, latestNum)) {
- return '';
- }
- return 'patchInfoOldPatchSet';
- }
-
- _hideIncludedIn(change) {
- return change && change.status === MERGED_STATUS ? '' : 'hide';
- }
-
- _handleUploadTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('open-upload-help-dialog', {bubbles: false}));
- }
-
- _computeUploadHelpContainerClass(change, account) {
- const changeIsMerged = change && change.status === MERGED_STATUS;
- const ownerId = change && change.owner && change.owner._account_id ?
- change.owner._account_id : null;
- const userId = account && account._account_id;
- const userIsOwner = ownerId && userId && ownerId === userId;
- const hideContainer = !userIsOwner || changeIsMerged;
- return 'uploadContainer desktop' + (hideContainer ? ' hide' : '');
}
}
- customElements.define(GrFileListHeader.is, GrFileListHeader);
-})();
+ _handleDescriptionChanged(e) {
+ const desc = e.detail.trim();
+ this._updateDescription(desc, e);
+ }
+
+ /**
+ * Update the patchset description with the rest API.
+ *
+ * @param {string} desc
+ * @param {?(Event|Node)} e
+ * @return {!Promise}
+ */
+ _updateDescription(desc, e) {
+ const target = dom(e).rootTarget;
+ if (target) { target.disabled = true; }
+ const rev = this.getRevisionByPatchNum(this.change.revisions,
+ this.patchNum);
+ const sha = this._getPatchsetHash(this.change.revisions, rev);
+ return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
+ .then(res => {
+ if (res.ok) {
+ if (target) { target.disabled = false; }
+ this.set(['change', 'revisions', sha, 'description'], desc);
+ this._patchsetDescription = desc;
+ }
+ })
+ .catch(err => {
+ if (target) { target.disabled = false; }
+ return;
+ });
+ }
+
+ _computePrefsButtonHidden(prefs, diffPrefsDisabled) {
+ return diffPrefsDisabled || !prefs;
+ }
+
+ _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
+ return shownFileCount <= maxFilesForBulkActions;
+ }
+
+ _handlePatchChange(e) {
+ const {basePatchNum, patchNum} = e.detail;
+ if (this.patchNumEquals(basePatchNum, this.basePatchNum) &&
+ this.patchNumEquals(patchNum, this.patchNum)) { return; }
+ Gerrit.Nav.navigateToChange(this.change, patchNum, basePatchNum);
+ }
+
+ _handlePrefsTap(e) {
+ e.preventDefault();
+ this.fire('open-diff-prefs');
+ }
+
+ _handleIncludedInTap(e) {
+ e.preventDefault();
+ this.fire('open-included-in-dialog');
+ }
+
+ _handleDownloadTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent('open-download-dialog', {bubbles: false}));
+ }
+
+ _computeEditModeClass(editMode) {
+ return editMode ? 'editMode' : '';
+ }
+
+ _computePatchInfoClass(patchNum, allPatchSets) {
+ const latestNum = this.computeLatestPatchNum(allPatchSets);
+ if (this.patchNumEquals(patchNum, latestNum)) {
+ return '';
+ }
+ return 'patchInfoOldPatchSet';
+ }
+
+ _hideIncludedIn(change) {
+ return change && change.status === MERGED_STATUS ? '' : 'hide';
+ }
+
+ _handleUploadTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent('open-upload-help-dialog', {bubbles: false}));
+ }
+
+ _computeUploadHelpContainerClass(change, account) {
+ const changeIsMerged = change && change.status === MERGED_STATUS;
+ const ownerId = change && change.owner && change.owner._account_id ?
+ change.owner._account_id : null;
+ const userId = account && account._account_id;
+ const userIsOwner = ownerId && userId && ownerId === userId;
+ const hideContainer = !userIsOwner || changeIsMerged;
+ return 'uploadContainer desktop' + (hideContainer ? ' hide' : '');
+ }
+}
+
+customElements.define(GrFileListHeader.is, GrFileListHeader);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
index 79f6c50..28cd645 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
@@ -1,39 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../diff/gr-diff-mode-selector/gr-diff-mode-selector.html">
-<link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html">
-<link rel="import" href="../../edit/gr-edit-controls/gr-edit-controls.html">
-<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
-<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../gr-file-list-constants.html">
-<link rel="import" href="../gr-commit-info/gr-commit-info.html">
-
-<dom-module id="gr-file-list-header">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.prefsButton {
float: right;
@@ -152,96 +135,49 @@
}
}
</style>
- <div class$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
+ <div class\$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]">
<div class="patchInfo-left">
<div class="patchInfoContent">
- <gr-patch-range-select
- id="rangeSelect"
- change-comments="[[changeComments]]"
- change-num="[[changeNum]]"
- patch-num="[[patchNum]]"
- base-patch-num="[[basePatchNum]]"
- available-patches="[[allPatchSets]]"
- revisions="[[change.revisions]]"
- revision-info="[[revisionInfo]]"
- on-patch-range-change="_handlePatchChange">
+ <gr-patch-range-select id="rangeSelect" change-comments="[[changeComments]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" base-patch-num="[[basePatchNum]]" available-patches="[[allPatchSets]]" revisions="[[change.revisions]]" revision-info="[[revisionInfo]]" on-patch-range-change="_handlePatchChange">
</gr-patch-range-select>
<span class="separator"></span>
- <gr-commit-info
- change="[[change]]"
- server-config="[[serverConfig]]"
- commit-info="[[commitInfo]]"></gr-commit-info>
+ <gr-commit-info change="[[change]]" server-config="[[serverConfig]]" commit-info="[[commitInfo]]"></gr-commit-info>
<span class="container latestPatchContainer">
<span class="separator"></span>
- <a href$="[[changeUrl]]">Go to latest patch set</a>
+ <a href\$="[[changeUrl]]">Go to latest patch set</a>
</span>
<span class="container descriptionContainer hideOnEdit">
<span class="separator"></span>
- <template
- is="dom-if"
- if="[[_patchsetDescription]]">
- <gr-linked-chip
- id="descriptionChip"
- text="[[_patchsetDescription]]"
- removable="[[!_descriptionReadOnly]]"
- on-remove="_handleDescriptionRemoved"></gr-linked-chip>
+ <template is="dom-if" if="[[_patchsetDescription]]">
+ <gr-linked-chip id="descriptionChip" text="[[_patchsetDescription]]" removable="[[!_descriptionReadOnly]]" on-remove="_handleDescriptionRemoved"></gr-linked-chip>
</template>
- <template
- is="dom-if"
- if="[[!_patchsetDescription]]">
- <gr-editable-label
- id="descriptionLabel"
- uppercase
- class="descriptionLabel"
- label-text="Add patchset description"
- value="[[_patchsetDescription]]"
- placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
- read-only="[[_descriptionReadOnly]]"
- on-changed="_handleDescriptionChanged"></gr-editable-label>
+ <template is="dom-if" if="[[!_patchsetDescription]]">
+ <gr-editable-label id="descriptionLabel" uppercase="" class="descriptionLabel" label-text="Add patchset description" value="[[_patchsetDescription]]" placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]" read-only="[[_descriptionReadOnly]]" on-changed="_handleDescriptionChanged"></gr-editable-label>
</template>
</span>
</div>
</div>
- <div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
+ <div class\$="rightControls [[_computeExpandedClass(filesExpanded)]]">
<span class="showOnEdit flexContainer">
- <gr-edit-controls
- id="editControls"
- patch-num="[[patchNum]]"
- change="[[change]]"></gr-edit-controls>
+ <gr-edit-controls id="editControls" patch-num="[[patchNum]]" change="[[change]]"></gr-edit-controls>
<span class="separator"></span>
</span>
- <span class$="[[_computeUploadHelpContainerClass(change, account)]]">
- <gr-button link
- class="upload"
- on-click="_handleUploadTap">Update Change</gr-button>
+ <span class\$="[[_computeUploadHelpContainerClass(change, account)]]">
+ <gr-button link="" class="upload" on-click="_handleUploadTap">Update Change</gr-button>
</span>
<span class="downloadContainer desktop">
- <gr-button link
- class="download"
- title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
- ShortcutSection.ACTIONS)]]"
- on-click="_handleDownloadTap">Download</gr-button>
+ <gr-button link="" class="download" title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
+ ShortcutSection.ACTIONS)]]" on-click="_handleDownloadTap">Download</gr-button>
</span>
- <span class$="includedInContainer [[_hideIncludedIn(change)]] desktop">
- <gr-button link
- class="includedIn"
- on-click="_handleIncludedInTap">Included In</gr-button>
+ <span class\$="includedInContainer [[_hideIncludedIn(change)]] desktop">
+ <gr-button link="" class="includedIn" on-click="_handleIncludedInTap">Included In</gr-button>
</span>
- <template is="dom-if"
- if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
- <gr-button
- id="expandBtn"
- link
- title="[[createTitle(Shortcut.EXPAND_ALL_DIFF_CONTEXT,
- ShortcutSection.DIFFS)]]"
- on-click="_expandAllDiffs">Expand All</gr-button>
- <gr-button
- id="collapseBtn"
- link
- on-click="_collapseAllDiffs">Collapse All</gr-button>
+ <template is="dom-if" if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
+ <gr-button id="expandBtn" link="" title="[[createTitle(Shortcut.EXPAND_ALL_DIFF_CONTEXT,
+ ShortcutSection.DIFFS)]]" on-click="_expandAllDiffs">Expand All</gr-button>
+ <gr-button id="collapseBtn" link="" on-click="_collapseAllDiffs">Collapse All</gr-button>
</template>
- <template is="dom-if"
- if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
+ <template is="dom-if" if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]">
<div class="warning">
Bulk actions disabled because there are too many files.
</div>
@@ -249,25 +185,12 @@
<div class="fileViewActions">
<span class="separator"></span>
<span class="fileViewActionsLabel">Diff view:</span>
- <gr-diff-mode-selector
- id="modeSelect"
- mode="{{diffViewMode}}"
- save-on-change="[[!diffPrefsDisabled]]"></gr-diff-mode-selector>
- <span id="diffPrefsContainer"
- class="hideOnEdit"
- hidden$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]"
- hidden>
- <gr-button
- link
- has-tooltip
- title="Diff preferences"
- class="prefsButton desktop"
- on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
+ <gr-diff-mode-selector id="modeSelect" mode="{{diffViewMode}}" save-on-change="[[!diffPrefsDisabled]]"></gr-diff-mode-selector>
+ <span id="diffPrefsContainer" class="hideOnEdit" hidden\$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]" hidden="">
+ <gr-button link="" has-tooltip="" title="Diff preferences" class="prefsButton desktop" on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
</span>
</div>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-file-list-header.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index f8a4e87..32ecb14 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -19,17 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list-header</title>
-<script src="/bower_components/webcomponentsjs/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-file-list-header.html">
+<script type="module" src="./gr-file-list-header.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-file-list-header.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -43,281 +48,284 @@
</template>
</test-fixture>
-<script>
- suite('gr-file-list-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-file-list-header.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-file-list-header tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({test: 'config'}); },
- getAccount() { return Promise.resolve(null); },
- _fetchSharedCacheURL() { return Promise.resolve({}); },
- });
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({test: 'config'}); },
+ getAccount() { return Promise.resolve(null); },
+ _fetchSharedCacheURL() { return Promise.resolve({}); },
});
+ element = fixture('basic');
+ });
- teardown(done => {
- flush(() => {
- sandbox.restore();
- done();
- });
- });
-
- test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => {
- element.diffPrefsDisabled = true;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element.diffPrefsDisabled = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element.diffPrefsDisabled = true;
- element.diffPrefs = {font_size: '12'};
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element.diffPrefsDisabled = false;
- flushAsynchronousOperations();
- assert.isFalse(element.$.diffPrefsContainer.hidden);
- });
-
- test('_computeDescriptionReadOnly', () => {
- assert.equal(element._computeDescriptionReadOnly(false,
- {owner: {_account_id: 1}}, {_account_id: 1}), true);
- assert.equal(element._computeDescriptionReadOnly(true,
- {owner: {_account_id: 0}}, {_account_id: 1}), true);
- assert.equal(element._computeDescriptionReadOnly(true,
- {owner: {_account_id: 1}}, {_account_id: 1}), false);
- });
-
- test('_computeDescriptionPlaceholder', () => {
- assert.equal(element._computeDescriptionPlaceholder(true),
- 'No patchset description');
- assert.equal(element._computeDescriptionPlaceholder(false),
- 'Add patchset description');
- });
-
- test('description editing', () => {
- const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
- .returns(Promise.resolve({ok: true}));
-
- element.changeNum = '42';
- element.basePatchNum = 'PARENT';
- element.patchNum = 1;
-
- element.change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}},
- },
- current_revision: 'rev1',
- status: 'NEW',
- labels: {},
- actions: {},
- owner: {_account_id: 1},
- };
- element.account = {_account_id: 1};
- element.loggedIn = true;
-
- flushAsynchronousOperations();
-
- // The element has a description, so the account chip should be visible
- // and the description label should not exist.
- const chip = Polymer.dom(element.root).querySelector('#descriptionChip');
- let label = Polymer.dom(element.root).querySelector('#descriptionLabel');
-
- assert.equal(chip.text, 'test');
- assert.isNotOk(label);
-
- // Simulate tapping the remove button, but call function directly so that
- // can determine what happens after the promise is resolved.
- return element._handleDescriptionRemoved()
- .then(() => {
- // The API stub should be called with an empty string for the new
- // description.
- assert.equal(putDescStub.lastCall.args[2], '');
- assert.equal(element.change.revisions.rev1.description, '');
-
- flushAsynchronousOperations();
- // The editable label should now be visible and the chip hidden.
- label = Polymer.dom(element.root).querySelector('#descriptionLabel');
- assert.isOk(label);
- assert.equal(getComputedStyle(chip).display, 'none');
- assert.notEqual(getComputedStyle(label).display, 'none');
- assert.isFalse(label.readOnly);
- // Edit the label to have a new value of test2, and save.
- label.editing = true;
- label._inputText = 'test2';
- label._save();
- flushAsynchronousOperations();
- // The API stub should be called with an `test2` for the new
- // description.
- assert.equal(putDescStub.callCount, 2);
- assert.equal(putDescStub.lastCall.args[2], 'test2');
- })
- .then(() => {
- flushAsynchronousOperations();
- // The chip should be visible again, and the label hidden.
- assert.equal(element.change.revisions.rev1.description, 'test2');
- assert.equal(getComputedStyle(label).display, 'none');
- assert.notEqual(getComputedStyle(chip).display, 'none');
- });
- });
-
- test('expandAllDiffs called when expand button clicked', () => {
- element.shownFileCount = 1;
- flushAsynchronousOperations();
- sandbox.stub(element, '_expandAllDiffs');
- MockInteractions.tap(Polymer.dom(element.root).querySelector(
- '#expandBtn'));
- assert.isTrue(element._expandAllDiffs.called);
- });
-
- test('collapseAllDiffs called when expand button clicked', () => {
- element.shownFileCount = 1;
- flushAsynchronousOperations();
- sandbox.stub(element, '_collapseAllDiffs');
- MockInteractions.tap(Polymer.dom(element.root).querySelector(
- '#collapseBtn'));
- assert.isTrue(element._collapseAllDiffs.called);
- });
-
- test('show/hide diffs disabled for large amounts of files', done => {
- const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
- element._files = [];
- element.changeNum = '42';
- element.basePatchNum = 'PARENT';
- element.patchNum = '2';
- element.shownFileCount = 1;
- flush(() => {
- assert.isTrue(computeSpy.lastCall.returnValue);
- _.times(element._maxFilesForBulkActions + 1, () => {
- element.shownFileCount = element.shownFileCount + 1;
- });
- assert.isFalse(computeSpy.lastCall.returnValue);
- done();
- });
- });
-
- test('fileViewActions are properly hidden', () => {
- const actions = element.shadowRoot
- .querySelector('.fileViewActions');
- assert.equal(getComputedStyle(actions).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(actions).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(actions).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(actions).display, 'none');
- });
-
- test('expand/collapse buttons are toggled correctly', () => {
- element.shownFileCount = 10;
- flushAsynchronousOperations();
- const expandBtn = element.shadowRoot.querySelector('#expandBtn');
- const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
- assert.notEqual(getComputedStyle(expandBtn).display, 'none');
- assert.equal(getComputedStyle(collapseBtn).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(expandBtn).display, 'none');
- assert.equal(getComputedStyle(collapseBtn).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(expandBtn).display, 'none');
- assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
- element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(expandBtn).display, 'none');
- assert.equal(getComputedStyle(collapseBtn).display, 'none');
- });
-
- test('navigateToChange called when range select changes', () => {
- const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element.change = {
- change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
- revisions: {
- rev2: {_number: 2},
- rev1: {_number: 1},
- rev13: {_number: 13},
- rev3: {_number: 3},
- },
- status: 'NEW',
- labels: {},
- };
- element.basePatchNum = 1;
- element.patchNum = 2;
-
- element._handlePatchChange({detail: {basePatchNum: 1, patchNum: 3}});
- assert.equal(navigateToChangeStub.callCount, 1);
- assert.isTrue(navigateToChangeStub.lastCall
- .calledWithExactly(element.change, 3, 1));
- });
-
- test('class is applied to file list on old patch set', () => {
- const allPatchSets = [{num: 4}, {num: 2}, {num: 1}];
- assert.equal(element._computePatchInfoClass('1', allPatchSets),
- 'patchInfoOldPatchSet');
- assert.equal(element._computePatchInfoClass('2', allPatchSets),
- 'patchInfoOldPatchSet');
- assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
- });
-
- suite('editMode behavior', () => {
- setup(() => {
- element.diffPrefsDisabled = false;
- element.diffPrefs = {};
- });
-
- const isVisible = el => {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') !== 'none';
- };
-
- test('patch specific elements', () => {
- element.editMode = true;
- sandbox.stub(element, 'computeLatestPatchNum').returns('2');
- flushAsynchronousOperations();
-
- assert.isFalse(isVisible(element.$.diffPrefsContainer));
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.descriptionContainer')));
-
- element.editMode = false;
- flushAsynchronousOperations();
-
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.descriptionContainer')));
- assert.isTrue(isVisible(element.$.diffPrefsContainer));
- });
-
- test('edit-controls visibility', () => {
- element.editMode = true;
- flushAsynchronousOperations();
- assert.isTrue(isVisible(element.$.editControls.parentElement));
-
- element.editMode = false;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.$.editControls.parentElement));
- });
-
- test('_computeUploadHelpContainerClass', () => {
- // Only show the upload helper button when an unmerged change is viewed
- // by its owner.
- const accountA = {_account_id: 1};
- const accountB = {_account_id: 2};
- assert.notInclude(element._computeUploadHelpContainerClass(
- {owner: accountA}, accountA), 'hide');
- assert.include(element._computeUploadHelpContainerClass(
- {owner: accountA}, accountB), 'hide');
- });
+ teardown(done => {
+ flush(() => {
+ sandbox.restore();
+ done();
});
});
+
+ test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => {
+ element.diffPrefsDisabled = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element.diffPrefsDisabled = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element.diffPrefsDisabled = true;
+ element.diffPrefs = {font_size: '12'};
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element.diffPrefsDisabled = false;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ });
+
+ test('_computeDescriptionReadOnly', () => {
+ assert.equal(element._computeDescriptionReadOnly(false,
+ {owner: {_account_id: 1}}, {_account_id: 1}), true);
+ assert.equal(element._computeDescriptionReadOnly(true,
+ {owner: {_account_id: 0}}, {_account_id: 1}), true);
+ assert.equal(element._computeDescriptionReadOnly(true,
+ {owner: {_account_id: 1}}, {_account_id: 1}), false);
+ });
+
+ test('_computeDescriptionPlaceholder', () => {
+ assert.equal(element._computeDescriptionPlaceholder(true),
+ 'No patchset description');
+ assert.equal(element._computeDescriptionPlaceholder(false),
+ 'Add patchset description');
+ });
+
+ test('description editing', () => {
+ const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
+ .returns(Promise.resolve({ok: true}));
+
+ element.changeNum = '42';
+ element.basePatchNum = 'PARENT';
+ element.patchNum = 1;
+
+ element.change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}},
+ },
+ current_revision: 'rev1',
+ status: 'NEW',
+ labels: {},
+ actions: {},
+ owner: {_account_id: 1},
+ };
+ element.account = {_account_id: 1};
+ element.loggedIn = true;
+
+ flushAsynchronousOperations();
+
+ // The element has a description, so the account chip should be visible
+ // and the description label should not exist.
+ const chip = dom(element.root).querySelector('#descriptionChip');
+ let label = dom(element.root).querySelector('#descriptionLabel');
+
+ assert.equal(chip.text, 'test');
+ assert.isNotOk(label);
+
+ // Simulate tapping the remove button, but call function directly so that
+ // can determine what happens after the promise is resolved.
+ return element._handleDescriptionRemoved()
+ .then(() => {
+ // The API stub should be called with an empty string for the new
+ // description.
+ assert.equal(putDescStub.lastCall.args[2], '');
+ assert.equal(element.change.revisions.rev1.description, '');
+
+ flushAsynchronousOperations();
+ // The editable label should now be visible and the chip hidden.
+ label = dom(element.root).querySelector('#descriptionLabel');
+ assert.isOk(label);
+ assert.equal(getComputedStyle(chip).display, 'none');
+ assert.notEqual(getComputedStyle(label).display, 'none');
+ assert.isFalse(label.readOnly);
+ // Edit the label to have a new value of test2, and save.
+ label.editing = true;
+ label._inputText = 'test2';
+ label._save();
+ flushAsynchronousOperations();
+ // The API stub should be called with an `test2` for the new
+ // description.
+ assert.equal(putDescStub.callCount, 2);
+ assert.equal(putDescStub.lastCall.args[2], 'test2');
+ })
+ .then(() => {
+ flushAsynchronousOperations();
+ // The chip should be visible again, and the label hidden.
+ assert.equal(element.change.revisions.rev1.description, 'test2');
+ assert.equal(getComputedStyle(label).display, 'none');
+ assert.notEqual(getComputedStyle(chip).display, 'none');
+ });
+ });
+
+ test('expandAllDiffs called when expand button clicked', () => {
+ element.shownFileCount = 1;
+ flushAsynchronousOperations();
+ sandbox.stub(element, '_expandAllDiffs');
+ MockInteractions.tap(dom(element.root).querySelector(
+ '#expandBtn'));
+ assert.isTrue(element._expandAllDiffs.called);
+ });
+
+ test('collapseAllDiffs called when expand button clicked', () => {
+ element.shownFileCount = 1;
+ flushAsynchronousOperations();
+ sandbox.stub(element, '_collapseAllDiffs');
+ MockInteractions.tap(dom(element.root).querySelector(
+ '#collapseBtn'));
+ assert.isTrue(element._collapseAllDiffs.called);
+ });
+
+ test('show/hide diffs disabled for large amounts of files', done => {
+ const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
+ element._files = [];
+ element.changeNum = '42';
+ element.basePatchNum = 'PARENT';
+ element.patchNum = '2';
+ element.shownFileCount = 1;
+ flush(() => {
+ assert.isTrue(computeSpy.lastCall.returnValue);
+ _.times(element._maxFilesForBulkActions + 1, () => {
+ element.shownFileCount = element.shownFileCount + 1;
+ });
+ assert.isFalse(computeSpy.lastCall.returnValue);
+ done();
+ });
+ });
+
+ test('fileViewActions are properly hidden', () => {
+ const actions = element.shadowRoot
+ .querySelector('.fileViewActions');
+ assert.equal(getComputedStyle(actions).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(actions).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(actions).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(actions).display, 'none');
+ });
+
+ test('expand/collapse buttons are toggled correctly', () => {
+ element.shownFileCount = 10;
+ flushAsynchronousOperations();
+ const expandBtn = element.shadowRoot.querySelector('#expandBtn');
+ const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
+ assert.notEqual(getComputedStyle(expandBtn).display, 'none');
+ assert.equal(getComputedStyle(collapseBtn).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(expandBtn).display, 'none');
+ assert.equal(getComputedStyle(collapseBtn).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL;
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(expandBtn).display, 'none');
+ assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
+ element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE;
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(expandBtn).display, 'none');
+ assert.equal(getComputedStyle(collapseBtn).display, 'none');
+ });
+
+ test('navigateToChange called when range select changes', () => {
+ const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element.change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev2: {_number: 2},
+ rev1: {_number: 1},
+ rev13: {_number: 13},
+ rev3: {_number: 3},
+ },
+ status: 'NEW',
+ labels: {},
+ };
+ element.basePatchNum = 1;
+ element.patchNum = 2;
+
+ element._handlePatchChange({detail: {basePatchNum: 1, patchNum: 3}});
+ assert.equal(navigateToChangeStub.callCount, 1);
+ assert.isTrue(navigateToChangeStub.lastCall
+ .calledWithExactly(element.change, 3, 1));
+ });
+
+ test('class is applied to file list on old patch set', () => {
+ const allPatchSets = [{num: 4}, {num: 2}, {num: 1}];
+ assert.equal(element._computePatchInfoClass('1', allPatchSets),
+ 'patchInfoOldPatchSet');
+ assert.equal(element._computePatchInfoClass('2', allPatchSets),
+ 'patchInfoOldPatchSet');
+ assert.equal(element._computePatchInfoClass('4', allPatchSets), '');
+ });
+
+ suite('editMode behavior', () => {
+ setup(() => {
+ element.diffPrefsDisabled = false;
+ element.diffPrefs = {};
+ });
+
+ const isVisible = el => {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ };
+
+ test('patch specific elements', () => {
+ element.editMode = true;
+ sandbox.stub(element, 'computeLatestPatchNum').returns('2');
+ flushAsynchronousOperations();
+
+ assert.isFalse(isVisible(element.$.diffPrefsContainer));
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.descriptionContainer')));
+
+ element.editMode = false;
+ flushAsynchronousOperations();
+
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.descriptionContainer')));
+ assert.isTrue(isVisible(element.$.diffPrefsContainer));
+ });
+
+ test('edit-controls visibility', () => {
+ element.editMode = true;
+ flushAsynchronousOperations();
+ assert.isTrue(isVisible(element.$.editControls.parentElement));
+
+ element.editMode = false;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.$.editControls.parentElement));
+ });
+
+ test('_computeUploadHelpContainerClass', () => {
+ // Only show the upload helper button when an unmerged change is viewed
+ // by its owner.
+ const accountA = {_account_id: 1};
+ const accountB = {_account_id: 2};
+ assert.notInclude(element._computeUploadHelpContainerClass(
+ {owner: accountA}, accountA), 'hide');
+ assert.include(element._computeUploadHelpContainerClass(
+ {owner: accountA}, accountB), 'hide');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index d2f11c2..858fc05 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.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,1359 +14,1388 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Maximum length for patch set descriptions.
- const PATCH_DESC_MAX_LENGTH = 500;
- const WARN_SHOW_ALL_THRESHOLD = 1000;
- const LOADING_DEBOUNCE_INTERVAL = 100;
+import '../../../behaviors/async-foreach-behavior/async-foreach-behavior.js';
+import '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../diff/gr-diff-cursor/gr-diff-cursor.js';
+import '../../diff/gr-diff-host/gr-diff-host.js';
+import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
+import '../../edit/gr-edit-file-controls/gr-edit-file-controls.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-linked-text/gr-linked-text.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../gr-file-list-constants.js';
+import {dom, flush} 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-file-list_html.js';
- const SIZE_BAR_MAX_WIDTH = 61;
- const SIZE_BAR_GAP_WIDTH = 1;
- const SIZE_BAR_MIN_WIDTH = 1.5;
+// Maximum length for patch set descriptions.
+const PATCH_DESC_MAX_LENGTH = 500;
+const WARN_SHOW_ALL_THRESHOLD = 1000;
+const LOADING_DEBOUNCE_INTERVAL = 100;
- const RENDER_TIMING_LABEL = 'FileListRenderTime';
- const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile';
- const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs';
- const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff';
+const SIZE_BAR_MAX_WIDTH = 61;
+const SIZE_BAR_GAP_WIDTH = 1;
+const SIZE_BAR_MIN_WIDTH = 1.5;
- const FileStatus = {
- A: 'Added',
- C: 'Copied',
- D: 'Deleted',
- M: 'Modified',
- R: 'Renamed',
- W: 'Rewritten',
- U: 'Unchanged',
- };
+const RENDER_TIMING_LABEL = 'FileListRenderTime';
+const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile';
+const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs';
+const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff';
+
+const FileStatus = {
+ A: 'Added',
+ C: 'Copied',
+ D: 'Deleted',
+ M: 'Modified',
+ R: 'Renamed',
+ W: 'Rewritten',
+ U: 'Unchanged',
+};
+
+/**
+ * @appliesMixin Gerrit.AsyncForeachMixin
+ * @appliesMixin Gerrit.DomUtilMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @extends Polymer.Element
+ */
+class GrFileList extends mixinBehaviors( [
+ Gerrit.AsyncForeachBehavior,
+ Gerrit.DomUtilBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-file-list'; }
+ /**
+ * Fired when a draft refresh should get triggered
+ *
+ * @event reload-drafts
+ */
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ patchRange: Object,
+ patchNum: String,
+ changeNum: String,
+ /** @type {?} */
+ changeComments: Object,
+ drafts: Object,
+ revisions: Array,
+ projectConfig: Object,
+ selectedIndex: {
+ type: Number,
+ notify: true,
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ /** @type {?} */
+ change: Object,
+ diffViewMode: {
+ type: String,
+ notify: true,
+ observer: '_updateDiffPreferences',
+ },
+ editMode: {
+ type: Boolean,
+ observer: '_editModeChanged',
+ },
+ filesExpanded: {
+ type: String,
+ value: GrFileListConstants.FilesExpandedState.NONE,
+ notify: true,
+ },
+ _filesByPath: Object,
+ _files: {
+ type: Array,
+ observer: '_filesChanged',
+ value() { return []; },
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _reviewed: {
+ type: Array,
+ value() { return []; },
+ },
+ diffPrefs: {
+ type: Object,
+ notify: true,
+ observer: '_updateDiffPreferences',
+ },
+ /** @type {?} */
+ _userPrefs: Object,
+ _showInlineDiffs: Boolean,
+ numFilesShown: {
+ type: Number,
+ notify: true,
+ },
+ /** @type {?} */
+ _patchChange: {
+ type: Object,
+ computed: '_calculatePatchChange(_files)',
+ },
+ fileListIncrement: Number,
+ _hideChangeTotals: {
+ type: Boolean,
+ computed: '_shouldHideChangeTotals(_patchChange)',
+ },
+ _hideBinaryChangeTotals: {
+ type: Boolean,
+ computed: '_shouldHideBinaryChangeTotals(_patchChange)',
+ },
+
+ _shownFiles: {
+ type: Array,
+ computed: '_computeFilesShown(numFilesShown, _files)',
+ },
+
+ /**
+ * The amount of files added to the shown files list the last time it was
+ * updated. This is used for reporting the average render time.
+ */
+ _reportinShownFilesIncrement: Number,
+
+ _expandedFilePaths: {
+ type: Array,
+ value() { return []; },
+ },
+ _displayLine: Boolean,
+ _loading: {
+ type: Boolean,
+ observer: '_loadingChanged',
+ },
+ /** @type {Gerrit.LayoutStats|undefined} */
+ _sizeBarLayout: {
+ type: Object,
+ computed: '_computeSizeBarLayout(_shownFiles.*)',
+ },
+
+ _showSizeBars: {
+ type: Boolean,
+ value: true,
+ computed: '_computeShowSizeBars(_userPrefs)',
+ },
+
+ /** @type {Function} */
+ _cancelForEachDiff: Function,
+
+ _showDynamicColumns: {
+ type: Boolean,
+ computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
+ '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
+ },
+ /** @type {Array<string>} */
+ _dynamicHeaderEndpoints: {
+ type: Array,
+ },
+ /** @type {Array<string>} */
+ _dynamicContentEndpoints: {
+ type: Array,
+ },
+ /** @type {Array<string>} */
+ _dynamicSummaryEndpoints: {
+ type: Array,
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_expandedPathsChanged(_expandedFilePaths.splices)',
+ '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
+ '_loading)',
+ ];
+ }
+
+ get keyBindings() {
+ return {
+ esc: '_handleEscKey',
+ };
+ }
+
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
+ [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
+ [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
+ [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+ [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
+ [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
+ [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
+ [this.Shortcut.PREV_LINE]: '_handleCursorPrev',
+ [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
+ [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
+ [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
+ [this.Shortcut.OPEN_FILE]: '_handleOpenFile',
+ [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
+ [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
+ [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+ [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
+
+ // Final two are actually handled by gr-comment-thread.
+ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+ [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('keydown',
+ e => this._scopedKeydownHandler(e));
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ Gerrit.awaitPluginsLoaded().then(() => {
+ this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-view-file-list-header');
+ this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-view-file-list-content');
+ this._dynamicSummaryEndpoints = Gerrit._endpoints.getDynamicEndpoints(
+ 'change-view-file-list-summary');
+
+ if (this._dynamicHeaderEndpoints.length !==
+ this._dynamicContentEndpoints.length) {
+ console.warn(
+ 'Different number of dynamic file-list header and content.');
+ }
+ if (this._dynamicHeaderEndpoints.length !==
+ this._dynamicSummaryEndpoints.length) {
+ console.warn(
+ 'Different number of dynamic file-list headers and summary.');
+ }
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this._cancelDiffs();
+ }
/**
- * @appliesMixin Gerrit.AsyncForeachMixin
- * @appliesMixin Gerrit.DomUtilMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.PathListMixin
- * @extends Polymer.Element
+ * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
+ * events must be scoped to a component level (e.g. `enter`) in order to not
+ * override native browser functionality.
+ *
+ * Context: Issue 7277
*/
- class GrFileList extends Polymer.mixinBehaviors( [
- Gerrit.AsyncForeachBehavior,
- Gerrit.DomUtilBehavior,
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.PathListBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-file-list'; }
- /**
- * Fired when a draft refresh should get triggered
- *
- * @event reload-drafts
- */
-
- static get properties() {
- return {
- /** @type {?} */
- patchRange: Object,
- patchNum: String,
- changeNum: String,
- /** @type {?} */
- changeComments: Object,
- drafts: Object,
- revisions: Array,
- projectConfig: Object,
- selectedIndex: {
- type: Number,
- notify: true,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- /** @type {?} */
- change: Object,
- diffViewMode: {
- type: String,
- notify: true,
- observer: '_updateDiffPreferences',
- },
- editMode: {
- type: Boolean,
- observer: '_editModeChanged',
- },
- filesExpanded: {
- type: String,
- value: GrFileListConstants.FilesExpandedState.NONE,
- notify: true,
- },
- _filesByPath: Object,
- _files: {
- type: Array,
- observer: '_filesChanged',
- value() { return []; },
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _reviewed: {
- type: Array,
- value() { return []; },
- },
- diffPrefs: {
- type: Object,
- notify: true,
- observer: '_updateDiffPreferences',
- },
- /** @type {?} */
- _userPrefs: Object,
- _showInlineDiffs: Boolean,
- numFilesShown: {
- type: Number,
- notify: true,
- },
- /** @type {?} */
- _patchChange: {
- type: Object,
- computed: '_calculatePatchChange(_files)',
- },
- fileListIncrement: Number,
- _hideChangeTotals: {
- type: Boolean,
- computed: '_shouldHideChangeTotals(_patchChange)',
- },
- _hideBinaryChangeTotals: {
- type: Boolean,
- computed: '_shouldHideBinaryChangeTotals(_patchChange)',
- },
-
- _shownFiles: {
- type: Array,
- computed: '_computeFilesShown(numFilesShown, _files)',
- },
-
- /**
- * The amount of files added to the shown files list the last time it was
- * updated. This is used for reporting the average render time.
- */
- _reportinShownFilesIncrement: Number,
-
- _expandedFilePaths: {
- type: Array,
- value() { return []; },
- },
- _displayLine: Boolean,
- _loading: {
- type: Boolean,
- observer: '_loadingChanged',
- },
- /** @type {Gerrit.LayoutStats|undefined} */
- _sizeBarLayout: {
- type: Object,
- computed: '_computeSizeBarLayout(_shownFiles.*)',
- },
-
- _showSizeBars: {
- type: Boolean,
- value: true,
- computed: '_computeShowSizeBars(_userPrefs)',
- },
-
- /** @type {Function} */
- _cancelForEachDiff: Function,
-
- _showDynamicColumns: {
- type: Boolean,
- computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
- '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
- },
- /** @type {Array<string>} */
- _dynamicHeaderEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicContentEndpoints: {
- type: Array,
- },
- /** @type {Array<string>} */
- _dynamicSummaryEndpoints: {
- type: Array,
- },
- };
- }
-
- static get observers() {
- return [
- '_expandedPathsChanged(_expandedFilePaths.splices)',
- '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' +
- '_loading)',
- ];
- }
-
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- };
- }
-
- keyboardShortcuts() {
- return {
- [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
- [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
- [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
- [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
- [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
- [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
- [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
- [this.Shortcut.PREV_LINE]: '_handleCursorPrev',
- [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
- [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
- [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
- [this.Shortcut.OPEN_FILE]: '_handleOpenFile',
- [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
- [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
- [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
- [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
-
- // Final two are actually handled by gr-comment-thread.
- [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
- [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
- };
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('keydown',
- e => this._scopedKeydownHandler(e));
- }
-
- /** @override */
- attached() {
- super.attached();
- Gerrit.awaitPluginsLoaded().then(() => {
- this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-view-file-list-header');
- this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-view-file-list-content');
- this._dynamicSummaryEndpoints = Gerrit._endpoints.getDynamicEndpoints(
- 'change-view-file-list-summary');
-
- if (this._dynamicHeaderEndpoints.length !==
- this._dynamicContentEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list header and content.');
- }
- if (this._dynamicHeaderEndpoints.length !==
- this._dynamicSummaryEndpoints.length) {
- console.warn(
- 'Different number of dynamic file-list headers and summary.');
- }
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this._cancelDiffs();
- }
-
- /**
- * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
- * events must be scoped to a component level (e.g. `enter`) in order to not
- * override native browser functionality.
- *
- * Context: Issue 7277
- */
- _scopedKeydownHandler(e) {
- if (e.keyCode === 13) {
- // Enter.
- this._handleOpenFile(e);
- }
- }
-
- reload() {
- if (!this.changeNum || !this.patchRange.patchNum) {
- return Promise.resolve();
- }
-
- this._loading = true;
-
- this.collapseAllDiffs();
- const promises = [];
-
- promises.push(this._getFiles().then(filesByPath => {
- this._filesByPath = filesByPath;
- }));
- promises.push(this._getLoggedIn()
- .then(loggedIn => this._loggedIn = loggedIn)
- .then(loggedIn => {
- if (!loggedIn) { return; }
-
- return this._getReviewedFiles().then(reviewed => {
- this._reviewed = reviewed;
- });
- }));
-
- promises.push(this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- }));
-
- promises.push(this._getPreferences().then(prefs => {
- this._userPrefs = prefs;
- }));
-
- return Promise.all(promises).then(() => {
- this._loading = false;
- this._detectChromiteButler();
- this.$.reporting.fileListDisplayed();
- });
- }
-
- _detectChromiteButler() {
- const hasButler = !!document.getElementById('butler-suggested-owners');
- if (hasButler) {
- this.$.reporting.reportExtension('butler');
- }
- }
-
- get diffs() {
- const diffs = Polymer.dom(this.root).querySelectorAll('gr-diff-host');
- // It is possible that a bogus diff element is hanging around invisibly
- // from earlier with a different patch set choice and associated with a
- // different entry in the files array. So filter on visible items only.
- return Array.from(diffs).filter(
- el => !!el && !!el.style && el.style.display !== 'none');
- }
-
- openDiffPrefs() {
- this.$.diffPreferencesDialog.open();
- }
-
- _calculatePatchChange(files) {
- const magicFilesExcluded = files.filter(files =>
- !this.isMagicPath(files.__path)
- );
-
- return magicFilesExcluded.reduce((acc, obj) => {
- const inserted = obj.lines_inserted ? obj.lines_inserted : 0;
- const deleted = obj.lines_deleted ? obj.lines_deleted : 0;
- const total_size = (obj.size && obj.binary) ? obj.size : 0;
- const size_delta_inserted =
- obj.binary && obj.size_delta > 0 ? obj.size_delta : 0;
- const size_delta_deleted =
- obj.binary && obj.size_delta < 0 ? obj.size_delta : 0;
-
- return {
- inserted: acc.inserted + inserted,
- deleted: acc.deleted + deleted,
- size_delta_inserted: acc.size_delta_inserted + size_delta_inserted,
- size_delta_deleted: acc.size_delta_deleted + size_delta_deleted,
- total_size: acc.total_size + total_size,
- };
- }, {inserted: 0, deleted: 0, size_delta_inserted: 0,
- size_delta_deleted: 0, total_size: 0});
- }
-
- _getDiffPreferences() {
- return this.$.restAPI.getDiffPreferences();
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- _togglePathExpanded(path) {
- // Is the path in the list of expanded diffs? IF so remove it, otherwise
- // add it to the list.
- const pathIndex = this._expandedFilePaths.indexOf(path);
- if (pathIndex === -1) {
- this.push('_expandedFilePaths', path);
- } else {
- this.splice('_expandedFilePaths', pathIndex, 1);
- }
- }
-
- _togglePathExpandedByIndex(index) {
- this._togglePathExpanded(this._files[index].__path);
- }
-
- _updateDiffPreferences() {
- if (!this.diffs.length) { return; }
- // Re-render all expanded diffs sequentially.
- this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
- this._renderInOrder(this._expandedFilePaths, this.diffs,
- this._expandedFilePaths.length);
- }
-
- _forEachDiff(fn) {
- const diffs = this.diffs;
- for (let i = 0; i < diffs.length; i++) {
- fn(diffs[i]);
- }
- }
-
- expandAllDiffs() {
- this._showInlineDiffs = true;
-
- // Find the list of paths that are in the file list, but not in the
- // expanded list.
- const newPaths = [];
- let path;
- for (let i = 0; i < this._shownFiles.length; i++) {
- path = this._shownFiles[i].__path;
- if (!this._expandedFilePaths.includes(path)) {
- newPaths.push(path);
- }
- }
-
- this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
- }
-
- collapseAllDiffs() {
- this._showInlineDiffs = false;
- this._expandedFilePaths = [];
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFilePaths.length, this._files.length);
- this.$.diffCursor.handleDiffUpdate();
- }
-
- /**
- * Computes a string with the number of comments and unresolved comments.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeCommentsString(changeComments, patchRange, path) {
- const unresolvedCount =
- changeComments.computeUnresolvedNum(patchRange.basePatchNum, path) +
- changeComments.computeUnresolvedNum(patchRange.patchNum, path);
- const commentCount =
- changeComments.computeCommentCount(patchRange.basePatchNum, path) +
- changeComments.computeCommentCount(patchRange.patchNum, path);
- const commentString = GrCountStringFormatter.computePluralString(
- commentCount, 'comment');
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
-
- return commentString +
- // Add a space if both comments and unresolved
- (commentString && unresolvedString ? ' ' : '') +
- // Add parentheses around unresolved if it exists.
- (unresolvedString ? `(${unresolvedString})` : '');
- }
-
- /**
- * Computes a string with the number of drafts.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeDraftsString(changeComments, patchRange, path) {
- const draftCount =
- changeComments.computeDraftCount(patchRange.basePatchNum, path) +
- changeComments.computeDraftCount(patchRange.patchNum, path);
- return GrCountStringFormatter.computePluralString(draftCount, 'draft');
- }
-
- /**
- * Computes a shortened string with the number of drafts.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeDraftsStringMobile(changeComments, patchRange, path) {
- const draftCount =
- changeComments.computeDraftCount(patchRange.basePatchNum, path) +
- changeComments.computeDraftCount(patchRange.patchNum, path);
- return GrCountStringFormatter.computeShortString(draftCount, 'd');
- }
-
- /**
- * Computes a shortened string with the number of comments.
- *
- * @param {!Object} changeComments
- * @param {!Object} patchRange
- * @param {string} path
- * @return {string}
- */
- _computeCommentsStringMobile(changeComments, patchRange, path) {
- const commentCount =
- changeComments.computeCommentCount(patchRange.basePatchNum, path) +
- changeComments.computeCommentCount(patchRange.patchNum, path);
- return GrCountStringFormatter.computeShortString(commentCount, 'c');
- }
-
- /**
- * @param {string} path
- * @param {boolean=} opt_reviewed
- */
- _reviewFile(path, opt_reviewed) {
- if (this.editMode) { return; }
- const index = this._files.findIndex(file => file.__path === path);
- const reviewed = opt_reviewed || !this._files[index].isReviewed;
-
- this.set(['_files', index, 'isReviewed'], reviewed);
- if (index < this._shownFiles.length) {
- this.notifyPath(`_shownFiles.${index}.isReviewed`);
- }
-
- this._saveReviewedState(path, reviewed);
- }
-
- _saveReviewedState(path, reviewed) {
- return this.$.restAPI.saveFileReviewed(this.changeNum,
- this.patchRange.patchNum, path, reviewed);
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getReviewedFiles() {
- if (this.editMode) { return Promise.resolve([]); }
- return this.$.restAPI.getReviewedFiles(this.changeNum,
- this.patchRange.patchNum);
- }
-
- _getFiles() {
- return this.$.restAPI.getChangeOrEditFiles(
- this.changeNum, this.patchRange);
- }
-
- /**
- * The closure compiler doesn't realize this.specialFilePathCompare is
- * valid.
- *
- * @suppress {checkTypes}
- */
- _normalizeChangeFilesResponse(response) {
- if (!response) { return []; }
- const paths = Object.keys(response).sort(this.specialFilePathCompare);
- const files = [];
- for (let i = 0; i < paths.length; i++) {
- const info = response[paths[i]];
- info.__path = paths[i];
- info.lines_inserted = info.lines_inserted || 0;
- info.lines_deleted = info.lines_deleted || 0;
- files.push(info);
- }
- return files;
- }
-
- /**
- * Handle all events from the file list dom-repeat so event handleers don't
- * have to get registered for potentially very long lists.
- */
- _handleFileListClick(e) {
- // Traverse upwards to find the row element if the target is not the row.
- let row = e.target;
- while (!row.classList.contains('row') && row.parentElement) {
- row = row.parentElement;
- }
-
- const path = row.dataset.path;
- // Handle checkbox mark as reviewed.
- if (e.target.classList.contains('markReviewed')) {
- e.preventDefault();
- return this._reviewFile(path);
- }
-
- // If a path cannot be interpreted from the click target (meaning it's not
- // somewhere in the row, e.g. diff content) or if the user clicked the
- // link, defer to the native behavior.
- if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; }
-
- // Disregard the event if the click target is in the edit controls.
- if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
-
- e.preventDefault();
- this._togglePathExpanded(path);
- }
-
- _handleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- this.$.diffCursor.moveLeft();
- }
-
- _handleRightPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- this.$.diffCursor.moveRight();
- }
-
- _handleToggleInlineDiff(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) ||
- this.$.fileCursor.index === -1) { return; }
-
- e.preventDefault();
- this._togglePathExpandedByIndex(this.$.fileCursor.index);
- }
-
- _handleToggleAllInlineDiffs(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._toggleInlineDiffs();
- }
-
- _handleCursorNext(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- if (this._showInlineDiffs) {
- e.preventDefault();
- this.$.diffCursor.moveDown();
- this._displayLine = true;
- } else {
- // Down key
- if (this.getKeyboardEvent(e).keyCode === 40) { return; }
- e.preventDefault();
- this.$.fileCursor.next();
- this.selectedIndex = this.$.fileCursor.index;
- }
- }
-
- _handleCursorPrev(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- if (this._showInlineDiffs) {
- e.preventDefault();
- this.$.diffCursor.moveUp();
- this._displayLine = true;
- } else {
- // Up key
- if (this.getKeyboardEvent(e).keyCode === 38) { return; }
- e.preventDefault();
- this.$.fileCursor.previous();
- this.selectedIndex = this.$.fileCursor.index;
- }
- }
-
- _handleNewComment(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this.$.diffCursor.createCommentInPlace();
- }
-
- _handleOpenLastFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._openSelectedFile(this._files.length - 1);
- }
-
- _handleOpenFirstFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._openSelectedFile(0);
- }
-
- _handleOpenFile(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
-
- if (this._showInlineDiffs) {
- this._openCursorFile();
- return;
- }
-
- this._openSelectedFile();
- }
-
- _handleNextChunk(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
- this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- if (this.isModifierPressed(e, 'shiftKey')) {
- this.$.diffCursor.moveToNextCommentThread();
- } else {
- this.$.diffCursor.moveToNextChunk();
- }
- }
-
- _handlePrevChunk(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
- this._noDiffsExpanded()) {
- return;
- }
-
- e.preventDefault();
- if (this.isModifierPressed(e, 'shiftKey')) {
- this.$.diffCursor.moveToPreviousCommentThread();
- } else {
- this.$.diffCursor.moveToPreviousChunk();
- }
- }
-
- _handleToggleFileReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
- return;
- }
-
- e.preventDefault();
- if (!this._files[this.$.fileCursor.index]) { return; }
- this._reviewFile(this._files[this.$.fileCursor.index].__path);
- }
-
- _handleToggleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this._forEachDiff(diff => {
- diff.toggleLeftDiff();
- });
- }
-
- _toggleInlineDiffs() {
- if (this._showInlineDiffs) {
- this.collapseAllDiffs();
- } else {
- this.expandAllDiffs();
- }
- }
-
- _openCursorFile() {
- const diff = this.$.diffCursor.getTargetDiffElement();
- Gerrit.Nav.navigateToDiff(this.change, diff.path,
- diff.patchRange.patchNum, this.patchRange.basePatchNum);
- }
-
- /**
- * @param {number=} opt_index
- */
- _openSelectedFile(opt_index) {
- if (opt_index != null) {
- this.$.fileCursor.setCursorAtIndex(opt_index);
- }
- if (!this._files[this.$.fileCursor.index]) { return; }
- Gerrit.Nav.navigateToDiff(this.change,
- this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum,
- this.patchRange.basePatchNum);
- }
-
- _addDraftAtTarget() {
- const diff = this.$.diffCursor.getTargetDiffElement();
- const target = this.$.diffCursor.getTargetLineElement();
- if (diff && target) {
- diff.addDraftAtLine(target);
- }
- }
-
- _shouldHideChangeTotals(_patchChange) {
- return _patchChange.inserted === 0 && _patchChange.deleted === 0;
- }
-
- _shouldHideBinaryChangeTotals(_patchChange) {
- return _patchChange.size_delta_inserted === 0 &&
- _patchChange.size_delta_deleted === 0;
- }
-
- _computeFileStatus(status) {
- return status || 'M';
- }
-
- _computeDiffURL(change, patchRange, path, editMode) {
- // Polymer 2: check for undefined
- if ([change, patchRange, path, editMode]
- .some(arg => arg === undefined)) {
- return;
- }
- // TODO(kaspern): Fix editing for commit messages and merge lists.
- if (editMode && path !== this.COMMIT_MESSAGE_PATH &&
- path !== this.MERGE_LIST_PATH) {
- return Gerrit.Nav.getEditUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
- return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
-
- _formatBytes(bytes) {
- if (bytes == 0) return '+/-0 B';
- const bits = 1024;
- const decimals = 1;
- const sizes =
- ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
- const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits));
- const prepend = bytes > 0 ? '+' : '';
- return prepend + parseFloat((bytes / Math.pow(bits, exponent))
- .toFixed(decimals)) + ' ' + sizes[exponent];
- }
-
- _formatPercentage(size, delta) {
- const oldSize = size - delta;
-
- if (oldSize === 0) { return ''; }
-
- const percentage = Math.round(Math.abs(delta * 100 / oldSize));
- return '(' + (delta > 0 ? '+' : '-') + percentage + '%)';
- }
-
- _computeBinaryClass(delta) {
- if (delta === 0) { return; }
- return delta >= 0 ? 'added' : 'removed';
- }
-
- /**
- * @param {string} baseClass
- * @param {string} path
- */
- _computeClass(baseClass, path) {
- const classes = [];
- if (baseClass) {
- classes.push(baseClass);
- }
- if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
- classes.push('invisible');
- }
- return classes.join(' ');
- }
-
- _computePathClass(path, expandedFilesRecord) {
- return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
- }
-
- _computeShowHideIcon(path, expandedFilesRecord) {
- return this._isFileExpanded(path, expandedFilesRecord) ?
- 'gr-icons:expand-less' : 'gr-icons:expand-more';
- }
-
- _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
- // Polymer 2: check for undefined
- if ([
- filesByPath,
- changeComments,
- patchRange,
- reviewed,
- loading,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- // Await all promises resolving from reload. @See Issue 9057
- if (loading || !changeComments) { return; }
-
- const commentedPaths = changeComments.getPaths(patchRange);
- const files = Object.assign({}, filesByPath);
- Object.keys(commentedPaths).forEach(commentedPath => {
- if (files.hasOwnProperty(commentedPath)) { return; }
- files[commentedPath] = {status: 'U'};
- });
- const reviewedSet = new Set(reviewed || []);
- for (const filePath in files) {
- if (!files.hasOwnProperty(filePath)) { continue; }
- files[filePath].isReviewed = reviewedSet.has(filePath);
- }
-
- this._files = this._normalizeChangeFilesResponse(files);
- }
-
- _computeFilesShown(numFilesShown, files) {
- // Polymer 2: check for undefined
- if ([numFilesShown, files].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const previousNumFilesShown = this._shownFiles ?
- this._shownFiles.length : 0;
-
- const filesShown = files.slice(0, numFilesShown);
- this.fire('files-shown-changed', {length: filesShown.length});
-
- // Start the timer for the rendering work hwere because this is where the
- // _shownFiles property is being set, and _shownFiles is used in the
- // dom-repeat binding.
- this.$.reporting.time(RENDER_TIMING_LABEL);
-
- // How many more files are being shown (if it's an increase).
- this._reportinShownFilesIncrement =
- Math.max(0, filesShown.length - previousNumFilesShown);
-
- return filesShown;
- }
-
- _updateDiffCursor() {
- // Overwrite the cursor's list of diffs:
- this.$.diffCursor.splice(
- ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs));
- }
-
- _filesChanged() {
- if (this._files && this._files.length > 0) {
- Polymer.dom.flush();
- const files = Array.from(
- Polymer.dom(this.root).querySelectorAll('.file-row'));
- this.$.fileCursor.stops = files;
- this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
- }
- }
-
- _incrementNumFilesShown() {
- this.numFilesShown += this.fileListIncrement;
- }
-
- _computeFileListControlClass(numFilesShown, files) {
- return numFilesShown >= files.length ? 'invisible' : '';
- }
-
- _computeIncrementText(numFilesShown, files) {
- if (!files) { return ''; }
- const text =
- Math.min(this.fileListIncrement, files.length - numFilesShown);
- return 'Show ' + text + ' more';
- }
-
- _computeShowAllText(files) {
- if (!files) { return ''; }
- return 'Show all ' + files.length + ' files';
- }
-
- _computeWarnShowAll(files) {
- return files.length > WARN_SHOW_ALL_THRESHOLD;
- }
-
- _computeShowAllWarning(files) {
- if (!this._computeWarnShowAll(files)) { return ''; }
- return 'Warning: showing all ' + files.length +
- ' files may take several seconds.';
- }
-
- _showAllFiles() {
- this.numFilesShown = this._files.length;
- }
-
- _computePatchSetDescription(revisions, patchNum) {
- // Polymer 2: check for undefined
- if ([revisions, patchNum].some(arg => arg === undefined)) {
- return '';
- }
-
- const rev = this.getRevisionByPatchNum(revisions, patchNum);
- return (rev && rev.description) ?
- rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
- }
-
- /**
- * Get a descriptive label for use in the status indicator's tooltip and
- * ARIA label.
- *
- * @param {string} status
- * @return {string}
- */
- _computeFileStatusLabel(status) {
- const statusCode = this._computeFileStatus(status);
- return FileStatus.hasOwnProperty(statusCode) ?
- FileStatus[statusCode] : 'Status Unknown';
- }
-
- _isFileExpanded(path, expandedFilesRecord) {
- return expandedFilesRecord.base.includes(path);
- }
-
- _onLineSelected(e, detail) {
- this.$.diffCursor.moveToLineNumber(detail.number, detail.side,
- detail.path);
- }
-
- _computeExpandedFiles(expandedCount, totalCount) {
- if (expandedCount === 0) {
- return GrFileListConstants.FilesExpandedState.NONE;
- } else if (expandedCount === totalCount) {
- return GrFileListConstants.FilesExpandedState.ALL;
- }
- return GrFileListConstants.FilesExpandedState.SOME;
- }
-
- /**
- * Handle splices to the list of expanded file paths. If there are any new
- * entries in the expanded list, then render each diff corresponding in
- * order by waiting for the previous diff to finish before starting the next
- * one.
- *
- * @param {!Array} record The splice record in the expanded paths list.
- */
- _expandedPathsChanged(record) {
- // Clear content for any diffs that are not open so if they get re-opened
- // the stale content does not flash before it is cleared and reloaded.
- const collapsedDiffs = this.diffs.filter(diff =>
- this._expandedFilePaths.indexOf(diff.path) === -1);
- this._clearCollapsedDiffs(collapsedDiffs);
-
- if (!record) { return; } // Happens after "Collapse all" clicked.
-
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFilePaths.length, this._files.length);
-
- // Find the paths introduced by the new index splices:
- const newPaths = record.indexSplices
- .map(splice => splice.object.slice(
- splice.index, splice.index + splice.addedCount))
- .reduce((acc, paths) => acc.concat(paths), []);
-
- // Required so that the newly created diff view is included in this.diffs.
- Polymer.dom.flush();
-
- this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
-
- if (newPaths.length) {
- this._renderInOrder(newPaths, this.diffs, newPaths.length);
- }
-
- this._updateDiffCursor();
- this.$.diffCursor.handleDiffUpdate();
- }
-
- _clearCollapsedDiffs(collapsedDiffs) {
- for (const diff of collapsedDiffs) {
- diff.cancel();
- diff.clearDiffContent();
- }
- }
-
- /**
- * Given an array of paths and a NodeList of diff elements, render the diff
- * for each path in order, awaiting the previous render to complete before
- * continung.
- *
- * @param {!Array<string>} paths
- * @param {!NodeList<!Object>} diffElements (GrDiffHostElement)
- * @param {number} initialCount The total number of paths in the pass. This
- * is used to generate log messages.
- * @return {!Promise}
- */
- _renderInOrder(paths, diffElements, initialCount) {
- let iter = 0;
-
- return (new Promise(resolve => {
- this.fire('reload-drafts', {resolve});
- })).then(() => this.asyncForeach(paths, (path, cancel) => {
- this._cancelForEachDiff = cancel;
-
- iter++;
- console.log('Expanding diff', iter, 'of', initialCount, ':',
- path);
- const diffElem = this._findDiffByPath(path, diffElements);
- if (!diffElem) {
- console.warn(`Did not find <gr-diff-host> element for ${path}`);
- return Promise.resolve();
- }
- diffElem.comments = this.changeComments.getCommentsBySideForPath(
- path, this.patchRange, this.projectConfig);
- const promises = [diffElem.reload()];
- if (this._loggedIn && !this.diffPrefs.manual_review) {
- promises.push(this._reviewFile(path, true));
- }
- return Promise.all(promises);
- }).then(() => {
- this._cancelForEachDiff = null;
- this._nextRenderParams = null;
- console.log('Finished expanding', initialCount, 'diff(s)');
- this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
- EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
- this.$.diffCursor.handleDiffUpdate();
- }));
- }
-
- /** Cancel the rendering work of every diff in the list */
- _cancelDiffs() {
- if (this._cancelForEachDiff) { this._cancelForEachDiff(); }
- this._forEachDiff(d => d.cancel());
- }
-
- /**
- * In the given NodeList of diff elements, find the diff for the given path.
- *
- * @param {string} path
- * @param {!NodeList<!Object>} diffElements (GrDiffElement)
- * @return {!Object|undefined} (GrDiffElement)
- */
- _findDiffByPath(path, diffElements) {
- for (let i = 0; i < diffElements.length; i++) {
- if (diffElements[i].path === path) {
- return diffElements[i];
- }
- }
- }
-
- /**
- * Reset the comments of a modified thread
- *
- * @param {string} rootId
- * @param {string} path
- */
- reloadCommentsForThreadWithRootId(rootId, path) {
- // Don't bother continuing if we already know that the path that contains
- // the updated comment thread is not expanded.
- if (!this._expandedFilePaths.includes(path)) { return; }
- const diff = this.diffs.find(d => d.path === path);
-
- const threadEl = diff.getThreadEls().find(t => t.rootId === rootId);
- if (!threadEl) { return; }
-
- const newComments = this.changeComments.getCommentsForThread(rootId);
-
- // If newComments is null, it means that a single draft was
- // removed from a thread in the thread view, and the thread should
- // no longer exist. Remove the existing thread element in the diff
- // view.
- if (!newComments) {
- threadEl.fireRemoveSelf();
- return;
- }
-
- // Comments are not returned with the commentSide attribute from
- // the api, but it's necessary to be stored on the diff's
- // comments due to use in the _handleCommentUpdate function.
- // The comment thread already has a side associated with it, so
- // set the comment's side to match.
- threadEl.comments = newComments.map(c => Object.assign(
- c, {__commentSide: threadEl.commentSide}
- ));
- Polymer.dom.flush();
- return;
- }
-
- _handleEscKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this._displayLine = false;
- }
-
- /**
- * Update the loading class for the file list rows. The update is inside a
- * debouncer so that the file list doesn't flash gray when the API requests
- * are reasonably fast.
- *
- * @param {boolean} loading
- */
- _loadingChanged(loading) {
- this.debounce('loading-change', () => {
- // Only show set the loading if there have been files loaded to show. In
- // this way, the gray loading style is not shown on initial loads.
- this.classList.toggle('loading', loading && !!this._files.length);
- }, LOADING_DEBOUNCE_INTERVAL);
- }
-
- _editModeChanged(editMode) {
- this.classList.toggle('editMode', editMode);
- }
-
- _computeReviewedClass(isReviewed) {
- return isReviewed ? 'isReviewed' : '';
- }
-
- _computeReviewedText(isReviewed) {
- return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
- }
-
- /**
- * Given a file path, return whether that path should have visible size bars
- * and be included in the size bars calculation.
- *
- * @param {string} path
- * @return {boolean}
- */
- _showBarsForPath(path) {
- return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH;
- }
-
- /**
- * Compute size bar layout values from the file list.
- *
- * @return {Gerrit.LayoutStats|undefined}
- *
- */
- _computeSizeBarLayout(shownFilesRecord) {
- if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; }
- const stats = {
- maxInserted: 0,
- maxDeleted: 0,
- maxAdditionWidth: 0,
- maxDeletionWidth: 0,
- deletionOffset: 0,
- };
- shownFilesRecord.base
- .filter(f => this._showBarsForPath(f.__path))
- .forEach(f => {
- if (f.lines_inserted) {
- stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted);
- }
- if (f.lines_deleted) {
- stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted);
- }
- });
- const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted);
- if (!isNaN(ratio)) {
- stats.maxAdditionWidth =
- (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio;
- stats.maxDeletionWidth =
- SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
- stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH;
- }
- return stats;
- }
-
- /**
- * Get the width of the addition bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarAdditionWidth(file, stats) {
- if (stats.maxInserted === 0 ||
- !file.lines_inserted ||
- !this._showBarsForPath(file.__path)) {
- return 0;
- }
- const width =
- stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted;
- return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
- }
-
- /**
- * Get the x-offset of the addition bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarAdditionX(file, stats) {
- return stats.maxAdditionWidth -
- this._computeBarAdditionWidth(file, stats);
- }
-
- /**
- * Get the width of the deletion bar for a file.
- *
- * @param {Object} file
- * @param {Gerrit.LayoutStats} stats
- * @return {number}
- */
- _computeBarDeletionWidth(file, stats) {
- if (stats.maxDeleted === 0 ||
- !file.lines_deleted ||
- !this._showBarsForPath(file.__path)) {
- return 0;
- }
- const width =
- stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted;
- return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
- }
-
- /**
- * Get the x-offset of the deletion bar for a file.
- *
- * @param {Gerrit.LayoutStats} stats
- *
- * @return {number}
- */
- _computeBarDeletionX(stats) {
- return stats.deletionOffset;
- }
-
- _computeShowSizeBars(userPrefs) {
- return !!userPrefs.size_bar_in_change_table;
- }
-
- _computeSizeBarsClass(showSizeBars, path) {
- let hideClass = '';
- if (!showSizeBars) {
- hideClass = 'hide';
- } else if (!this._showBarsForPath(path)) {
- hideClass = 'invisible';
- }
- return `sizeBars desktop ${hideClass}`;
- }
-
- /**
- * Shows registered dynamic columns iff the 'header', 'content' and
- * 'summary' endpoints are regiestered the exact same number of times.
- * Ideally, there should be a better way to enforce the expectation of the
- * dependencies between dynamic endpoints.
- */
- _computeShowDynamicColumns(
- headerEndpoints, contentEndpoints, summaryEndpoints) {
- return headerEndpoints && contentEndpoints && summaryEndpoints &&
- headerEndpoints.length === contentEndpoints.length &&
- headerEndpoints.length === summaryEndpoints.length;
- }
-
- /**
- * Returns true if none of the inline diffs have been expanded.
- *
- * @return {boolean}
- */
- _noDiffsExpanded() {
- return this.filesExpanded === GrFileListConstants.FilesExpandedState.NONE;
- }
-
- /**
- * Method to call via binding when each file list row is rendered. This
- * allows approximate detection of when the dom-repeat has completed
- * rendering.
- *
- * @param {number} index The index of the row being rendered.
- * @return {string} an empty string.
- */
- _reportRenderedRow(index) {
- if (index === this._shownFiles.length - 1) {
- this.async(() => {
- this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
- RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
- }, 1);
- }
- return '';
- }
-
- _reviewedTitle(reviewed) {
- if (reviewed) {
- return 'Mark as not reviewed (shortcut: r)';
- }
-
- return 'Mark as reviewed (shortcut: r)';
- }
-
- _handleReloadingDiffPreference() {
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ _scopedKeydownHandler(e) {
+ if (e.keyCode === 13) {
+ // Enter.
+ this._handleOpenFile(e);
}
}
- customElements.define(GrFileList.is, GrFileList);
-})();
+ reload() {
+ if (!this.changeNum || !this.patchRange.patchNum) {
+ return Promise.resolve();
+ }
+
+ this._loading = true;
+
+ this.collapseAllDiffs();
+ const promises = [];
+
+ promises.push(this._getFiles().then(filesByPath => {
+ this._filesByPath = filesByPath;
+ }));
+ promises.push(this._getLoggedIn()
+ .then(loggedIn => this._loggedIn = loggedIn)
+ .then(loggedIn => {
+ if (!loggedIn) { return; }
+
+ return this._getReviewedFiles().then(reviewed => {
+ this._reviewed = reviewed;
+ });
+ }));
+
+ promises.push(this._getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ }));
+
+ promises.push(this._getPreferences().then(prefs => {
+ this._userPrefs = prefs;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
+ this._detectChromiteButler();
+ this.$.reporting.fileListDisplayed();
+ });
+ }
+
+ _detectChromiteButler() {
+ const hasButler = !!document.getElementById('butler-suggested-owners');
+ if (hasButler) {
+ this.$.reporting.reportExtension('butler');
+ }
+ }
+
+ get diffs() {
+ const diffs = dom(this.root).querySelectorAll('gr-diff-host');
+ // It is possible that a bogus diff element is hanging around invisibly
+ // from earlier with a different patch set choice and associated with a
+ // different entry in the files array. So filter on visible items only.
+ return Array.from(diffs).filter(
+ el => !!el && !!el.style && el.style.display !== 'none');
+ }
+
+ openDiffPrefs() {
+ this.$.diffPreferencesDialog.open();
+ }
+
+ _calculatePatchChange(files) {
+ const magicFilesExcluded = files.filter(files =>
+ !this.isMagicPath(files.__path)
+ );
+
+ return magicFilesExcluded.reduce((acc, obj) => {
+ const inserted = obj.lines_inserted ? obj.lines_inserted : 0;
+ const deleted = obj.lines_deleted ? obj.lines_deleted : 0;
+ const total_size = (obj.size && obj.binary) ? obj.size : 0;
+ const size_delta_inserted =
+ obj.binary && obj.size_delta > 0 ? obj.size_delta : 0;
+ const size_delta_deleted =
+ obj.binary && obj.size_delta < 0 ? obj.size_delta : 0;
+
+ return {
+ inserted: acc.inserted + inserted,
+ deleted: acc.deleted + deleted,
+ size_delta_inserted: acc.size_delta_inserted + size_delta_inserted,
+ size_delta_deleted: acc.size_delta_deleted + size_delta_deleted,
+ total_size: acc.total_size + total_size,
+ };
+ }, {inserted: 0, deleted: 0, size_delta_inserted: 0,
+ size_delta_deleted: 0, total_size: 0});
+ }
+
+ _getDiffPreferences() {
+ return this.$.restAPI.getDiffPreferences();
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ _togglePathExpanded(path) {
+ // Is the path in the list of expanded diffs? IF so remove it, otherwise
+ // add it to the list.
+ const pathIndex = this._expandedFilePaths.indexOf(path);
+ if (pathIndex === -1) {
+ this.push('_expandedFilePaths', path);
+ } else {
+ this.splice('_expandedFilePaths', pathIndex, 1);
+ }
+ }
+
+ _togglePathExpandedByIndex(index) {
+ this._togglePathExpanded(this._files[index].__path);
+ }
+
+ _updateDiffPreferences() {
+ if (!this.diffs.length) { return; }
+ // Re-render all expanded diffs sequentially.
+ this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+ this._renderInOrder(this._expandedFilePaths, this.diffs,
+ this._expandedFilePaths.length);
+ }
+
+ _forEachDiff(fn) {
+ const diffs = this.diffs;
+ for (let i = 0; i < diffs.length; i++) {
+ fn(diffs[i]);
+ }
+ }
+
+ expandAllDiffs() {
+ this._showInlineDiffs = true;
+
+ // Find the list of paths that are in the file list, but not in the
+ // expanded list.
+ const newPaths = [];
+ let path;
+ for (let i = 0; i < this._shownFiles.length; i++) {
+ path = this._shownFiles[i].__path;
+ if (!this._expandedFilePaths.includes(path)) {
+ newPaths.push(path);
+ }
+ }
+
+ this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
+ }
+
+ collapseAllDiffs() {
+ this._showInlineDiffs = false;
+ this._expandedFilePaths = [];
+ this.filesExpanded = this._computeExpandedFiles(
+ this._expandedFilePaths.length, this._files.length);
+ this.$.diffCursor.handleDiffUpdate();
+ }
+
+ /**
+ * Computes a string with the number of comments and unresolved comments.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeCommentsString(changeComments, patchRange, path) {
+ const unresolvedCount =
+ changeComments.computeUnresolvedNum(patchRange.basePatchNum, path) +
+ changeComments.computeUnresolvedNum(patchRange.patchNum, path);
+ const commentCount =
+ changeComments.computeCommentCount(patchRange.basePatchNum, path) +
+ changeComments.computeCommentCount(patchRange.patchNum, path);
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ return commentString +
+ // Add a space if both comments and unresolved
+ (commentString && unresolvedString ? ' ' : '') +
+ // Add parentheses around unresolved if it exists.
+ (unresolvedString ? `(${unresolvedString})` : '');
+ }
+
+ /**
+ * Computes a string with the number of drafts.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeDraftsString(changeComments, patchRange, path) {
+ const draftCount =
+ changeComments.computeDraftCount(patchRange.basePatchNum, path) +
+ changeComments.computeDraftCount(patchRange.patchNum, path);
+ return GrCountStringFormatter.computePluralString(draftCount, 'draft');
+ }
+
+ /**
+ * Computes a shortened string with the number of drafts.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeDraftsStringMobile(changeComments, patchRange, path) {
+ const draftCount =
+ changeComments.computeDraftCount(patchRange.basePatchNum, path) +
+ changeComments.computeDraftCount(patchRange.patchNum, path);
+ return GrCountStringFormatter.computeShortString(draftCount, 'd');
+ }
+
+ /**
+ * Computes a shortened string with the number of comments.
+ *
+ * @param {!Object} changeComments
+ * @param {!Object} patchRange
+ * @param {string} path
+ * @return {string}
+ */
+ _computeCommentsStringMobile(changeComments, patchRange, path) {
+ const commentCount =
+ changeComments.computeCommentCount(patchRange.basePatchNum, path) +
+ changeComments.computeCommentCount(patchRange.patchNum, path);
+ return GrCountStringFormatter.computeShortString(commentCount, 'c');
+ }
+
+ /**
+ * @param {string} path
+ * @param {boolean=} opt_reviewed
+ */
+ _reviewFile(path, opt_reviewed) {
+ if (this.editMode) { return; }
+ const index = this._files.findIndex(file => file.__path === path);
+ const reviewed = opt_reviewed || !this._files[index].isReviewed;
+
+ this.set(['_files', index, 'isReviewed'], reviewed);
+ if (index < this._shownFiles.length) {
+ this.notifyPath(`_shownFiles.${index}.isReviewed`);
+ }
+
+ this._saveReviewedState(path, reviewed);
+ }
+
+ _saveReviewedState(path, reviewed) {
+ return this.$.restAPI.saveFileReviewed(this.changeNum,
+ this.patchRange.patchNum, path, reviewed);
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getReviewedFiles() {
+ if (this.editMode) { return Promise.resolve([]); }
+ return this.$.restAPI.getReviewedFiles(this.changeNum,
+ this.patchRange.patchNum);
+ }
+
+ _getFiles() {
+ return this.$.restAPI.getChangeOrEditFiles(
+ this.changeNum, this.patchRange);
+ }
+
+ /**
+ * The closure compiler doesn't realize this.specialFilePathCompare is
+ * valid.
+ *
+ * @suppress {checkTypes}
+ */
+ _normalizeChangeFilesResponse(response) {
+ if (!response) { return []; }
+ const paths = Object.keys(response).sort(this.specialFilePathCompare);
+ const files = [];
+ for (let i = 0; i < paths.length; i++) {
+ const info = response[paths[i]];
+ info.__path = paths[i];
+ info.lines_inserted = info.lines_inserted || 0;
+ info.lines_deleted = info.lines_deleted || 0;
+ files.push(info);
+ }
+ return files;
+ }
+
+ /**
+ * Handle all events from the file list dom-repeat so event handleers don't
+ * have to get registered for potentially very long lists.
+ */
+ _handleFileListClick(e) {
+ // Traverse upwards to find the row element if the target is not the row.
+ let row = e.target;
+ while (!row.classList.contains('row') && row.parentElement) {
+ row = row.parentElement;
+ }
+
+ const path = row.dataset.path;
+ // Handle checkbox mark as reviewed.
+ if (e.target.classList.contains('markReviewed')) {
+ e.preventDefault();
+ return this._reviewFile(path);
+ }
+
+ // If a path cannot be interpreted from the click target (meaning it's not
+ // somewhere in the row, e.g. diff content) or if the user clicked the
+ // link, defer to the native behavior.
+ if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; }
+
+ // Disregard the event if the click target is in the edit controls.
+ if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
+
+ e.preventDefault();
+ this._togglePathExpanded(path);
+ }
+
+ _handleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.diffCursor.moveLeft();
+ }
+
+ _handleRightPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ this.$.diffCursor.moveRight();
+ }
+
+ _handleToggleInlineDiff(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e) ||
+ this.$.fileCursor.index === -1) { return; }
+
+ e.preventDefault();
+ this._togglePathExpandedByIndex(this.$.fileCursor.index);
+ }
+
+ _handleToggleAllInlineDiffs(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this._toggleInlineDiffs();
+ }
+
+ _handleCursorNext(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ if (this._showInlineDiffs) {
+ e.preventDefault();
+ this.$.diffCursor.moveDown();
+ this._displayLine = true;
+ } else {
+ // Down key
+ if (this.getKeyboardEvent(e).keyCode === 40) { return; }
+ e.preventDefault();
+ this.$.fileCursor.next();
+ this.selectedIndex = this.$.fileCursor.index;
+ }
+ }
+
+ _handleCursorPrev(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ if (this._showInlineDiffs) {
+ e.preventDefault();
+ this.$.diffCursor.moveUp();
+ this._displayLine = true;
+ } else {
+ // Up key
+ if (this.getKeyboardEvent(e).keyCode === 38) { return; }
+ e.preventDefault();
+ this.$.fileCursor.previous();
+ this.selectedIndex = this.$.fileCursor.index;
+ }
+ }
+
+ _handleNewComment(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+ this.$.diffCursor.createCommentInPlace();
+ }
+
+ _handleOpenLastFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._openSelectedFile(this._files.length - 1);
+ }
+
+ _handleOpenFirstFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._openSelectedFile(0);
+ }
+
+ _handleOpenFile(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+
+ if (this._showInlineDiffs) {
+ this._openCursorFile();
+ return;
+ }
+
+ this._openSelectedFile();
+ }
+
+ _handleNextChunk(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
+ this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ if (this.isModifierPressed(e, 'shiftKey')) {
+ this.$.diffCursor.moveToNextCommentThread();
+ } else {
+ this.$.diffCursor.moveToNextChunk();
+ }
+ }
+
+ _handlePrevChunk(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
+ this._noDiffsExpanded()) {
+ return;
+ }
+
+ e.preventDefault();
+ if (this.isModifierPressed(e, 'shiftKey')) {
+ this.$.diffCursor.moveToPreviousCommentThread();
+ } else {
+ this.$.diffCursor.moveToPreviousChunk();
+ }
+ }
+
+ _handleToggleFileReviewed(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ if (!this._files[this.$.fileCursor.index]) { return; }
+ this._reviewFile(this._files[this.$.fileCursor.index].__path);
+ }
+
+ _handleToggleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this._forEachDiff(diff => {
+ diff.toggleLeftDiff();
+ });
+ }
+
+ _toggleInlineDiffs() {
+ if (this._showInlineDiffs) {
+ this.collapseAllDiffs();
+ } else {
+ this.expandAllDiffs();
+ }
+ }
+
+ _openCursorFile() {
+ const diff = this.$.diffCursor.getTargetDiffElement();
+ Gerrit.Nav.navigateToDiff(this.change, diff.path,
+ diff.patchRange.patchNum, this.patchRange.basePatchNum);
+ }
+
+ /**
+ * @param {number=} opt_index
+ */
+ _openSelectedFile(opt_index) {
+ if (opt_index != null) {
+ this.$.fileCursor.setCursorAtIndex(opt_index);
+ }
+ if (!this._files[this.$.fileCursor.index]) { return; }
+ Gerrit.Nav.navigateToDiff(this.change,
+ this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum,
+ this.patchRange.basePatchNum);
+ }
+
+ _addDraftAtTarget() {
+ const diff = this.$.diffCursor.getTargetDiffElement();
+ const target = this.$.diffCursor.getTargetLineElement();
+ if (diff && target) {
+ diff.addDraftAtLine(target);
+ }
+ }
+
+ _shouldHideChangeTotals(_patchChange) {
+ return _patchChange.inserted === 0 && _patchChange.deleted === 0;
+ }
+
+ _shouldHideBinaryChangeTotals(_patchChange) {
+ return _patchChange.size_delta_inserted === 0 &&
+ _patchChange.size_delta_deleted === 0;
+ }
+
+ _computeFileStatus(status) {
+ return status || 'M';
+ }
+
+ _computeDiffURL(change, patchRange, path, editMode) {
+ // Polymer 2: check for undefined
+ if ([change, patchRange, path, editMode]
+ .some(arg => arg === undefined)) {
+ return;
+ }
+ // TODO(kaspern): Fix editing for commit messages and merge lists.
+ if (editMode && path !== this.COMMIT_MESSAGE_PATH &&
+ path !== this.MERGE_LIST_PATH) {
+ return Gerrit.Nav.getEditUrlForDiff(change, path, patchRange.patchNum,
+ patchRange.basePatchNum);
+ }
+ return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
+ patchRange.basePatchNum);
+ }
+
+ _formatBytes(bytes) {
+ if (bytes == 0) return '+/-0 B';
+ const bits = 1024;
+ const decimals = 1;
+ const sizes =
+ ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+ const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits));
+ const prepend = bytes > 0 ? '+' : '';
+ return prepend + parseFloat((bytes / Math.pow(bits, exponent))
+ .toFixed(decimals)) + ' ' + sizes[exponent];
+ }
+
+ _formatPercentage(size, delta) {
+ const oldSize = size - delta;
+
+ if (oldSize === 0) { return ''; }
+
+ const percentage = Math.round(Math.abs(delta * 100 / oldSize));
+ return '(' + (delta > 0 ? '+' : '-') + percentage + '%)';
+ }
+
+ _computeBinaryClass(delta) {
+ if (delta === 0) { return; }
+ return delta >= 0 ? 'added' : 'removed';
+ }
+
+ /**
+ * @param {string} baseClass
+ * @param {string} path
+ */
+ _computeClass(baseClass, path) {
+ const classes = [];
+ if (baseClass) {
+ classes.push(baseClass);
+ }
+ if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
+ classes.push('invisible');
+ }
+ return classes.join(' ');
+ }
+
+ _computePathClass(path, expandedFilesRecord) {
+ return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
+ }
+
+ _computeShowHideIcon(path, expandedFilesRecord) {
+ return this._isFileExpanded(path, expandedFilesRecord) ?
+ 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ }
+
+ _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) {
+ // Polymer 2: check for undefined
+ if ([
+ filesByPath,
+ changeComments,
+ patchRange,
+ reviewed,
+ loading,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ // Await all promises resolving from reload. @See Issue 9057
+ if (loading || !changeComments) { return; }
+
+ const commentedPaths = changeComments.getPaths(patchRange);
+ const files = Object.assign({}, filesByPath);
+ Object.keys(commentedPaths).forEach(commentedPath => {
+ if (files.hasOwnProperty(commentedPath)) { return; }
+ files[commentedPath] = {status: 'U'};
+ });
+ const reviewedSet = new Set(reviewed || []);
+ for (const filePath in files) {
+ if (!files.hasOwnProperty(filePath)) { continue; }
+ files[filePath].isReviewed = reviewedSet.has(filePath);
+ }
+
+ this._files = this._normalizeChangeFilesResponse(files);
+ }
+
+ _computeFilesShown(numFilesShown, files) {
+ // Polymer 2: check for undefined
+ if ([numFilesShown, files].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const previousNumFilesShown = this._shownFiles ?
+ this._shownFiles.length : 0;
+
+ const filesShown = files.slice(0, numFilesShown);
+ this.fire('files-shown-changed', {length: filesShown.length});
+
+ // Start the timer for the rendering work hwere because this is where the
+ // _shownFiles property is being set, and _shownFiles is used in the
+ // dom-repeat binding.
+ this.$.reporting.time(RENDER_TIMING_LABEL);
+
+ // How many more files are being shown (if it's an increase).
+ this._reportinShownFilesIncrement =
+ Math.max(0, filesShown.length - previousNumFilesShown);
+
+ return filesShown;
+ }
+
+ _updateDiffCursor() {
+ // Overwrite the cursor's list of diffs:
+ this.$.diffCursor.splice(
+ ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs));
+ }
+
+ _filesChanged() {
+ if (this._files && this._files.length > 0) {
+ flush();
+ const files = Array.from(
+ dom(this.root).querySelectorAll('.file-row'));
+ this.$.fileCursor.stops = files;
+ this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
+ }
+ }
+
+ _incrementNumFilesShown() {
+ this.numFilesShown += this.fileListIncrement;
+ }
+
+ _computeFileListControlClass(numFilesShown, files) {
+ return numFilesShown >= files.length ? 'invisible' : '';
+ }
+
+ _computeIncrementText(numFilesShown, files) {
+ if (!files) { return ''; }
+ const text =
+ Math.min(this.fileListIncrement, files.length - numFilesShown);
+ return 'Show ' + text + ' more';
+ }
+
+ _computeShowAllText(files) {
+ if (!files) { return ''; }
+ return 'Show all ' + files.length + ' files';
+ }
+
+ _computeWarnShowAll(files) {
+ return files.length > WARN_SHOW_ALL_THRESHOLD;
+ }
+
+ _computeShowAllWarning(files) {
+ if (!this._computeWarnShowAll(files)) { return ''; }
+ return 'Warning: showing all ' + files.length +
+ ' files may take several seconds.';
+ }
+
+ _showAllFiles() {
+ this.numFilesShown = this._files.length;
+ }
+
+ _computePatchSetDescription(revisions, patchNum) {
+ // Polymer 2: check for undefined
+ if ([revisions, patchNum].some(arg => arg === undefined)) {
+ return '';
+ }
+
+ const rev = this.getRevisionByPatchNum(revisions, patchNum);
+ return (rev && rev.description) ?
+ rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
+ }
+
+ /**
+ * Get a descriptive label for use in the status indicator's tooltip and
+ * ARIA label.
+ *
+ * @param {string} status
+ * @return {string}
+ */
+ _computeFileStatusLabel(status) {
+ const statusCode = this._computeFileStatus(status);
+ return FileStatus.hasOwnProperty(statusCode) ?
+ FileStatus[statusCode] : 'Status Unknown';
+ }
+
+ _isFileExpanded(path, expandedFilesRecord) {
+ return expandedFilesRecord.base.includes(path);
+ }
+
+ _onLineSelected(e, detail) {
+ this.$.diffCursor.moveToLineNumber(detail.number, detail.side,
+ detail.path);
+ }
+
+ _computeExpandedFiles(expandedCount, totalCount) {
+ if (expandedCount === 0) {
+ return GrFileListConstants.FilesExpandedState.NONE;
+ } else if (expandedCount === totalCount) {
+ return GrFileListConstants.FilesExpandedState.ALL;
+ }
+ return GrFileListConstants.FilesExpandedState.SOME;
+ }
+
+ /**
+ * Handle splices to the list of expanded file paths. If there are any new
+ * entries in the expanded list, then render each diff corresponding in
+ * order by waiting for the previous diff to finish before starting the next
+ * one.
+ *
+ * @param {!Array} record The splice record in the expanded paths list.
+ */
+ _expandedPathsChanged(record) {
+ // Clear content for any diffs that are not open so if they get re-opened
+ // the stale content does not flash before it is cleared and reloaded.
+ const collapsedDiffs = this.diffs.filter(diff =>
+ this._expandedFilePaths.indexOf(diff.path) === -1);
+ this._clearCollapsedDiffs(collapsedDiffs);
+
+ if (!record) { return; } // Happens after "Collapse all" clicked.
+
+ this.filesExpanded = this._computeExpandedFiles(
+ this._expandedFilePaths.length, this._files.length);
+
+ // Find the paths introduced by the new index splices:
+ const newPaths = record.indexSplices
+ .map(splice => splice.object.slice(
+ splice.index, splice.index + splice.addedCount))
+ .reduce((acc, paths) => acc.concat(paths), []);
+
+ // Required so that the newly created diff view is included in this.diffs.
+ flush();
+
+ this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+
+ if (newPaths.length) {
+ this._renderInOrder(newPaths, this.diffs, newPaths.length);
+ }
+
+ this._updateDiffCursor();
+ this.$.diffCursor.handleDiffUpdate();
+ }
+
+ _clearCollapsedDiffs(collapsedDiffs) {
+ for (const diff of collapsedDiffs) {
+ diff.cancel();
+ diff.clearDiffContent();
+ }
+ }
+
+ /**
+ * Given an array of paths and a NodeList of diff elements, render the diff
+ * for each path in order, awaiting the previous render to complete before
+ * continung.
+ *
+ * @param {!Array<string>} paths
+ * @param {!NodeList<!Object>} diffElements (GrDiffHostElement)
+ * @param {number} initialCount The total number of paths in the pass. This
+ * is used to generate log messages.
+ * @return {!Promise}
+ */
+ _renderInOrder(paths, diffElements, initialCount) {
+ let iter = 0;
+
+ return (new Promise(resolve => {
+ this.fire('reload-drafts', {resolve});
+ })).then(() => this.asyncForeach(paths, (path, cancel) => {
+ this._cancelForEachDiff = cancel;
+
+ iter++;
+ console.log('Expanding diff', iter, 'of', initialCount, ':',
+ path);
+ const diffElem = this._findDiffByPath(path, diffElements);
+ if (!diffElem) {
+ console.warn(`Did not find <gr-diff-host> element for ${path}`);
+ return Promise.resolve();
+ }
+ diffElem.comments = this.changeComments.getCommentsBySideForPath(
+ path, this.patchRange, this.projectConfig);
+ const promises = [diffElem.reload()];
+ if (this._loggedIn && !this.diffPrefs.manual_review) {
+ promises.push(this._reviewFile(path, true));
+ }
+ return Promise.all(promises);
+ }).then(() => {
+ this._cancelForEachDiff = null;
+ this._nextRenderParams = null;
+ console.log('Finished expanding', initialCount, 'diff(s)');
+ this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
+ EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
+ this.$.diffCursor.handleDiffUpdate();
+ }));
+ }
+
+ /** Cancel the rendering work of every diff in the list */
+ _cancelDiffs() {
+ if (this._cancelForEachDiff) { this._cancelForEachDiff(); }
+ this._forEachDiff(d => d.cancel());
+ }
+
+ /**
+ * In the given NodeList of diff elements, find the diff for the given path.
+ *
+ * @param {string} path
+ * @param {!NodeList<!Object>} diffElements (GrDiffElement)
+ * @return {!Object|undefined} (GrDiffElement)
+ */
+ _findDiffByPath(path, diffElements) {
+ for (let i = 0; i < diffElements.length; i++) {
+ if (diffElements[i].path === path) {
+ return diffElements[i];
+ }
+ }
+ }
+
+ /**
+ * Reset the comments of a modified thread
+ *
+ * @param {string} rootId
+ * @param {string} path
+ */
+ reloadCommentsForThreadWithRootId(rootId, path) {
+ // Don't bother continuing if we already know that the path that contains
+ // the updated comment thread is not expanded.
+ if (!this._expandedFilePaths.includes(path)) { return; }
+ const diff = this.diffs.find(d => d.path === path);
+
+ const threadEl = diff.getThreadEls().find(t => t.rootId === rootId);
+ if (!threadEl) { return; }
+
+ const newComments = this.changeComments.getCommentsForThread(rootId);
+
+ // If newComments is null, it means that a single draft was
+ // removed from a thread in the thread view, and the thread should
+ // no longer exist. Remove the existing thread element in the diff
+ // view.
+ if (!newComments) {
+ threadEl.fireRemoveSelf();
+ return;
+ }
+
+ // Comments are not returned with the commentSide attribute from
+ // the api, but it's necessary to be stored on the diff's
+ // comments due to use in the _handleCommentUpdate function.
+ // The comment thread already has a side associated with it, so
+ // set the comment's side to match.
+ threadEl.comments = newComments.map(c => Object.assign(
+ c, {__commentSide: threadEl.commentSide}
+ ));
+ flush();
+ return;
+ }
+
+ _handleEscKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+ this._displayLine = false;
+ }
+
+ /**
+ * Update the loading class for the file list rows. The update is inside a
+ * debouncer so that the file list doesn't flash gray when the API requests
+ * are reasonably fast.
+ *
+ * @param {boolean} loading
+ */
+ _loadingChanged(loading) {
+ this.debounce('loading-change', () => {
+ // Only show set the loading if there have been files loaded to show. In
+ // this way, the gray loading style is not shown on initial loads.
+ this.classList.toggle('loading', loading && !!this._files.length);
+ }, LOADING_DEBOUNCE_INTERVAL);
+ }
+
+ _editModeChanged(editMode) {
+ this.classList.toggle('editMode', editMode);
+ }
+
+ _computeReviewedClass(isReviewed) {
+ return isReviewed ? 'isReviewed' : '';
+ }
+
+ _computeReviewedText(isReviewed) {
+ return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
+ }
+
+ /**
+ * Given a file path, return whether that path should have visible size bars
+ * and be included in the size bars calculation.
+ *
+ * @param {string} path
+ * @return {boolean}
+ */
+ _showBarsForPath(path) {
+ return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH;
+ }
+
+ /**
+ * Compute size bar layout values from the file list.
+ *
+ * @return {Gerrit.LayoutStats|undefined}
+ *
+ */
+ _computeSizeBarLayout(shownFilesRecord) {
+ if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; }
+ const stats = {
+ maxInserted: 0,
+ maxDeleted: 0,
+ maxAdditionWidth: 0,
+ maxDeletionWidth: 0,
+ deletionOffset: 0,
+ };
+ shownFilesRecord.base
+ .filter(f => this._showBarsForPath(f.__path))
+ .forEach(f => {
+ if (f.lines_inserted) {
+ stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted);
+ }
+ if (f.lines_deleted) {
+ stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted);
+ }
+ });
+ const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted);
+ if (!isNaN(ratio)) {
+ stats.maxAdditionWidth =
+ (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio;
+ stats.maxDeletionWidth =
+ SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
+ stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH;
+ }
+ return stats;
+ }
+
+ /**
+ * Get the width of the addition bar for a file.
+ *
+ * @param {Object} file
+ * @param {Gerrit.LayoutStats} stats
+ * @return {number}
+ */
+ _computeBarAdditionWidth(file, stats) {
+ if (stats.maxInserted === 0 ||
+ !file.lines_inserted ||
+ !this._showBarsForPath(file.__path)) {
+ return 0;
+ }
+ const width =
+ stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted;
+ return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
+ }
+
+ /**
+ * Get the x-offset of the addition bar for a file.
+ *
+ * @param {Object} file
+ * @param {Gerrit.LayoutStats} stats
+ * @return {number}
+ */
+ _computeBarAdditionX(file, stats) {
+ return stats.maxAdditionWidth -
+ this._computeBarAdditionWidth(file, stats);
+ }
+
+ /**
+ * Get the width of the deletion bar for a file.
+ *
+ * @param {Object} file
+ * @param {Gerrit.LayoutStats} stats
+ * @return {number}
+ */
+ _computeBarDeletionWidth(file, stats) {
+ if (stats.maxDeleted === 0 ||
+ !file.lines_deleted ||
+ !this._showBarsForPath(file.__path)) {
+ return 0;
+ }
+ const width =
+ stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted;
+ return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width);
+ }
+
+ /**
+ * Get the x-offset of the deletion bar for a file.
+ *
+ * @param {Gerrit.LayoutStats} stats
+ *
+ * @return {number}
+ */
+ _computeBarDeletionX(stats) {
+ return stats.deletionOffset;
+ }
+
+ _computeShowSizeBars(userPrefs) {
+ return !!userPrefs.size_bar_in_change_table;
+ }
+
+ _computeSizeBarsClass(showSizeBars, path) {
+ let hideClass = '';
+ if (!showSizeBars) {
+ hideClass = 'hide';
+ } else if (!this._showBarsForPath(path)) {
+ hideClass = 'invisible';
+ }
+ return `sizeBars desktop ${hideClass}`;
+ }
+
+ /**
+ * Shows registered dynamic columns iff the 'header', 'content' and
+ * 'summary' endpoints are regiestered the exact same number of times.
+ * Ideally, there should be a better way to enforce the expectation of the
+ * dependencies between dynamic endpoints.
+ */
+ _computeShowDynamicColumns(
+ headerEndpoints, contentEndpoints, summaryEndpoints) {
+ return headerEndpoints && contentEndpoints && summaryEndpoints &&
+ headerEndpoints.length === contentEndpoints.length &&
+ headerEndpoints.length === summaryEndpoints.length;
+ }
+
+ /**
+ * Returns true if none of the inline diffs have been expanded.
+ *
+ * @return {boolean}
+ */
+ _noDiffsExpanded() {
+ return this.filesExpanded === GrFileListConstants.FilesExpandedState.NONE;
+ }
+
+ /**
+ * Method to call via binding when each file list row is rendered. This
+ * allows approximate detection of when the dom-repeat has completed
+ * rendering.
+ *
+ * @param {number} index The index of the row being rendered.
+ * @return {string} an empty string.
+ */
+ _reportRenderedRow(index) {
+ if (index === this._shownFiles.length - 1) {
+ this.async(() => {
+ this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
+ RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
+ }, 1);
+ }
+ return '';
+ }
+
+ _reviewedTitle(reviewed) {
+ if (reviewed) {
+ return 'Mark as not reviewed (shortcut: r)';
+ }
+
+ return 'Mark as reviewed (shortcut: r)';
+ }
+
+ _handleReloadingDiffPreference() {
+ this._getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ });
+ }
+}
+
+customElements.define(GrFileList.is, GrFileList);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
index 289e3f4..9652156 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -1,46 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html">
-<link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.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="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
-<link rel="import" href="../../diff/gr-diff-host/gr-diff-host.html">
-<link rel="import" href="../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
-<link rel="import" href="../../edit/gr-edit-file-controls/gr-edit-file-controls.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../gr-file-list-constants.html">
-
-<dom-module id="gr-file-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -298,9 +274,7 @@
}
}
</style>
- <div
- id="container"
- on-click="_handleFileListClick">
+ <div id="container" on-click="_handleFileListClick">
<div class="header-row row">
<div class="status"></div>
<div class="path">File</div>
@@ -309,55 +283,38 @@
<div class="header-stats">Delta</div>
<template is="dom-if" if="[[_showDynamicColumns]]">
<template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" as="headerEndpoint">
- <gr-endpoint-decorator name$="[[headerEndpoint]]">
+ <gr-endpoint-decorator name\$="[[headerEndpoint]]">
</gr-endpoint-decorator>
</template>
</template>
<!-- Empty div here exists to keep spacing in sync with file rows. -->
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
+ <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]"></div>
<div class="editFileControls showOnEdit"></div>
<div class="show-hide"></div>
</div>
- <template is="dom-repeat"
- items="[[_shownFiles]]"
- id="files"
- as="file"
- initial-count="[[fileListIncrement]]"
- target-framerate="1">
+ <template is="dom-repeat" items="[[_shownFiles]]" id="files" as="file" initial-count="[[fileListIncrement]]" target-framerate="1">
[[_reportRenderedRow(index)]]
<div class="stickyArea">
- <div class$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]"
- data-path$="[[file.__path]]" tabindex="-1">
- <div class$="[[_computeClass('status', file.__path)]]"
- tabindex="0"
- title$="[[_computeFileStatusLabel(file.status)]]"
- aria-label$="[[_computeFileStatusLabel(file.status)]]">
+ <div class\$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" tabindex="-1">
+ <div class\$="[[_computeClass('status', file.__path)]]" tabindex="0" title\$="[[_computeFileStatusLabel(file.status)]]" aria-label\$="[[_computeFileStatusLabel(file.status)]]">
[[_computeFileStatus(file.status)]]
</div>
<!-- TODO: Remove data-url as it appears its not used -->
- <span
- data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
- class="path">
- <a class="pathLink" href$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]">
- <span title$="[[computeDisplayPath(file.__path)]]"
- class="fullFileName">
+ <span data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]" class="path">
+ <a class="pathLink" href\$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]">
+ <span title\$="[[computeDisplayPath(file.__path)]]" class="fullFileName">
[[computeDisplayPath(file.__path)]]
</span>
- <span title$="[[computeDisplayPath(file.__path)]]"
- class="truncatedFileName">
+ <span title\$="[[computeDisplayPath(file.__path)]]" class="truncatedFileName">
[[computeTruncatedPath(file.__path)]]
</span>
- <gr-copy-clipboard
- hide-input
- text="[[file.__path]]"></gr-copy-clipboard>
+ <gr-copy-clipboard hide-input="" text="[[file.__path]]"></gr-copy-clipboard>
</a>
<template is="dom-if" if="[[file.old_path]]">
- <div class="oldPath" title$="[[file.old_path]]">
+ <div class="oldPath" title\$="[[file.old_path]]">
[[file.old_path]]
- <gr-copy-clipboard
- hide-input
- text="[[file.old_path]]"></gr-copy-clipboard>
+ <gr-copy-clipboard hide-input="" text="[[file.old_path]]"></gr-copy-clipboard>
</div>
</template>
</span>
@@ -375,46 +332,27 @@
[[_computeCommentsStringMobile(changeComments, patchRange,
file.__path)]]
</div>
- <div class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]">
+ <div class\$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]">
<svg width="61" height="8">
- <rect
- x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
- y="0"
- height="8"
- fill="#388E3C"
- width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]" />
- <rect
- x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
- y="0"
- height="8"
- fill="#D32F2F"
- width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]" />
+ <rect x\$="[[_computeBarAdditionX(file, _sizeBarLayout)]]" y="0" height="8" fill="#388E3C" width\$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"></rect>
+ <rect x\$="[[_computeBarDeletionX(_sizeBarLayout)]]" y="0" height="8" fill="#D32F2F" width\$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"></rect>
</svg>
</div>
- <div class$="[[_computeClass('stats', file.__path)]]">
- <span
- class="added"
- tabindex="0"
- aria-label$="[[file.lines_inserted]] lines added"
- hidden$=[[file.binary]]>
+ <div class\$="[[_computeClass('stats', file.__path)]]">
+ <span class="added" tabindex="0" aria-label\$="[[file.lines_inserted]] lines added" hidden\$="[[file.binary]]">
+[[file.lines_inserted]]
</span>
- <span
- class="removed"
- tabindex="0"
- aria-label$="[[file.lines_deleted]] lines removed"
- hidden$=[[file.binary]]>
+ <span class="removed" tabindex="0" aria-label\$="[[file.lines_deleted]] lines removed" hidden\$="[[file.binary]]">
-[[file.lines_deleted]]
</span>
- <span class$="[[_computeBinaryClass(file.size_delta)]]"
- hidden$=[[!file.binary]]>
+ <span class\$="[[_computeBinaryClass(file.size_delta)]]" hidden\$="[[!file.binary]]">
[[_formatBytes(file.size_delta)]]
[[_formatPercentage(file.size, file.size_delta)]]
</span>
</div>
<template is="dom-if" if="[[_showDynamicColumns]]">
<template is="dom-repeat" items="[[_dynamicContentEndpoints]]" as="contentEndpoint">
- <div class$="[[_computeClass('', file.__path)]]">
+ <div class\$="[[_computeClass('', file.__path)]]">
<gr-endpoint-decorator name="[[contentEndpoint]]">
<gr-endpoint-param name="changeNum" value="[[changeNum]]">
</gr-endpoint-param>
@@ -426,66 +364,38 @@
</div>
</template>
</template>
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden>
- <span class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span>
+ <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]" hidden="">
+ <span class\$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span>
<label>
<input class="reviewed" type="checkbox" checked="[[file.isReviewed]]">
- <span class="markReviewed" title$="[[_reviewedTitle(file.isReviewed)]]">[[_computeReviewedText(file.isReviewed)]]</span>
+ <span class="markReviewed" title\$="[[_reviewedTitle(file.isReviewed)]]">[[_computeReviewedText(file.isReviewed)]]</span>
</label>
</div>
<div class="editFileControls showOnEdit">
<template is="dom-if" if="[[editMode]]">
- <gr-edit-file-controls
- class$="[[_computeClass('', file.__path)]]"
- file-path="[[file.__path]]"></gr-edit-file-controls>
+ <gr-edit-file-controls class\$="[[_computeClass('', file.__path)]]" file-path="[[file.__path]]"></gr-edit-file-controls>
</template>
</div>
<div class="show-hide">
- <label class="show-hide" data-path$="[[file.__path]]"
- data-expand=true>
- <input type="checkbox" class="show-hide"
- checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
- data-path$="[[file.__path]]" data-expand=true>
- <iron-icon
- id="icon"
- icon="[[_computeShowHideIcon(file.__path, _expandedFilePaths.*)]]">
+ <label class="show-hide" data-path\$="[[file.__path]]" data-expand="true">
+ <input type="checkbox" class="show-hide" checked\$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" data-expand="true">
+ <iron-icon id="icon" icon="[[_computeShowHideIcon(file.__path, _expandedFilePaths.*)]]">
</iron-icon>
</label>
</div>
</div>
- <template is="dom-if"
- if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
- <gr-diff-host
- no-auto-render
- show-load-failure
- display-line="[[_displayLine]]"
- hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
- change-num="[[changeNum]]"
- patch-range="[[patchRange]]"
- path="[[file.__path]]"
- prefs="[[diffPrefs]]"
- project-name="[[change.project]]"
- on-line-selected="_onLineSelected"
- no-render-on-prefs-change
- view-mode="[[diffViewMode]]"></gr-diff-host>
+ <template is="dom-if" if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
+ <gr-diff-host no-auto-render="" show-load-failure="" display-line="[[_displayLine]]" hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" change-num="[[changeNum]]" patch-range="[[patchRange]]" path="[[file.__path]]" prefs="[[diffPrefs]]" project-name="[[change.project]]" on-line-selected="_onLineSelected" no-render-on-prefs-change="" view-mode="[[diffViewMode]]"></gr-diff-host>
</template>
</div>
</template>
</div>
- <div
- class="row totalChanges"
- hidden$="[[_hideChangeTotals]]">
+ <div class="row totalChanges" hidden\$="[[_hideChangeTotals]]">
<div class="total-stats">
- <span
- class="added"
- tabindex="0"
- aria-label$="[[_patchChange.inserted]] lines added">
+ <span class="added" tabindex="0" aria-label\$="[[_patchChange.inserted]] lines added">
+[[_patchChange.inserted]]
</span>
- <span
- class="removed"
- tabindex="0"
- aria-label$="[[_patchChange.deleted]] lines removed">
+ <span class="removed" tabindex="0" aria-label\$="[[_patchChange.deleted]] lines removed">
-[[_patchChange.deleted]]
</span>
</div>
@@ -496,13 +406,11 @@
</template>
</template>
<!-- Empty div here exists to keep spacing in sync with file rows. -->
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
+ <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]"></div>
<div class="editFileControls showOnEdit"></div>
<div class="show-hide"></div>
</div>
- <div
- class="row totalChanges"
- hidden$="[[_hideBinaryChangeTotals]]">
+ <div class="row totalChanges" hidden\$="[[_hideBinaryChangeTotals]]">
<div class="total-stats">
<span class="added" aria-label="Total lines added">
[[_formatBytes(_patchChange.size_delta_inserted)]]
@@ -516,39 +424,21 @@
</span>
</div>
</div>
- <div class$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]">
- <gr-button
- class="fileListButton"
- id="incrementButton"
- link on-click="_incrementNumFilesShown">
+ <div class\$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]">
+ <gr-button class="fileListButton" id="incrementButton" link="" on-click="_incrementNumFilesShown">
[[_computeIncrementText(numFilesShown, _files)]]
</gr-button>
- <gr-tooltip-content
- has-tooltip="[[_computeWarnShowAll(_files)]]"
- show-icon="[[_computeWarnShowAll(_files)]]"
- title$="[[_computeShowAllWarning(_files)]]">
- <gr-button
- class="fileListButton"
- id="showAllButton"
- link on-click="_showAllFiles">
+ <gr-tooltip-content has-tooltip="[[_computeWarnShowAll(_files)]]" show-icon="[[_computeWarnShowAll(_files)]]" title\$="[[_computeShowAllWarning(_files)]]">
+ <gr-button class="fileListButton" id="showAllButton" link="" on-click="_showAllFiles">
[[_computeShowAllText(_files)]]
</gr-button><!--
--></gr-tooltip-content>
</div>
- <gr-diff-preferences-dialog
- id="diffPreferencesDialog"
- diff-prefs="{{diffPrefs}}"
- on-reload-diff-preference="_handleReloadingDiffPreference">
+ <gr-diff-preferences-dialog id="diffPreferencesDialog" diff-prefs="{{diffPrefs}}" on-reload-diff-preference="_handleReloadingDiffPreference">
</gr-diff-preferences-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-storage id="storage"></gr-storage>
<gr-diff-cursor id="diffCursor"></gr-diff-cursor>
- <gr-cursor-manager
- id="fileCursor"
- scroll-behavior="keep-visible"
- focus-on-move
- cursor-target-class="selected"></gr-cursor-manager>
+ <gr-cursor-manager id="fileCursor" scroll-behavior="keep-visible" focus-on-move="" cursor-target-class="selected"></gr-cursor-manager>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-file-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index af80ca8..84dfc9c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -19,21 +19,30 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list</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>
+<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 src="/components/web-component-tester/data/a11ySuite.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
-<script src="../../../scripts/util.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script src="/node_modules/page/page.js"></script>
+<script type="module" src="../../diff/gr-comment-api/gr-comment-api.js"></script>
+<script type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-file-list.html">
+<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script>
+<script type="module" src="./gr-file-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../../scripts/util.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-file-list.js';
+import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+void(0);
+</script>
<dom-module id="comment-api-mock">
<template>
@@ -42,7 +51,7 @@
on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
<gr-comment-api id="commentAPI"></gr-comment-api>
</template>
- <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
+ <script type="module" src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
</dom-module>
<test-fixture id="basic">
@@ -51,1389 +60,1790 @@
</template>
</test-fixture>
-<script>
- suite('gr-file-list tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../../scripts/util.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-file-list.js';
+import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-file-list tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+ kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+ kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+ kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
+ kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
+ kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
+ kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+ kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
- kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
- kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
- kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
- kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
- kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ let element;
+ let commentApiWrapper;
+ let sandbox;
+ let saveStub;
+ let loadCommentSpy;
- let element;
- let commentApiWrapper;
- let sandbox;
- let saveStub;
- let loadCommentSpy;
-
- suite('basic tests', () => {
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getPreferences() { return Promise.resolve({}); },
- getDiffPreferences() { return Promise.resolve({}); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- getAccountCapabilities() { return Promise.resolve({}); },
- });
- stub('gr-date-formatter', {
- _loadTimeFormat() { return Promise.resolve(''); },
- });
- stub('gr-diff-host', {
- reload() { return Promise.resolve(); },
- });
-
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.fileList;
- loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- commentApiWrapper.loadComments().then(() => {
- sandbox.stub(element.changeComments, 'getPaths').returns({});
- sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
- .returns({meta: {}, left: [], right: []});
- done();
- });
- element._loading = false;
- element.diffPrefs = {};
- element.numFilesShown = 200;
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- saveStub = sandbox.stub(element, '_saveReviewedState',
- () => Promise.resolve());
+ suite('basic tests', () => {
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getPreferences() { return Promise.resolve({}); },
+ getDiffPreferences() { return Promise.resolve({}); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ });
+ stub('gr-date-formatter', {
+ _loadTimeFormat() { return Promise.resolve(''); },
+ });
+ stub('gr-diff-host', {
+ reload() { return Promise.resolve(); },
});
- teardown(() => {
- sandbox.restore();
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.fileList;
+ loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ commentApiWrapper.loadComments().then(() => {
+ sandbox.stub(element.changeComments, 'getPaths').returns({});
+ sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
+ .returns({meta: {}, left: [], right: []});
+ done();
});
+ element._loading = false;
+ element.diffPrefs = {};
+ element.numFilesShown = 200;
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ saveStub = sandbox.stub(element, '_saveReviewedState',
+ () => Promise.resolve());
+ });
- test('correct number of files are shown', () => {
- element.fileListIncrement = 300;
- element._filesByPath = _.range(500)
- .reduce((_filesByPath, i) => {
- _filesByPath['/file' + i] = {lines_inserted: 9};
- return _filesByPath;
- }, {});
+ teardown(() => {
+ sandbox.restore();
+ });
- flushAsynchronousOperations();
- assert.equal(
- Polymer.dom(element.root).querySelectorAll('.file-row').length,
- element.numFilesShown);
- const controlRow = element.shadowRoot
- .querySelector('.controlRow');
- assert.isFalse(controlRow.classList.contains('invisible'));
- assert.equal(element.$.incrementButton.textContent.trim(),
- 'Show 300 more');
- assert.equal(element.$.showAllButton.textContent.trim(),
- 'Show all 500 files');
+ test('correct number of files are shown', () => {
+ element.fileListIncrement = 300;
+ element._filesByPath = _.range(500)
+ .reduce((_filesByPath, i) => {
+ _filesByPath['/file' + i] = {lines_inserted: 9};
+ return _filesByPath;
+ }, {});
- MockInteractions.tap(element.$.showAllButton);
- flushAsynchronousOperations();
+ flushAsynchronousOperations();
+ assert.equal(
+ dom(element.root).querySelectorAll('.file-row').length,
+ element.numFilesShown);
+ const controlRow = element.shadowRoot
+ .querySelector('.controlRow');
+ assert.isFalse(controlRow.classList.contains('invisible'));
+ assert.equal(element.$.incrementButton.textContent.trim(),
+ 'Show 300 more');
+ assert.equal(element.$.showAllButton.textContent.trim(),
+ 'Show all 500 files');
- assert.equal(element.numFilesShown, 500);
- assert.equal(element._shownFiles.length, 500);
- assert.isTrue(controlRow.classList.contains('invisible'));
+ MockInteractions.tap(element.$.showAllButton);
+ flushAsynchronousOperations();
+
+ assert.equal(element.numFilesShown, 500);
+ assert.equal(element._shownFiles.length, 500);
+ assert.isTrue(controlRow.classList.contains('invisible'));
+ });
+
+ test('rendering each row calls the _reportRenderedRow method', () => {
+ const renderedStub = sandbox.stub(element, '_reportRenderedRow');
+ element._filesByPath = _.range(10)
+ .reduce((_filesByPath, i) => {
+ _filesByPath['/file' + i] = {lines_inserted: 9};
+ return _filesByPath;
+ }, {});
+ flushAsynchronousOperations();
+ assert.equal(
+ dom(element.root).querySelectorAll('.file-row').length, 10);
+ assert.equal(renderedStub.callCount, 10);
+ });
+
+ test('calculate totals for patch number', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {
+ lines_inserted: 9,
+ },
+ '/MERGE_LIST': {
+ lines_inserted: 9,
+ },
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ };
+
+ assert.deepEqual(element._patchChange, {
+ inserted: 2,
+ deleted: 2,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
- test('rendering each row calls the _reportRenderedRow method', () => {
- const renderedStub = sandbox.stub(element, '_reportRenderedRow');
- element._filesByPath = _.range(10)
- .reduce((_filesByPath, i) => {
- _filesByPath['/file' + i] = {lines_inserted: 9};
- return _filesByPath;
- }, {});
- flushAsynchronousOperations();
- assert.equal(
- Polymer.dom(element.root).querySelectorAll('.file-row').length, 10);
- assert.equal(renderedStub.callCount, 10);
+ // Test with a commit message that isn't the first file.
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ '/COMMIT_MSG': {
+ lines_inserted: 9,
+ },
+ '/MERGE_LIST': {
+ lines_inserted: 9,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ };
+
+ assert.deepEqual(element._patchChange, {
+ inserted: 2,
+ deleted: 2,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
- test('calculate totals for patch number', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {
- lines_inserted: 9,
- },
- '/MERGE_LIST': {
- lines_inserted: 9,
- },
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- };
+ // Test with no commit message.
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ };
- assert.deepEqual(element._patchChange, {
- inserted: 2,
- deleted: 2,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
-
- // Test with a commit message that isn't the first file.
- element._filesByPath = {
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- '/COMMIT_MSG': {
- lines_inserted: 9,
- },
- '/MERGE_LIST': {
- lines_inserted: 9,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- };
-
- assert.deepEqual(element._patchChange, {
- inserted: 2,
- deleted: 2,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
-
- // Test with no commit message.
- element._filesByPath = {
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- },
- };
-
- assert.deepEqual(element._patchChange, {
- inserted: 2,
- deleted: 2,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
-
- // Test with files missing either lines_inserted or lines_deleted.
- element._filesByPath = {
- 'file_added_in_rev2.txt': {lines_inserted: 1},
- 'myfile.txt': {lines_deleted: 1},
- };
- assert.deepEqual(element._patchChange, {
- inserted: 1,
- deleted: 1,
- size_delta_inserted: 0,
- size_delta_deleted: 0,
- total_size: 0,
- });
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ assert.deepEqual(element._patchChange, {
+ inserted: 2,
+ deleted: 2,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
- test('binary only files', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {lines_inserted: 9},
- 'file_binary_1': {binary: true, size_delta: 10, size: 100},
- 'file_binary_2': {binary: true, size_delta: -5, size: 120},
- };
- assert.deepEqual(element._patchChange, {
- inserted: 0,
- deleted: 0,
- size_delta_inserted: 10,
- size_delta_deleted: -5,
- total_size: 220,
- });
- assert.isFalse(element._hideBinaryChangeTotals);
- assert.isTrue(element._hideChangeTotals);
+ // Test with files missing either lines_inserted or lines_deleted.
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {lines_inserted: 1},
+ 'myfile.txt': {lines_deleted: 1},
+ };
+ assert.deepEqual(element._patchChange, {
+ inserted: 1,
+ deleted: 1,
+ size_delta_inserted: 0,
+ size_delta_deleted: 0,
+ total_size: 0,
});
+ assert.isTrue(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
+ });
- test('binary and regular files', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {lines_inserted: 9},
- 'file_binary_1': {binary: true, size_delta: 10, size: 100},
- 'file_binary_2': {binary: true, size_delta: -5, size: 120},
- 'myfile.txt': {lines_deleted: 5, size_delta: -10, size: 100},
- 'myfile2.txt': {lines_inserted: 10},
- };
- assert.deepEqual(element._patchChange, {
- inserted: 10,
- deleted: 5,
- size_delta_inserted: 10,
- size_delta_deleted: -5,
- total_size: 220,
- });
- assert.isFalse(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ test('binary only files', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_binary_1': {binary: true, size_delta: 10, size: 100},
+ 'file_binary_2': {binary: true, size_delta: -5, size: 120},
+ };
+ assert.deepEqual(element._patchChange, {
+ inserted: 0,
+ deleted: 0,
+ size_delta_inserted: 10,
+ size_delta_deleted: -5,
+ total_size: 220,
});
+ assert.isFalse(element._hideBinaryChangeTotals);
+ assert.isTrue(element._hideChangeTotals);
+ });
- test('_formatBytes function', () => {
- const table = {
- '64': '+64 B',
- '1023': '+1023 B',
- '1024': '+1 KiB',
- '4096': '+4 KiB',
- '1073741824': '+1 GiB',
- '-64': '-64 B',
- '-1023': '-1023 B',
- '-1024': '-1 KiB',
- '-4096': '-4 KiB',
- '-1073741824': '-1 GiB',
- '0': '+/-0 B',
- };
+ test('binary and regular files', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_binary_1': {binary: true, size_delta: 10, size: 100},
+ 'file_binary_2': {binary: true, size_delta: -5, size: 120},
+ 'myfile.txt': {lines_deleted: 5, size_delta: -10, size: 100},
+ 'myfile2.txt': {lines_inserted: 10},
+ };
+ assert.deepEqual(element._patchChange, {
+ inserted: 10,
+ deleted: 5,
+ size_delta_inserted: 10,
+ size_delta_deleted: -5,
+ total_size: 220,
+ });
+ assert.isFalse(element._hideBinaryChangeTotals);
+ assert.isFalse(element._hideChangeTotals);
+ });
- for (const bytes in table) {
- if (table.hasOwnProperty(bytes)) {
- assert.equal(element._formatBytes(bytes), table[bytes]);
- }
+ test('_formatBytes function', () => {
+ const table = {
+ '64': '+64 B',
+ '1023': '+1023 B',
+ '1024': '+1 KiB',
+ '4096': '+4 KiB',
+ '1073741824': '+1 GiB',
+ '-64': '-64 B',
+ '-1023': '-1023 B',
+ '-1024': '-1 KiB',
+ '-4096': '-4 KiB',
+ '-1073741824': '-1 GiB',
+ '0': '+/-0 B',
+ };
+
+ for (const bytes in table) {
+ if (table.hasOwnProperty(bytes)) {
+ assert.equal(element._formatBytes(bytes), table[bytes]);
}
- });
+ }
+ });
- test('_formatPercentage function', () => {
- const table = [
- {size: 100,
- delta: 100,
- display: '',
+ test('_formatPercentage function', () => {
+ const table = [
+ {size: 100,
+ delta: 100,
+ display: '',
+ },
+ {size: 195060,
+ delta: 64,
+ display: '(+0%)',
+ },
+ {size: 195060,
+ delta: -64,
+ display: '(-0%)',
+ },
+ {size: 394892,
+ delta: -7128,
+ display: '(-2%)',
+ },
+ {size: 90,
+ delta: -10,
+ display: '(-10%)',
+ },
+ {size: 110,
+ delta: 10,
+ display: '(+10%)',
+ },
+ ];
+
+ for (const item of table) {
+ assert.equal(element._formatPercentage(
+ item.size, item.delta), item.display);
+ }
+ });
+
+ test('comment filtering', () => {
+ element.changeComments._comments = {
+ '/COMMIT_MSG': [
+ {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'},
+ {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'},
+ {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'},
+ ],
+ 'myfile.txt': [
+ {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'},
+ {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'},
+ {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'},
+ ],
+ 'unresolved.file': [
+ {
+ patch_set: 2,
+ message: 'wat!?',
+ updated: '2017-02-09 16:40:49',
+ id: '1',
+ unresolved: true,
},
- {size: 195060,
- delta: 64,
- display: '(+0%)',
+ {
+ patch_set: 2,
+ message: 'hi',
+ updated: '2017-02-10 16:40:49',
+ id: '2',
+ in_reply_to: '1',
+ unresolved: false,
},
- {size: 195060,
- delta: -64,
- display: '(-0%)',
+ {
+ patch_set: 2,
+ message: 'good news!',
+ updated: '2017-02-08 16:40:49',
+ id: '3',
+ unresolved: true,
},
- {size: 394892,
- delta: -7128,
- display: '(-2%)',
+ ],
+ };
+ element.changeComments._drafts = {
+ '/COMMIT_MSG': [
+ {
+ patch_set: 1,
+ message: 'hi',
+ updated: '2017-02-15 16:40:49',
+ id: '5',
+ unresolved: true,
},
- {size: 90,
- delta: -10,
- display: '(-10%)',
+ {
+ patch_set: 1,
+ message: 'fyi',
+ updated: '2017-02-15 16:40:49',
+ id: '6',
+ unresolved: false,
},
- {size: 110,
- delta: 10,
- display: '(+10%)',
+ ],
+ 'unresolved.file': [
+ {
+ patch_set: 1,
+ message: 'hi',
+ updated: '2017-02-11 16:40:49',
+ id: '4',
+ unresolved: false,
},
- ];
+ ],
+ };
- for (const item of table) {
- assert.equal(element._formatPercentage(
- item.size, item.delta), item.display);
- }
- });
+ const parentTo1 = {
+ basePatchNum: 'PARENT',
+ patchNum: '1',
+ };
- test('comment filtering', () => {
- element.changeComments._comments = {
- '/COMMIT_MSG': [
- {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'},
- {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'},
- {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'},
- ],
- 'myfile.txt': [
- {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'},
- {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'},
- {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'},
- ],
- 'unresolved.file': [
- {
- patch_set: 2,
- message: 'wat!?',
- updated: '2017-02-09 16:40:49',
- id: '1',
- unresolved: true,
- },
- {
- patch_set: 2,
- message: 'hi',
- updated: '2017-02-10 16:40:49',
- id: '2',
- in_reply_to: '1',
- unresolved: false,
- },
- {
- patch_set: 2,
- message: 'good news!',
- updated: '2017-02-08 16:40:49',
- id: '3',
- unresolved: true,
- },
- ],
- };
- element.changeComments._drafts = {
- '/COMMIT_MSG': [
- {
- patch_set: 1,
- message: 'hi',
- updated: '2017-02-15 16:40:49',
- id: '5',
- unresolved: true,
- },
- {
- patch_set: 1,
- message: 'fyi',
- updated: '2017-02-15 16:40:49',
- id: '6',
- unresolved: false,
- },
- ],
- 'unresolved.file': [
- {
- patch_set: 1,
- message: 'hi',
- updated: '2017-02-11 16:40:49',
- id: '4',
- unresolved: false,
- },
- ],
- };
+ const parentTo2 = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
- const parentTo1 = {
- basePatchNum: 'PARENT',
- patchNum: '1',
- };
+ const _1To2 = {
+ basePatchNum: '1',
+ patchNum: '2',
+ };
- const parentTo2 = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo1,
+ '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, parentTo1
+ , '/COMMIT_MSG'), '2c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2
+ , '/COMMIT_MSG'), '3c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ 'unresolved.file'), '1 draft');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ 'unresolved.file'), '1 draft');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo1,
+ 'unresolved.file'), '1d');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'unresolved.file'), '1d');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo1,
+ 'myfile.txt', 'comment'), '1 comment');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'myfile.txt', 'comment'), '3 comments');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo1,
+ 'myfile.txt'
+ ), '1c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '3c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo1,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo1,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo1,
+ 'file_added_in_rev2.txt'
+ ), '');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo1,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ '/COMMIT_MSG', 'comment'), '1 comment');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo2,
+ '/COMMIT_MSG'
+ ), '1c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ '/COMMIT_MSG'), '3c');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, parentTo1,
+ '/COMMIT_MSG'), '2 drafts');
+ assert.equal(
+ element._computeDraftsString(element.changeComments, _1To2,
+ '/COMMIT_MSG'), '2 drafts');
+ assert.equal(
+ element._computeDraftsStringMobile(
+ element.changeComments,
+ parentTo1,
+ '/COMMIT_MSG'
+ ), '2d');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ '/COMMIT_MSG'), '2d');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ 'myfile.txt', 'comment'), '2 comments');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'myfile.txt', 'comment'), '3 comments');
+ assert.equal(
+ element._computeCommentsStringMobile(
+ element.changeComments,
+ parentTo2,
+ 'myfile.txt'
+ ), '2c');
+ assert.equal(
+ element._computeCommentsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '3c');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, parentTo2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeDraftsStringMobile(element.changeComments, _1To2,
+ 'myfile.txt'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'file_added_in_rev2.txt', 'comment'), '');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, parentTo2,
+ 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
+ assert.equal(
+ element._computeCommentsString(element.changeComments, _1To2,
+ 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
+ });
- const _1To2 = {
- basePatchNum: '1',
- patchNum: '2',
- };
+ test('_reviewedTitle', () => {
+ assert.equal(
+ element._reviewedTitle(true), 'Mark as not reviewed (shortcut: r)');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo1,
- '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, parentTo1
- , '/COMMIT_MSG'), '2c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2
- , '/COMMIT_MSG'), '3c');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- 'unresolved.file'), '1 draft');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- 'unresolved.file'), '1 draft');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1,
- 'unresolved.file'), '1d');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'unresolved.file'), '1d');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo1,
- 'myfile.txt', 'comment'), '1 comment');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'myfile.txt', 'comment'), '3 comments');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo1,
- 'myfile.txt'
- ), '1c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '3c');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo1,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo1,
- 'file_added_in_rev2.txt'
- ), '');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'file_added_in_rev2.txt'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- '/COMMIT_MSG', 'comment'), '1 comment');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo2,
- '/COMMIT_MSG'
- ), '1c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- '/COMMIT_MSG'), '3c');
- assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1,
- '/COMMIT_MSG'), '2 drafts');
- assert.equal(
- element._computeDraftsString(element.changeComments, _1To2,
- '/COMMIT_MSG'), '2 drafts');
- assert.equal(
- element._computeDraftsStringMobile(
- element.changeComments,
- parentTo1,
- '/COMMIT_MSG'
- ), '2d');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- '/COMMIT_MSG'), '2d');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- 'myfile.txt', 'comment'), '2 comments');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'myfile.txt', 'comment'), '3 comments');
- assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo2,
- 'myfile.txt'
- ), '2c');
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '3c');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2,
- 'myfile.txt'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'file_added_in_rev2.txt', 'comment'), '');
- assert.equal(
- element._computeCommentsString(element.changeComments, parentTo2,
- 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
- assert.equal(
- element._computeCommentsString(element.changeComments, _1To2,
- 'unresolved.file', 'comment'), '3 comments (1 unresolved)');
- });
+ assert.equal(
+ element._reviewedTitle(false), 'Mark as reviewed (shortcut: r)');
+ });
- test('_reviewedTitle', () => {
- assert.equal(
- element._reviewedTitle(true), 'Mark as not reviewed (shortcut: r)');
-
- assert.equal(
- element._reviewedTitle(false), 'Mark as reviewed (shortcut: r)');
- });
-
- suite('keyboard shortcuts', () => {
- setup(() => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- 'file_added_in_rev2.txt': {},
- 'myfile.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- element.change = {_number: 42};
- element.$.fileCursor.setCursorAtIndex(0);
- });
-
- test('toggle left diff via shortcut', () => {
- const toggleLeftDiffStub = sandbox.stub();
- // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
- // https://github.com/sinonjs/sinon/issues/781
- const diffsStub = sinon.stub(element, 'diffs', {
- get() {
- return [{toggleLeftDiff: toggleLeftDiffStub}];
- },
- });
- MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
- assert.isTrue(toggleLeftDiffStub.calledOnce);
- diffsStub.restore();
- });
-
- test('keyboard shortcuts', () => {
- flushAsynchronousOperations();
-
- const items = Polymer.dom(element.root).querySelectorAll('.file-row');
- element.$.fileCursor.stops = items;
- element.$.fileCursor.setCursorAtIndex(0);
- assert.equal(items.length, 3);
- assert.isTrue(items[0].classList.contains('selected'));
- assert.isFalse(items[1].classList.contains('selected'));
- assert.isFalse(items[2].classList.contains('selected'));
- // j with a modifier should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j');
- assert.equal(element.$.fileCursor.index, 0);
- // down should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'down');
- assert.equal(element.$.fileCursor.index, 0);
-
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.$.fileCursor.index, 1);
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
-
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- assert.equal(element.$.fileCursor.index, 2);
- assert.equal(element.selectedIndex, 2);
-
- // k with a modifier should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
- assert.equal(element.$.fileCursor.index, 2);
-
- // up should not move the cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down');
- assert.equal(element.$.fileCursor.index, 2);
-
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.$.fileCursor.index, 1);
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o');
-
- assert(navStub.lastCall.calledWith(element.change,
- 'file_added_in_rev2.txt', '2'),
- 'Should navigate to /c/42/2/file_added_in_rev2.txt');
-
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.$.fileCursor.index, 0);
- assert.equal(element.selectedIndex, 0);
-
- const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor,
- 'createCommentInPlace');
- MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c');
- assert.isTrue(createCommentInPlaceStub.called);
- });
-
- test('i key shows/hides selected inline diff', () => {
- const paths = Object.keys(element._filesByPath);
- sandbox.stub(element, '_expandedPathsChanged');
- flushAsynchronousOperations();
- const files = Polymer.dom(element.root).querySelectorAll('.file-row');
- element.$.fileCursor.stops = files;
- element.$.fileCursor.setCursorAtIndex(0);
- assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFilePaths.length, 0);
-
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 1);
- assert.equal(element.diffs[0].path, paths[0]);
- assert.equal(element._expandedFilePaths.length, 1);
- assert.equal(element._expandedFilePaths[0], paths[0]);
-
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFilePaths.length, 0);
-
- element.$.fileCursor.setCursorAtIndex(1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 1);
- assert.equal(element.diffs[0].path, paths[1]);
- assert.equal(element._expandedFilePaths.length, 1);
- assert.equal(element._expandedFilePaths[0], paths[1]);
-
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, paths.length);
- assert.equal(element._expandedFilePaths.length, paths.length);
- for (const index in element.diffs) {
- if (!element.diffs.hasOwnProperty(index)) { continue; }
- assert.include(element._expandedFilePaths, element.diffs[index].path);
- }
-
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- flushAsynchronousOperations();
- assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFilePaths.length, 0);
- });
-
- test('r key toggles reviewed flag', () => {
- const reducer = (accum, file) => (file.isReviewed ? ++accum : accum);
- const getNumReviewed = () => element._files.reduce(reducer, 0);
- flushAsynchronousOperations();
-
- // Default state should be unreviewed.
- assert.equal(getNumReviewed(), 0);
-
- // Press the review key to toggle it (set the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- flushAsynchronousOperations();
- assert.equal(getNumReviewed(), 1);
-
- // Press the review key to toggle it (clear the flag).
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.equal(getNumReviewed(), 0);
- });
-
- suite('_handleOpenFile', () => {
- let interact;
-
- setup(() => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut')
- .returns(false);
- sandbox.stub(element, 'modifierPressed').returns(false);
- const openCursorStub = sandbox.stub(element, '_openCursorFile');
- const openSelectedStub = sandbox.stub(element, '_openSelectedFile');
- const expandStub = sandbox.stub(element, '_togglePathExpanded');
-
- interact = function(opt_payload) {
- openCursorStub.reset();
- openSelectedStub.reset();
- expandStub.reset();
-
- const e = new CustomEvent('fake-keyboard-event', opt_payload);
- sinon.stub(e, 'preventDefault');
- element._handleOpenFile(e);
- assert.isTrue(e.preventDefault.called);
- const result = {};
- if (openCursorStub.called) {
- result.opened_cursor = true;
- }
- if (openSelectedStub.called) {
- result.opened_selected = true;
- }
- if (expandStub.called) {
- result.expanded = true;
- }
- return result;
- };
- });
-
- test('open from selected file', () => {
- element._showInlineDiffs = false;
- assert.deepEqual(interact(), {opened_selected: true});
- });
-
- test('open from diff cursor', () => {
- element._showInlineDiffs = true;
- assert.deepEqual(interact(), {opened_cursor: true});
- });
-
- test('expand when user prefers', () => {
- element._showInlineDiffs = false;
- assert.deepEqual(interact(), {opened_selected: true});
- element._userPrefs = {};
- assert.deepEqual(interact(), {opened_selected: true});
- });
- });
-
- test('shift+left/shift+right', () => {
- const moveLeftStub = sandbox.stub(element.$.diffCursor, 'moveLeft');
- const moveRightStub = sandbox.stub(element.$.diffCursor, 'moveRight');
-
- let noDiffsExpanded = true;
- sandbox.stub(element, '_noDiffsExpanded', () => noDiffsExpanded);
-
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
- assert.isFalse(moveLeftStub.called);
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
- assert.isFalse(moveRightStub.called);
-
- noDiffsExpanded = false;
-
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
- assert.isTrue(moveLeftStub.called);
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
- assert.isTrue(moveRightStub.called);
- });
- });
-
- test('computed properties', () => {
- assert.equal(element._computeFileStatus('A'), 'A');
- assert.equal(element._computeFileStatus(undefined), 'M');
- assert.equal(element._computeFileStatus(null), 'M');
-
- assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
- assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
- 'clazz invisible');
- });
-
- test('file review status', () => {
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ suite('keyboard shortcuts', () => {
+ setup(() => {
element._filesByPath = {
'/COMMIT_MSG': {},
'file_added_in_rev2.txt': {},
'myfile.txt': {},
};
- element._loggedIn = true;
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: '2',
};
+ element.change = {_number: 42};
element.$.fileCursor.setCursorAtIndex(0);
+ });
+ test('toggle left diff via shortcut', () => {
+ const toggleLeftDiffStub = sandbox.stub();
+ // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
+ // https://github.com/sinonjs/sinon/issues/781
+ const diffsStub = sinon.stub(element, 'diffs', {
+ get() {
+ return [{toggleLeftDiff: toggleLeftDiffStub}];
+ },
+ });
+ MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
+ assert.isTrue(toggleLeftDiffStub.calledOnce);
+ diffsStub.restore();
+ });
+
+ test('keyboard shortcuts', () => {
flushAsynchronousOperations();
- const fileRows =
- Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
- const checkSelector = 'input.reviewed[type="checkbox"]';
- const commitMsg = fileRows[0].querySelector(checkSelector);
- const fileAdded = fileRows[1].querySelector(checkSelector);
- const myFile = fileRows[2].querySelector(checkSelector);
- assert.isTrue(commitMsg.checked);
- assert.isFalse(fileAdded.checked);
- assert.isTrue(myFile.checked);
+ const items = dom(element.root).querySelectorAll('.file-row');
+ element.$.fileCursor.stops = items;
+ element.$.fileCursor.setCursorAtIndex(0);
+ assert.equal(items.length, 3);
+ assert.isTrue(items[0].classList.contains('selected'));
+ assert.isFalse(items[1].classList.contains('selected'));
+ assert.isFalse(items[2].classList.contains('selected'));
+ // j with a modifier should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j');
+ assert.equal(element.$.fileCursor.index, 0);
+ // down should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'down');
+ assert.equal(element.$.fileCursor.index, 0);
- const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
- const markReviewLabel = commitMsg.nextElementSibling;
- assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
- assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert.equal(element.$.fileCursor.index, 1);
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- const clickSpy = sandbox.spy(element, '_handleFileListClick');
- MockInteractions.tap(markReviewLabel);
- assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
- assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
- assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
- assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ assert.equal(element.$.fileCursor.index, 2);
+ assert.equal(element.selectedIndex, 2);
- MockInteractions.tap(markReviewLabel);
- assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
- assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
- assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
- assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ // k with a modifier should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
+ assert.equal(element.$.fileCursor.index, 2);
+
+ // up should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down');
+ assert.equal(element.$.fileCursor.index, 2);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.$.fileCursor.index, 1);
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o');
+
+ assert(navStub.lastCall.calledWith(element.change,
+ 'file_added_in_rev2.txt', '2'),
+ 'Should navigate to /c/42/2/file_added_in_rev2.txt');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+ assert.equal(element.$.fileCursor.index, 0);
+ assert.equal(element.selectedIndex, 0);
+
+ const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor,
+ 'createCommentInPlace');
+ MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c');
+ assert.isTrue(createCommentInPlaceStub.called);
});
- test('_computeFileStatusLabel', () => {
- assert.equal(element._computeFileStatusLabel('A'), 'Added');
- assert.equal(element._computeFileStatusLabel('M'), 'Modified');
+ test('i key shows/hides selected inline diff', () => {
+ const paths = Object.keys(element._filesByPath);
+ sandbox.stub(element, '_expandedPathsChanged');
+ flushAsynchronousOperations();
+ const files = dom(element.root).querySelectorAll('.file-row');
+ element.$.fileCursor.stops = files;
+ element.$.fileCursor.setCursorAtIndex(0);
+ assert.equal(element.diffs.length, 0);
+ assert.equal(element._expandedFilePaths.length, 0);
+
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 1);
+ assert.equal(element.diffs[0].path, paths[0]);
+ assert.equal(element._expandedFilePaths.length, 1);
+ assert.equal(element._expandedFilePaths[0], paths[0]);
+
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 0);
+ assert.equal(element._expandedFilePaths.length, 0);
+
+ element.$.fileCursor.setCursorAtIndex(1);
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 1);
+ assert.equal(element.diffs[0].path, paths[1]);
+ assert.equal(element._expandedFilePaths.length, 1);
+ assert.equal(element._expandedFilePaths[0], paths[1]);
+
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, paths.length);
+ assert.equal(element._expandedFilePaths.length, paths.length);
+ for (const index in element.diffs) {
+ if (!element.diffs.hasOwnProperty(index)) { continue; }
+ assert.include(element._expandedFilePaths, element.diffs[index].path);
+ }
+
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ flushAsynchronousOperations();
+ assert.equal(element.diffs.length, 0);
+ assert.equal(element._expandedFilePaths.length, 0);
});
- test('_handleFileListClick', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- 'f1.txt': {},
- 'f2.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
+ test('r key toggles reviewed flag', () => {
+ const reducer = (accum, file) => (file.isReviewed ? ++accum : accum);
+ const getNumReviewed = () => element._files.reduce(reducer, 0);
+ flushAsynchronousOperations();
- const clickSpy = sandbox.spy(element, '_handleFileListClick');
- const reviewStub = sandbox.stub(element, '_reviewFile');
- const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+ // Default state should be unreviewed.
+ assert.equal(getNumReviewed(), 0);
- const row = Polymer.dom(element.root)
- .querySelector('.row[data-path="f1.txt"]');
+ // Press the review key to toggle it (set the flag).
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ flushAsynchronousOperations();
+ assert.equal(getNumReviewed(), 1);
- // Click on the expand button, resulting in _togglePathExpanded being
- // called and not resulting in a call to _reviewFile.
- row.querySelector('div.show-hide').click();
- assert.isTrue(clickSpy.calledOnce);
- assert.isTrue(toggleExpandSpy.calledOnce);
+ // Press the review key to toggle it (clear the flag).
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.equal(getNumReviewed(), 0);
+ });
+
+ suite('_handleOpenFile', () => {
+ let interact;
+
+ setup(() => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut')
+ .returns(false);
+ sandbox.stub(element, 'modifierPressed').returns(false);
+ const openCursorStub = sandbox.stub(element, '_openCursorFile');
+ const openSelectedStub = sandbox.stub(element, '_openSelectedFile');
+ const expandStub = sandbox.stub(element, '_togglePathExpanded');
+
+ interact = function(opt_payload) {
+ openCursorStub.reset();
+ openSelectedStub.reset();
+ expandStub.reset();
+
+ const e = new CustomEvent('fake-keyboard-event', opt_payload);
+ sinon.stub(e, 'preventDefault');
+ element._handleOpenFile(e);
+ assert.isTrue(e.preventDefault.called);
+ const result = {};
+ if (openCursorStub.called) {
+ result.opened_cursor = true;
+ }
+ if (openSelectedStub.called) {
+ result.opened_selected = true;
+ }
+ if (expandStub.called) {
+ result.expanded = true;
+ }
+ return result;
+ };
+ });
+
+ test('open from selected file', () => {
+ element._showInlineDiffs = false;
+ assert.deepEqual(interact(), {opened_selected: true});
+ });
+
+ test('open from diff cursor', () => {
+ element._showInlineDiffs = true;
+ assert.deepEqual(interact(), {opened_cursor: true});
+ });
+
+ test('expand when user prefers', () => {
+ element._showInlineDiffs = false;
+ assert.deepEqual(interact(), {opened_selected: true});
+ element._userPrefs = {};
+ assert.deepEqual(interact(), {opened_selected: true});
+ });
+ });
+
+ test('shift+left/shift+right', () => {
+ const moveLeftStub = sandbox.stub(element.$.diffCursor, 'moveLeft');
+ const moveRightStub = sandbox.stub(element.$.diffCursor, 'moveRight');
+
+ let noDiffsExpanded = true;
+ sandbox.stub(element, '_noDiffsExpanded', () => noDiffsExpanded);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
+ assert.isFalse(moveLeftStub.called);
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
+ assert.isFalse(moveRightStub.called);
+
+ noDiffsExpanded = false;
+
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left');
+ assert.isTrue(moveLeftStub.called);
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right');
+ assert.isTrue(moveRightStub.called);
+ });
+ });
+
+ test('computed properties', () => {
+ assert.equal(element._computeFileStatus('A'), 'A');
+ assert.equal(element._computeFileStatus(undefined), 'M');
+ assert.equal(element._computeFileStatus(null), 'M');
+
+ assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
+ assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
+ 'clazz invisible');
+ });
+
+ test('file review status', () => {
+ element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'file_added_in_rev2.txt': {},
+ 'myfile.txt': {},
+ };
+ element._loggedIn = true;
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.$.fileCursor.setCursorAtIndex(0);
+
+ flushAsynchronousOperations();
+ const fileRows =
+ dom(element.root).querySelectorAll('.row:not(.header-row)');
+ const checkSelector = 'input.reviewed[type="checkbox"]';
+ const commitMsg = fileRows[0].querySelector(checkSelector);
+ const fileAdded = fileRows[1].querySelector(checkSelector);
+ const myFile = fileRows[2].querySelector(checkSelector);
+
+ assert.isTrue(commitMsg.checked);
+ assert.isFalse(fileAdded.checked);
+ assert.isTrue(myFile.checked);
+
+ const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
+ const markReviewLabel = commitMsg.nextElementSibling;
+ assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
+ assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
+
+ const clickSpy = sandbox.spy(element, '_handleFileListClick');
+ MockInteractions.tap(markReviewLabel);
+ assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
+ assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
+ assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
+ assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+
+ MockInteractions.tap(markReviewLabel);
+ assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
+ assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
+ assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
+ assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ });
+
+ test('_computeFileStatusLabel', () => {
+ assert.equal(element._computeFileStatusLabel('A'), 'Added');
+ assert.equal(element._computeFileStatusLabel('M'), 'Modified');
+ });
+
+ test('_handleFileListClick', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'f1.txt': {},
+ 'f2.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+
+ const clickSpy = sandbox.spy(element, '_handleFileListClick');
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+
+ const row = dom(element.root)
+ .querySelector('.row[data-path="f1.txt"]');
+
+ // Click on the expand button, resulting in _togglePathExpanded being
+ // called and not resulting in a call to _reviewFile.
+ row.querySelector('div.show-hide').click();
+ assert.isTrue(clickSpy.calledOnce);
+ assert.isTrue(toggleExpandSpy.calledOnce);
+ assert.isFalse(reviewStub.called);
+
+ // Click inside the diff. This should result in no additional calls to
+ // _togglePathExpanded or _reviewFile.
+ dom(element.root).querySelector('gr-diff-host')
+ .click();
+ assert.isTrue(clickSpy.calledTwice);
+ assert.isTrue(toggleExpandSpy.calledOnce);
+ assert.isFalse(reviewStub.called);
+
+ // Click the reviewed checkbox, resulting in a call to _reviewFile, but
+ // no additional call to _togglePathExpanded.
+ row.querySelector('.markReviewed').click();
+ assert.isTrue(clickSpy.calledThrice);
+ assert.isTrue(toggleExpandSpy.calledOnce);
+ assert.isTrue(reviewStub.calledOnce);
+ });
+
+ test('_handleFileListClick editMode', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'f1.txt': {},
+ 'f2.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.editMode = true;
+ flushAsynchronousOperations();
+ const clickSpy = sandbox.spy(element, '_handleFileListClick');
+ const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+
+ // Tap the edit controls. Should be ignored by _handleFileListClick.
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.editFileControls'));
+ assert.isTrue(clickSpy.calledOnce);
+ assert.isFalse(toggleExpandSpy.called);
+ });
+
+ test('patch set from revisions', () => {
+ const expected = [
+ {num: 4, desc: 'test', sha: 'rev4'},
+ {num: 3, desc: 'test', sha: 'rev3'},
+ {num: 2, desc: 'test', sha: 'rev2'},
+ {num: 1, desc: 'test', sha: 'rev1'},
+ ];
+ const patchNums = element.computeAllPatchSets({
+ revisions: {
+ rev3: {_number: 3, description: 'test', date: 3},
+ rev1: {_number: 1, description: 'test', date: 1},
+ rev4: {_number: 4, description: 'test', date: 4},
+ rev2: {_number: 2, description: 'test', date: 2},
+ },
+ });
+ assert.equal(patchNums.length, expected.length);
+ for (let i = 0; i < expected.length; i++) {
+ assert.deepEqual(patchNums[i], expected[i]);
+ }
+ });
+
+ test('checkbox shows/hides diff inline', () => {
+ element._filesByPath = {
+ 'myfile.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.$.fileCursor.setCursorAtIndex(0);
+ sandbox.stub(element, '_expandedPathsChanged');
+ flushAsynchronousOperations();
+ const fileRows =
+ dom(element.root).querySelectorAll('.row:not(.header-row)');
+ // Because the label surrounds the input, the tap event is triggered
+ // there first.
+ const showHideLabel = fileRows[0].querySelector('label.show-hide');
+ const showHideCheck = fileRows[0].querySelector(
+ 'input.show-hide[type="checkbox"]');
+ assert.isNotOk(showHideCheck.checked);
+ MockInteractions.tap(showHideLabel);
+ assert.isOk(showHideCheck.checked);
+ assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
+ });
+
+ test('diff mode correctly toggles the diffs', () => {
+ element._filesByPath = {
+ 'myfile.txt': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ sandbox.spy(element, '_updateDiffPreferences');
+ element.$.fileCursor.setCursorAtIndex(0);
+ flushAsynchronousOperations();
+
+ // Tap on a file to generate the diff.
+ const row = dom(element.root)
+ .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
+
+ MockInteractions.tap(row);
+ flushAsynchronousOperations();
+ const diffDisplay = element.diffs[0];
+ element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
+ element.set('diffViewMode', 'UNIFIED_DIFF');
+ assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
+ assert.isTrue(element._updateDiffPreferences.called);
+ });
+
+ test('expanded attribute not set on path when not expanded', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ };
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.expanded'));
+ });
+
+ test('tapping row ignores links', () => {
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ };
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ sandbox.stub(element, '_expandedPathsChanged');
+ flushAsynchronousOperations();
+ const commitMsgFile = dom(element.root)
+ .querySelectorAll('.row:not(.header-row) a.pathLink')[0];
+
+ // Remove href attribute so the app doesn't route to a diff view
+ commitMsgFile.removeAttribute('href');
+ const togglePathSpy = sandbox.spy(element, '_togglePathExpanded');
+
+ MockInteractions.tap(commitMsgFile);
+ flushAsynchronousOperations();
+ assert(togglePathSpy.notCalled, 'file is opened as diff view');
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.expanded'));
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.show-hide')).display,
+ 'none');
+ });
+
+ test('_togglePathExpanded', () => {
+ const path = 'path/to/my/file.txt';
+ element._filesByPath = {[path]: {}};
+ const renderSpy = sandbox.spy(element, '_renderInOrder');
+ const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
+
+ assert.equal(element.shadowRoot
+ .querySelector('iron-icon').icon, 'gr-icons:expand-more');
+ assert.equal(element._expandedFilePaths.length, 0);
+ element._togglePathExpanded(path);
+ flushAsynchronousOperations();
+ assert.equal(collapseStub.lastCall.args[0].length, 0);
+ assert.equal(element.shadowRoot
+ .querySelector('iron-icon').icon, 'gr-icons:expand-less');
+
+ assert.equal(renderSpy.callCount, 1);
+ assert.include(element._expandedFilePaths, path);
+ element._togglePathExpanded(path);
+ flushAsynchronousOperations();
+
+ assert.equal(element.shadowRoot
+ .querySelector('iron-icon').icon, 'gr-icons:expand-more');
+ assert.equal(renderSpy.callCount, 1);
+ assert.notInclude(element._expandedFilePaths, path);
+ assert.equal(collapseStub.lastCall.args[0].length, 1);
+ });
+
+ test('expandAllDiffs and collapseAllDiffs', () => {
+ const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
+ const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
+ 'handleDiffUpdate');
+
+ const path = 'path/to/my/file.txt';
+ element._filesByPath = {[path]: {}};
+ element.expandAllDiffs();
+ flushAsynchronousOperations();
+ assert.isTrue(element._showInlineDiffs);
+ assert.isTrue(cursorUpdateStub.calledOnce);
+ assert.equal(collapseStub.lastCall.args[0].length, 0);
+
+ element.collapseAllDiffs();
+ flushAsynchronousOperations();
+ assert.equal(element._expandedFilePaths.length, 0);
+ assert.isFalse(element._showInlineDiffs);
+ assert.isTrue(cursorUpdateStub.calledTwice);
+ assert.equal(collapseStub.lastCall.args[0].length, 1);
+ });
+
+ test('_expandedPathsChanged', done => {
+ sandbox.stub(element, '_reviewFile');
+ const path = 'path/to/my/file.txt';
+ const diffs = [{
+ path,
+ style: {},
+ reload() {
+ done();
+ },
+ cancel() {},
+ getCursorStops() { return []; },
+ addEventListener(eventName, callback) {
+ callback(new Event(eventName));
+ },
+ }];
+ sinon.stub(element, 'diffs', {
+ get() { return diffs; },
+ });
+ element.push('_expandedFilePaths', path);
+ });
+
+ test('_clearCollapsedDiffs', () => {
+ const diff = {
+ cancel: sinon.stub(),
+ clearDiffContent: sinon.stub(),
+ };
+ element._clearCollapsedDiffs([diff]);
+ assert.isTrue(diff.cancel.calledOnce);
+ assert.isTrue(diff.clearDiffContent.calledOnce);
+ });
+
+ test('filesExpanded value updates to correct enum', () => {
+ element._filesByPath = {
+ 'foo.bar': {},
+ 'baz.bar': {},
+ };
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.NONE);
+ element.push('_expandedFilePaths', 'baz.bar');
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.SOME);
+ element.push('_expandedFilePaths', 'foo.bar');
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.ALL);
+ element.collapseAllDiffs();
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.NONE);
+ element.expandAllDiffs();
+ flushAsynchronousOperations();
+ assert.equal(element.filesExpanded,
+ GrFileListConstants.FilesExpandedState.ALL);
+ });
+
+ test('_renderInOrder', done => {
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ let callCount = 0;
+ const diffs = [{
+ path: 'p0',
+ style: {},
+ reload() {
+ assert.equal(callCount++, 2);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p1',
+ style: {},
+ reload() {
+ assert.equal(callCount++, 1);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p2',
+ style: {},
+ reload() {
+ assert.equal(callCount++, 0);
+ return Promise.resolve();
+ },
+ }];
+ element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
+ .then(() => {
+ assert.isFalse(reviewStub.called);
+ assert.isTrue(loadCommentSpy.called);
+ done();
+ });
+ });
+
+ test('_renderInOrder logged in', done => {
+ element._loggedIn = true;
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ let callCount = 0;
+ const diffs = [{
+ path: 'p0',
+ style: {},
+ reload() {
+ assert.equal(reviewStub.callCount, 2);
+ assert.equal(callCount++, 2);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p1',
+ style: {},
+ reload() {
+ assert.equal(reviewStub.callCount, 1);
+ assert.equal(callCount++, 1);
+ return Promise.resolve();
+ },
+ }, {
+ path: 'p2',
+ style: {},
+ reload() {
+ assert.equal(reviewStub.callCount, 0);
+ assert.equal(callCount++, 0);
+ return Promise.resolve();
+ },
+ }];
+ element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
+ .then(() => {
+ assert.equal(reviewStub.callCount, 3);
+ done();
+ });
+ });
+
+ test('_renderInOrder respects diffPrefs.manual_review', () => {
+ element._loggedIn = true;
+ element.diffPrefs = {manual_review: true};
+ const reviewStub = sandbox.stub(element, '_reviewFile');
+ const diffs = [{
+ path: 'p',
+ style: {},
+ reload() { return Promise.resolve(); },
+ }];
+
+ return element._renderInOrder(['p'], diffs, 1).then(() => {
assert.isFalse(reviewStub.called);
+ delete element.diffPrefs.manual_review;
+ return element._renderInOrder(['p'], diffs, 1).then(() => {
+ assert.isTrue(reviewStub.called);
+ assert.isTrue(reviewStub.calledWithExactly('p', true));
+ });
+ });
+ });
- // Click inside the diff. This should result in no additional calls to
- // _togglePathExpanded or _reviewFile.
- Polymer.dom(element.root).querySelector('gr-diff-host')
- .click();
- assert.isTrue(clickSpy.calledTwice);
- assert.isTrue(toggleExpandSpy.calledOnce);
- assert.isFalse(reviewStub.called);
+ test('_loadingChanged fired from reload in debouncer', done => {
+ sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
+ element.changeNum = 123;
+ element.patchRange = {patchNum: 12};
+ element._filesByPath = {'foo.bar': {}};
- // Click the reviewed checkbox, resulting in a call to _reviewFile, but
- // no additional call to _togglePathExpanded.
- row.querySelector('.markReviewed').click();
- assert.isTrue(clickSpy.calledThrice);
- assert.isTrue(toggleExpandSpy.calledOnce);
- assert.isTrue(reviewStub.calledOnce);
+ element.reload().then(() => {
+ assert.isFalse(element._loading);
+ element.flushDebouncer('loading-change');
+ assert.isFalse(element.classList.contains('loading'));
+ done();
+ });
+ assert.isTrue(element._loading);
+ assert.isFalse(element.classList.contains('loading'));
+ element.flushDebouncer('loading-change');
+ assert.isTrue(element.classList.contains('loading'));
+ });
+
+ test('_loadingChanged does not set class when there are no files', () => {
+ sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
+ element.changeNum = 123;
+ element.patchRange = {patchNum: 12};
+ element.reload();
+ assert.isTrue(element._loading);
+ element.flushDebouncer('loading-change');
+ assert.isFalse(element.classList.contains('loading'));
+ });
+ });
+
+ suite('diff url file list', () => {
+ test('diff url', () => {
+ const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
+ .returns('/c/gerrit/+/1/1/index.php');
+ const change = {
+ _number: 1,
+ project: 'gerrit',
+ };
+ const path = 'index.php';
+ const patchRange = {
+ patchNum: 1,
+ };
+ assert.equal(
+ element._computeDiffURL(change, patchRange, path, false),
+ '/c/gerrit/+/1/1/index.php');
+ diffStub.restore();
+ });
+
+ test('diff url commit msg', () => {
+ const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
+ .returns('/c/gerrit/+/1/1//COMMIT_MSG');
+ const change = {
+ _number: 1,
+ project: 'gerrit',
+ };
+ const path = '/COMMIT_MSG';
+ const patchRange = {
+ patchNum: 1,
+ };
+ assert.equal(
+ element._computeDiffURL(change, patchRange, path, false),
+ '/c/gerrit/+/1/1//COMMIT_MSG');
+ diffStub.restore();
+ });
+ });
+
+ suite('size bars', () => {
+ test('_computeSizeBarLayout', () => {
+ assert.isUndefined(element._computeSizeBarLayout(null));
+ assert.isUndefined(element._computeSizeBarLayout({}));
+ assert.deepEqual(element._computeSizeBarLayout({base: []}), {
+ maxInserted: 0,
+ maxDeleted: 0,
+ maxAdditionWidth: 0,
+ maxDeletionWidth: 0,
+ deletionOffset: 0,
});
- test('_handleFileListClick editMode', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- 'f1.txt': {},
- 'f2.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
+ const files = [
+ {__path: '/COMMIT_MSG', lines_inserted: 10000},
+ {__path: 'foo', lines_inserted: 4, lines_deleted: 10},
+ {__path: 'bar', lines_inserted: 5, lines_deleted: 8},
+ ];
+ const layout = element._computeSizeBarLayout({base: files});
+ assert.equal(layout.maxInserted, 5);
+ assert.equal(layout.maxDeleted, 10);
+ });
+
+ test('_computeBarAdditionWidth', () => {
+ const file = {
+ __path: 'foo/bar.baz',
+ lines_inserted: 5,
+ lines_deleted: 0,
+ };
+ const stats = {
+ maxInserted: 10,
+ maxDeleted: 0,
+ maxAdditionWidth: 60,
+ maxDeletionWidth: 0,
+ deletionOffset: 60,
+ };
+
+ // Uses half the space when file is half the largest addition and there
+ // are no deletions.
+ assert.equal(element._computeBarAdditionWidth(file, stats), 30);
+
+ // If there are no insetions, there is no width.
+ stats.maxInserted = 0;
+ assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+
+ // If the insertions is not present on the file, there is no width.
+ stats.maxInserted = 10;
+ file.lines_inserted = undefined;
+ assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+
+ // If the file is a commit message, returns zero.
+ file.lines_inserted = 5;
+ file.__path = '/COMMIT_MSG';
+ assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+
+ // Width bottoms-out at the minimum width.
+ file.__path = 'stuff.txt';
+ file.lines_inserted = 1;
+ stats.maxInserted = 1000000;
+ assert.equal(element._computeBarAdditionWidth(file, stats), 1.5);
+ });
+
+ test('_computeBarAdditionX', () => {
+ const file = {
+ __path: 'foo/bar.baz',
+ lines_inserted: 5,
+ lines_deleted: 0,
+ };
+ const stats = {
+ maxInserted: 10,
+ maxDeleted: 0,
+ maxAdditionWidth: 60,
+ maxDeletionWidth: 0,
+ deletionOffset: 60,
+ };
+ assert.equal(element._computeBarAdditionX(file, stats), 30);
+ });
+
+ test('_computeBarDeletionWidth', () => {
+ const file = {
+ __path: 'foo/bar.baz',
+ lines_inserted: 0,
+ lines_deleted: 5,
+ };
+ const stats = {
+ maxInserted: 10,
+ maxDeleted: 10,
+ maxAdditionWidth: 30,
+ maxDeletionWidth: 30,
+ deletionOffset: 31,
+ };
+
+ // Uses a quarter the space when file is half the largest deletions and
+ // there are equal additions.
+ assert.equal(element._computeBarDeletionWidth(file, stats), 15);
+
+ // If there are no deletions, there is no width.
+ stats.maxDeleted = 0;
+ assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+
+ // If the deletions is not present on the file, there is no width.
+ stats.maxDeleted = 10;
+ file.lines_deleted = undefined;
+ assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+
+ // If the file is a commit message, returns zero.
+ file.lines_deleted = 5;
+ file.__path = '/COMMIT_MSG';
+ assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+
+ // Width bottoms-out at the minimum width.
+ file.__path = 'stuff.txt';
+ file.lines_deleted = 1;
+ stats.maxDeleted = 1000000;
+ assert.equal(element._computeBarDeletionWidth(file, stats), 1.5);
+ });
+
+ test('_computeSizeBarsClass', () => {
+ assert.equal(element._computeSizeBarsClass(false, 'foo/bar.baz'),
+ 'sizeBars desktop hide');
+ assert.equal(element._computeSizeBarsClass(true, '/COMMIT_MSG'),
+ 'sizeBars desktop invisible');
+ assert.equal(element._computeSizeBarsClass(true, 'foo/bar.baz'),
+ 'sizeBars desktop ');
+ });
+ });
+
+ suite('gr-file-list inline diff tests', () => {
+ let element;
+ let sandbox;
+
+ const commitMsgComments = [
+ {
+ patch_set: 2,
+ id: 'ecf0b9fa_fe1a5f62',
+ line: 20,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'another comment',
+ unresolved: true,
+ },
+ {
+ patch_set: 2,
+ id: '503008e2_0ab203ee',
+ line: 10,
+ updated: '2018-02-14 22:07:43.000000000',
+ message: 'a comment',
+ unresolved: true,
+ },
+ {
+ patch_set: 2,
+ id: 'cc788d2c_cb1d728c',
+ line: 20,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
+ updated: '2018-02-13 22:07:43.000000000',
+ message: 'response',
+ unresolved: true,
+ },
+ ];
+
+ const setupDiff = function(diff) {
+ const mock = document.createElement('mock-diff-response');
+ diff.comments = {
+ left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [],
+ right: [],
+ meta: {
+ changeNum: 1,
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ },
+ },
+ };
+ diff.prefs = {
+ context: 10,
+ tab_size: 8,
+ font_size: 12,
+ line_length: 100,
+ cursor_blink_rate: 0,
+ line_wrapping: false,
+ intraline_difference: true,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ auto_hide_diff_table_header: true,
+ theme: 'DEFAULT',
+ ignore_whitespace: 'IGNORE_NONE',
+ };
+ diff.diff = mock.diffResponse;
+ diff.$.diff.flushDebouncer('renderDiffTable');
+ };
+
+ const renderAndGetNewDiffs = function(index) {
+ const diffs =
+ dom(element.root).querySelectorAll('gr-diff-host');
+
+ for (let i = index; i < diffs.length; i++) {
+ setupDiff(diffs[i]);
+ }
+
+ element._updateDiffCursor();
+ element.$.diffCursor.handleDiffUpdate();
+ return diffs;
+ };
+
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getPreferences() { return Promise.resolve({}); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+ stub('gr-date-formatter', {
+ _loadTimeFormat() { return Promise.resolve(''); },
+ });
+ stub('gr-diff-host', {
+ reload() { return Promise.resolve(); },
+ });
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.fileList;
+ loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+ element.diffPrefs = {};
+ sandbox.stub(element, '_reviewFile');
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ commentApiWrapper.loadComments().then(() => {
+ sandbox.stub(element.changeComments, 'getPaths').returns({});
+ sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
+ .returns({meta: {}, left: [], right: []});
+ done();
+ });
+ element._loading = false;
+ element.numFilesShown = 75;
+ element.selectedIndex = 0;
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ size_delta: 10,
+ size: 100,
+ },
+ };
+ element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element._loggedIn = true;
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ sandbox.stub(window, 'fetch', () => Promise.resolve());
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('cursor with individually opened files', () => {
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+ let diffs = renderAndGetNewDiffs(0);
+ const diffStops = diffs[0].getCursorStops();
+
+ // 1 diff should be rendered.
+ assert.equal(diffs.length, 1);
+
+ // No line number is selected.
+ assert.isFalse(diffStops[10].classList.contains('target-row'));
+
+ // Tapping content on a line selects the line number.
+ MockInteractions.tap(dom(
+ diffStops[10]).querySelectorAll('.contentText')[0]);
+ flushAsynchronousOperations();
+ assert.isTrue(diffStops[10].classList.contains('target-row'));
+
+ // Keyboard shortcuts are still moving the file cursor, not the diff
+ // cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ flushAsynchronousOperations();
+ assert.isTrue(diffStops[10].classList.contains('target-row'));
+ assert.isFalse(diffStops[11].classList.contains('target-row'));
+
+ // The file cusor is now at 1.
+ assert.equal(element.$.fileCursor.index, 1);
+ MockInteractions.keyUpOn(element, 73, null, 'i');
+ flushAsynchronousOperations();
+
+ diffs = renderAndGetNewDiffs(1);
+ // Two diffs should be rendered.
+ assert.equal(diffs.length, 2);
+ const diffStopsFirst = diffs[0].getCursorStops();
+ const diffStopsSecond = diffs[1].getCursorStops();
+
+ // The line on the first diff is stil selected
+ assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
+ assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
+ });
+
+ test('cursor with toggle all files', () => {
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ flushAsynchronousOperations();
+
+ const diffs = renderAndGetNewDiffs(0);
+ const diffStops = diffs[0].getCursorStops();
+
+ // 1 diff should be rendered.
+ assert.equal(diffs.length, 3);
+
+ // No line number is selected.
+ assert.isFalse(diffStops[10].classList.contains('target-row'));
+
+ // Tapping content on a line selects the line number.
+ MockInteractions.tap(dom(
+ diffStops[10]).querySelectorAll('.contentText')[0]);
+ flushAsynchronousOperations();
+ assert.isTrue(diffStops[10].classList.contains('target-row'));
+
+ // Keyboard shortcuts are still moving the file cursor, not the diff
+ // cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ flushAsynchronousOperations();
+ assert.isFalse(diffStops[10].classList.contains('target-row'));
+ assert.isTrue(diffStops[11].classList.contains('target-row'));
+
+ // The file cusor is still at 0.
+ assert.equal(element.$.fileCursor.index, 0);
+ });
+
+ suite('n key presses', () => {
+ let nKeySpy;
+ let nextCommentStub;
+ let nextChunkStub;
+ let fileRows;
+
+ setup(() => {
+ sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
+ nKeySpy = sandbox.spy(element, '_handleNextChunk');
+ nextCommentStub = sandbox.stub(element.$.diffCursor,
+ 'moveToNextCommentThread');
+ nextChunkStub = sandbox.stub(element.$.diffCursor,
+ 'moveToNextChunk');
+ fileRows =
+ dom(element.root).querySelectorAll('.row:not(.header-row)');
+ });
+
+ test('n key with some files expanded and no shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(nextChunkStub.callCount, 1);
+
+ // Handle N key should return before calling diff cursor functions.
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isFalse(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 2);
+ assert.equal(element.filesExpanded, 'some');
+ });
+
+ test('n key with some files expanded and shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
+ flushAsynchronousOperations();
+ assert.equal(nextChunkStub.callCount, 1);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isTrue(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 1);
+ assert.equal(element.filesExpanded, 'some');
+ });
+
+ test('n key without all files expanded and shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ flushAsynchronousOperations();
+
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isFalse(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 2);
+ assert.isTrue(element._showInlineDiffs);
+ });
+
+ test('n key without all files expanded and no shift key', () => {
+ MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
+ flushAsynchronousOperations();
+
+ MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+ assert.isTrue(nKeySpy.called);
+ assert.isTrue(nextCommentStub.called);
+
+ // This is also called in diffCursor.moveToFirstChunk.
+ assert.equal(nextChunkStub.callCount, 1);
+ assert.isTrue(element._showInlineDiffs);
+ });
+ });
+
+ test('_openSelectedFile behavior', () => {
+ const _filesByPath = element._filesByPath;
+ element.set('_filesByPath', {});
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ // Noop when there are no files.
+ element._openSelectedFile();
+ assert.isFalse(navStub.called);
+
+ element.set('_filesByPath', _filesByPath);
+ flushAsynchronousOperations();
+ // Navigates when a file is selected.
+ element._openSelectedFile();
+ assert.isTrue(navStub.called);
+ });
+
+ test('_displayLine', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false);
+ sandbox.stub(element, 'modifierPressed', () => false);
+ element._showInlineDiffs = true;
+ const mockEvent = {preventDefault() {}};
+
+ element._displayLine = false;
+ element._handleCursorNext(mockEvent);
+ assert.isTrue(element._displayLine);
+
+ element._displayLine = false;
+ element._handleCursorPrev(mockEvent);
+ assert.isTrue(element._displayLine);
+
+ element._displayLine = true;
+ element._handleEscKey(mockEvent);
+ assert.isFalse(element._displayLine);
+ });
+
+ suite('editMode behavior', () => {
+ test('reviewed checkbox', () => {
+ element._reviewFile.restore();
+ const saveReviewStub = sandbox.stub(element, '_saveReviewedState');
+
+ element.editMode = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.isTrue(saveReviewStub.calledOnce);
+
element.editMode = true;
flushAsynchronousOperations();
- const clickSpy = sandbox.spy(element, '_handleFileListClick');
- const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
- // Tap the edit controls. Should be ignored by _handleFileListClick.
- MockInteractions.tap(element.shadowRoot
- .querySelector('.editFileControls'));
- assert.isTrue(clickSpy.calledOnce);
- assert.isFalse(toggleExpandSpy.called);
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.isTrue(saveReviewStub.calledOnce);
});
- test('patch set from revisions', () => {
- const expected = [
- {num: 4, desc: 'test', sha: 'rev4'},
- {num: 3, desc: 'test', sha: 'rev3'},
- {num: 2, desc: 'test', sha: 'rev2'},
- {num: 1, desc: 'test', sha: 'rev1'},
- ];
- const patchNums = element.computeAllPatchSets({
- revisions: {
- rev3: {_number: 3, description: 'test', date: 3},
- rev1: {_number: 1, description: 'test', date: 1},
- rev4: {_number: 4, description: 'test', date: 4},
- rev2: {_number: 2, description: 'test', date: 2},
- },
+ test('_getReviewedFiles does not call API', () => {
+ const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles');
+ element.editMode = true;
+ return element._getReviewedFiles().then(files => {
+ assert.equal(files.length, 0);
+ assert.isFalse(apiSpy.called);
});
- assert.equal(patchNums.length, expected.length);
- for (let i = 0; i < expected.length; i++) {
- assert.deepEqual(patchNums[i], expected[i]);
- }
- });
-
- test('checkbox shows/hides diff inline', () => {
- element._filesByPath = {
- 'myfile.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- element.$.fileCursor.setCursorAtIndex(0);
- sandbox.stub(element, '_expandedPathsChanged');
- flushAsynchronousOperations();
- const fileRows =
- Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
- // Because the label surrounds the input, the tap event is triggered
- // there first.
- const showHideLabel = fileRows[0].querySelector('label.show-hide');
- const showHideCheck = fileRows[0].querySelector(
- 'input.show-hide[type="checkbox"]');
- assert.isNotOk(showHideCheck.checked);
- MockInteractions.tap(showHideLabel);
- assert.isOk(showHideCheck.checked);
- assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
- });
-
- test('diff mode correctly toggles the diffs', () => {
- element._filesByPath = {
- 'myfile.txt': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- sandbox.spy(element, '_updateDiffPreferences');
- element.$.fileCursor.setCursorAtIndex(0);
- flushAsynchronousOperations();
-
- // Tap on a file to generate the diff.
- const row = Polymer.dom(element.root)
- .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
-
- MockInteractions.tap(row);
- flushAsynchronousOperations();
- const diffDisplay = element.diffs[0];
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
- element.set('diffViewMode', 'UNIFIED_DIFF');
- assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
- assert.isTrue(element._updateDiffPreferences.called);
- });
-
- test('expanded attribute not set on path when not expanded', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- };
- assert.isNotOk(element.shadowRoot
- .querySelector('.expanded'));
- });
-
- test('tapping row ignores links', () => {
- element._filesByPath = {
- '/COMMIT_MSG': {},
- };
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- sandbox.stub(element, '_expandedPathsChanged');
- flushAsynchronousOperations();
- const commitMsgFile = Polymer.dom(element.root)
- .querySelectorAll('.row:not(.header-row) a.pathLink')[0];
-
- // Remove href attribute so the app doesn't route to a diff view
- commitMsgFile.removeAttribute('href');
- const togglePathSpy = sandbox.spy(element, '_togglePathExpanded');
-
- MockInteractions.tap(commitMsgFile);
- flushAsynchronousOperations();
- assert(togglePathSpy.notCalled, 'file is opened as diff view');
- assert.isNotOk(element.shadowRoot
- .querySelector('.expanded'));
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.show-hide')).display,
- 'none');
- });
-
- test('_togglePathExpanded', () => {
- const path = 'path/to/my/file.txt';
- element._filesByPath = {[path]: {}};
- const renderSpy = sandbox.spy(element, '_renderInOrder');
- const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
-
- assert.equal(element.shadowRoot
- .querySelector('iron-icon').icon, 'gr-icons:expand-more');
- assert.equal(element._expandedFilePaths.length, 0);
- element._togglePathExpanded(path);
- flushAsynchronousOperations();
- assert.equal(collapseStub.lastCall.args[0].length, 0);
- assert.equal(element.shadowRoot
- .querySelector('iron-icon').icon, 'gr-icons:expand-less');
-
- assert.equal(renderSpy.callCount, 1);
- assert.include(element._expandedFilePaths, path);
- element._togglePathExpanded(path);
- flushAsynchronousOperations();
-
- assert.equal(element.shadowRoot
- .querySelector('iron-icon').icon, 'gr-icons:expand-more');
- assert.equal(renderSpy.callCount, 1);
- assert.notInclude(element._expandedFilePaths, path);
- assert.equal(collapseStub.lastCall.args[0].length, 1);
- });
-
- test('expandAllDiffs and collapseAllDiffs', () => {
- const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
- const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
- 'handleDiffUpdate');
-
- const path = 'path/to/my/file.txt';
- element._filesByPath = {[path]: {}};
- element.expandAllDiffs();
- flushAsynchronousOperations();
- assert.isTrue(element._showInlineDiffs);
- assert.isTrue(cursorUpdateStub.calledOnce);
- assert.equal(collapseStub.lastCall.args[0].length, 0);
-
- element.collapseAllDiffs();
- flushAsynchronousOperations();
- assert.equal(element._expandedFilePaths.length, 0);
- assert.isFalse(element._showInlineDiffs);
- assert.isTrue(cursorUpdateStub.calledTwice);
- assert.equal(collapseStub.lastCall.args[0].length, 1);
- });
-
- test('_expandedPathsChanged', done => {
- sandbox.stub(element, '_reviewFile');
- const path = 'path/to/my/file.txt';
- const diffs = [{
- path,
- style: {},
- reload() {
- done();
- },
- cancel() {},
- getCursorStops() { return []; },
- addEventListener(eventName, callback) {
- callback(new Event(eventName));
- },
- }];
- sinon.stub(element, 'diffs', {
- get() { return diffs; },
- });
- element.push('_expandedFilePaths', path);
- });
-
- test('_clearCollapsedDiffs', () => {
- const diff = {
- cancel: sinon.stub(),
- clearDiffContent: sinon.stub(),
- };
- element._clearCollapsedDiffs([diff]);
- assert.isTrue(diff.cancel.calledOnce);
- assert.isTrue(diff.clearDiffContent.calledOnce);
- });
-
- test('filesExpanded value updates to correct enum', () => {
- element._filesByPath = {
- 'foo.bar': {},
- 'baz.bar': {},
- };
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.NONE);
- element.push('_expandedFilePaths', 'baz.bar');
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.SOME);
- element.push('_expandedFilePaths', 'foo.bar');
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.ALL);
- element.collapseAllDiffs();
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.NONE);
- element.expandAllDiffs();
- flushAsynchronousOperations();
- assert.equal(element.filesExpanded,
- GrFileListConstants.FilesExpandedState.ALL);
- });
-
- test('_renderInOrder', done => {
- const reviewStub = sandbox.stub(element, '_reviewFile');
- let callCount = 0;
- const diffs = [{
- path: 'p0',
- style: {},
- reload() {
- assert.equal(callCount++, 2);
- return Promise.resolve();
- },
- }, {
- path: 'p1',
- style: {},
- reload() {
- assert.equal(callCount++, 1);
- return Promise.resolve();
- },
- }, {
- path: 'p2',
- style: {},
- reload() {
- assert.equal(callCount++, 0);
- return Promise.resolve();
- },
- }];
- element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
- .then(() => {
- assert.isFalse(reviewStub.called);
- assert.isTrue(loadCommentSpy.called);
- done();
- });
- });
-
- test('_renderInOrder logged in', done => {
- element._loggedIn = true;
- const reviewStub = sandbox.stub(element, '_reviewFile');
- let callCount = 0;
- const diffs = [{
- path: 'p0',
- style: {},
- reload() {
- assert.equal(reviewStub.callCount, 2);
- assert.equal(callCount++, 2);
- return Promise.resolve();
- },
- }, {
- path: 'p1',
- style: {},
- reload() {
- assert.equal(reviewStub.callCount, 1);
- assert.equal(callCount++, 1);
- return Promise.resolve();
- },
- }, {
- path: 'p2',
- style: {},
- reload() {
- assert.equal(reviewStub.callCount, 0);
- assert.equal(callCount++, 0);
- return Promise.resolve();
- },
- }];
- element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3)
- .then(() => {
- assert.equal(reviewStub.callCount, 3);
- done();
- });
- });
-
- test('_renderInOrder respects diffPrefs.manual_review', () => {
- element._loggedIn = true;
- element.diffPrefs = {manual_review: true};
- const reviewStub = sandbox.stub(element, '_reviewFile');
- const diffs = [{
- path: 'p',
- style: {},
- reload() { return Promise.resolve(); },
- }];
-
- return element._renderInOrder(['p'], diffs, 1).then(() => {
- assert.isFalse(reviewStub.called);
- delete element.diffPrefs.manual_review;
- return element._renderInOrder(['p'], diffs, 1).then(() => {
- assert.isTrue(reviewStub.called);
- assert.isTrue(reviewStub.calledWithExactly('p', true));
- });
- });
- });
-
- test('_loadingChanged fired from reload in debouncer', done => {
- sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
- element.changeNum = 123;
- element.patchRange = {patchNum: 12};
- element._filesByPath = {'foo.bar': {}};
-
- element.reload().then(() => {
- assert.isFalse(element._loading);
- element.flushDebouncer('loading-change');
- assert.isFalse(element.classList.contains('loading'));
- done();
- });
- assert.isTrue(element._loading);
- assert.isFalse(element.classList.contains('loading'));
- element.flushDebouncer('loading-change');
- assert.isTrue(element.classList.contains('loading'));
- });
-
- test('_loadingChanged does not set class when there are no files', () => {
- sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([]));
- element.changeNum = 123;
- element.patchRange = {patchNum: 12};
- element.reload();
- assert.isTrue(element._loading);
- element.flushDebouncer('loading-change');
- assert.isFalse(element.classList.contains('loading'));
});
});
- suite('diff url file list', () => {
- test('diff url', () => {
- const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
- .returns('/c/gerrit/+/1/1/index.php');
- const change = {
- _number: 1,
- project: 'gerrit',
- };
- const path = 'index.php';
- const patchRange = {
- patchNum: 1,
- };
- assert.equal(
- element._computeDiffURL(change, patchRange, path, false),
- '/c/gerrit/+/1/1/index.php');
- diffStub.restore();
- });
+ test('editing actions', () => {
+ // Edit controls are guarded behind a dom-if initially and not rendered.
+ assert.isNotOk(dom(element.root)
+ .querySelector('gr-edit-file-controls'));
- test('diff url commit msg', () => {
- const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff')
- .returns('/c/gerrit/+/1/1//COMMIT_MSG');
- const change = {
- _number: 1,
- project: 'gerrit',
- };
- const path = '/COMMIT_MSG';
- const patchRange = {
- patchNum: 1,
- };
- assert.equal(
- element._computeDiffURL(change, patchRange, path, false),
- '/c/gerrit/+/1/1//COMMIT_MSG');
- diffStub.restore();
- });
+ element.editMode = true;
+ flushAsynchronousOperations();
+
+ // Commit message should not have edit controls.
+ const editControls =
+ Array.from(
+ dom(element.root)
+ .querySelectorAll('.row:not(.header-row)'))
+ .map(row => row.querySelector('gr-edit-file-controls'));
+ assert.isTrue(editControls[0].classList.contains('invisible'));
});
- suite('size bars', () => {
- test('_computeSizeBarLayout', () => {
- assert.isUndefined(element._computeSizeBarLayout(null));
- assert.isUndefined(element._computeSizeBarLayout({}));
- assert.deepEqual(element._computeSizeBarLayout({base: []}), {
- maxInserted: 0,
- maxDeleted: 0,
- maxAdditionWidth: 0,
- maxDeletionWidth: 0,
- deletionOffset: 0,
- });
+ test('reloadCommentsForThreadWithRootId', () => {
+ // Expand the commit message diff
+ MockInteractions.keyUpOn(element, 73, 'shift', 'i');
+ const diffs = renderAndGetNewDiffs(0);
+ flushAsynchronousOperations();
- const files = [
- {__path: '/COMMIT_MSG', lines_inserted: 10000},
- {__path: 'foo', lines_inserted: 4, lines_deleted: 10},
- {__path: 'bar', lines_inserted: 5, lines_deleted: 8},
- ];
- const layout = element._computeSizeBarLayout({base: files});
- assert.equal(layout.maxInserted, 5);
- assert.equal(layout.maxDeleted, 10);
- });
+ // Two comment threads should be generated by renderAndGetNewDiffs
+ const threadEls = diffs[0].getThreadEls();
+ assert.equal(threadEls.length, 2);
+ const threadElsByRootId = new Map(
+ threadEls.map(threadEl => [threadEl.rootId, threadEl]));
- test('_computeBarAdditionWidth', () => {
- const file = {
- __path: 'foo/bar.baz',
- lines_inserted: 5,
- lines_deleted: 0,
- };
- const stats = {
- maxInserted: 10,
- maxDeleted: 0,
- maxAdditionWidth: 60,
- maxDeletionWidth: 0,
- deletionOffset: 60,
- };
+ const thread1 = threadElsByRootId.get('503008e2_0ab203ee');
+ assert.equal(thread1.comments.length, 1);
+ assert.equal(thread1.comments[0].message, 'a comment');
+ assert.equal(thread1.comments[0].line, 10);
- // Uses half the space when file is half the largest addition and there
- // are no deletions.
- assert.equal(element._computeBarAdditionWidth(file, stats), 30);
+ const thread2 = threadElsByRootId.get('ecf0b9fa_fe1a5f62');
+ assert.equal(thread2.comments.length, 2);
+ assert.isTrue(thread2.comments[0].unresolved);
+ assert.equal(thread2.comments[0].message, 'another comment');
+ assert.equal(thread2.comments[0].line, 20);
- // If there are no insetions, there is no width.
- stats.maxInserted = 0;
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
-
- // If the insertions is not present on the file, there is no width.
- stats.maxInserted = 10;
- file.lines_inserted = undefined;
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
-
- // If the file is a commit message, returns zero.
- file.lines_inserted = 5;
- file.__path = '/COMMIT_MSG';
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
-
- // Width bottoms-out at the minimum width.
- file.__path = 'stuff.txt';
- file.lines_inserted = 1;
- stats.maxInserted = 1000000;
- assert.equal(element._computeBarAdditionWidth(file, stats), 1.5);
- });
-
- test('_computeBarAdditionX', () => {
- const file = {
- __path: 'foo/bar.baz',
- lines_inserted: 5,
- lines_deleted: 0,
- };
- const stats = {
- maxInserted: 10,
- maxDeleted: 0,
- maxAdditionWidth: 60,
- maxDeletionWidth: 0,
- deletionOffset: 60,
- };
- assert.equal(element._computeBarAdditionX(file, stats), 30);
- });
-
- test('_computeBarDeletionWidth', () => {
- const file = {
- __path: 'foo/bar.baz',
- lines_inserted: 0,
- lines_deleted: 5,
- };
- const stats = {
- maxInserted: 10,
- maxDeleted: 10,
- maxAdditionWidth: 30,
- maxDeletionWidth: 30,
- deletionOffset: 31,
- };
-
- // Uses a quarter the space when file is half the largest deletions and
- // there are equal additions.
- assert.equal(element._computeBarDeletionWidth(file, stats), 15);
-
- // If there are no deletions, there is no width.
- stats.maxDeleted = 0;
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
-
- // If the deletions is not present on the file, there is no width.
- stats.maxDeleted = 10;
- file.lines_deleted = undefined;
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
-
- // If the file is a commit message, returns zero.
- file.lines_deleted = 5;
- file.__path = '/COMMIT_MSG';
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
-
- // Width bottoms-out at the minimum width.
- file.__path = 'stuff.txt';
- file.lines_deleted = 1;
- stats.maxDeleted = 1000000;
- assert.equal(element._computeBarDeletionWidth(file, stats), 1.5);
- });
-
- test('_computeSizeBarsClass', () => {
- assert.equal(element._computeSizeBarsClass(false, 'foo/bar.baz'),
- 'sizeBars desktop hide');
- assert.equal(element._computeSizeBarsClass(true, '/COMMIT_MSG'),
- 'sizeBars desktop invisible');
- assert.equal(element._computeSizeBarsClass(true, 'foo/bar.baz'),
- 'sizeBars desktop ');
- });
- });
-
- suite('gr-file-list inline diff tests', () => {
- let element;
- let sandbox;
-
- const commitMsgComments = [
+ const commentStub =
+ sandbox.stub(element.changeComments, 'getCommentsForThread');
+ const commentStubRes1 = [
+ {
+ patch_set: 2,
+ id: '503008e2_0ab203ee',
+ line: 20,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'edited text',
+ unresolved: false,
+ },
+ ];
+ const commentStubRes2 = [
{
patch_set: 2,
id: 'ecf0b9fa_fe1a5f62',
@@ -1446,453 +1856,58 @@
patch_set: 2,
id: '503008e2_0ab203ee',
line: 10,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
updated: '2018-02-14 22:07:43.000000000',
- message: 'a comment',
+ message: 'response',
unresolved: true,
},
{
patch_set: 2,
- id: 'cc788d2c_cb1d728c',
+ id: '503008e2_0ab203ef',
line: 20,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-13 22:07:43.000000000',
- message: 'response',
+ in_reply_to: '503008e2_0ab203ee',
+ updated: '2018-02-15 22:07:43.000000000',
+ message: 'a third comment in the thread',
unresolved: true,
},
];
+ commentStub.withArgs('503008e2_0ab203ee').returns(
+ commentStubRes1);
+ commentStub.withArgs('ecf0b9fa_fe1a5f62').returns(
+ commentStubRes2);
- const setupDiff = function(diff) {
- const mock = document.createElement('mock-diff-response');
- diff.comments = {
- left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [],
- right: [],
- meta: {
- changeNum: 1,
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 2,
- },
- },
- };
- diff.prefs = {
- context: 10,
- tab_size: 8,
- font_size: 12,
- line_length: 100,
- cursor_blink_rate: 0,
- line_wrapping: false,
- intraline_difference: true,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- auto_hide_diff_table_header: true,
- theme: 'DEFAULT',
- ignore_whitespace: 'IGNORE_NONE',
- };
- diff.diff = mock.diffResponse;
- diff.$.diff.flushDebouncer('renderDiffTable');
- };
+ // Reload comments from the first comment thread, which should have a
+ // an updated message and a toggled resolve state.
+ element.reloadCommentsForThreadWithRootId('503008e2_0ab203ee',
+ '/COMMIT_MSG');
+ assert.equal(thread1.comments.length, 1);
+ assert.isFalse(thread1.comments[0].unresolved);
+ assert.equal(thread1.comments[0].message, 'edited text');
- const renderAndGetNewDiffs = function(index) {
- const diffs =
- Polymer.dom(element.root).querySelectorAll('gr-diff-host');
+ // Reload comments from the second comment thread, which should have a new
+ // reply.
+ element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
+ '/COMMIT_MSG');
+ assert.equal(thread2.comments.length, 3);
- for (let i = index; i < diffs.length; i++) {
- setupDiff(diffs[i]);
- }
+ const commentStubCount = commentStub.callCount;
+ const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls');
- element._updateDiffCursor();
- element.$.diffCursor.handleDiffUpdate();
- return diffs;
- };
+ // Should not be getting threads when the file is not expanded.
+ element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
+ 'other/file');
+ assert.isFalse(getThreadsSpy.called);
+ assert.equal(commentStubCount, commentStub.callCount);
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getPreferences() { return Promise.resolve({}); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- });
- stub('gr-date-formatter', {
- _loadTimeFormat() { return Promise.resolve(''); },
- });
- stub('gr-diff-host', {
- reload() { return Promise.resolve(); },
- });
-
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.fileList;
- loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
- element.diffPrefs = {};
- sandbox.stub(element, '_reviewFile');
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- commentApiWrapper.loadComments().then(() => {
- sandbox.stub(element.changeComments, 'getPaths').returns({});
- sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
- .returns({meta: {}, left: [], right: []});
- done();
- });
- element._loading = false;
- element.numFilesShown = 75;
- element.selectedIndex = 0;
- element._filesByPath = {
- '/COMMIT_MSG': {lines_inserted: 9},
- 'file_added_in_rev2.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- 'myfile.txt': {
- lines_inserted: 1,
- lines_deleted: 1,
- size_delta: 10,
- size: 100,
- },
- };
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
- element._loggedIn = true;
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- sandbox.stub(window, 'fetch', () => Promise.resolve());
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('cursor with individually opened files', () => {
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
- let diffs = renderAndGetNewDiffs(0);
- const diffStops = diffs[0].getCursorStops();
-
- // 1 diff should be rendered.
- assert.equal(diffs.length, 1);
-
- // No line number is selected.
- assert.isFalse(diffStops[10].classList.contains('target-row'));
-
- // Tapping content on a line selects the line number.
- MockInteractions.tap(Polymer.dom(
- diffStops[10]).querySelectorAll('.contentText')[0]);
- flushAsynchronousOperations();
- assert.isTrue(diffStops[10].classList.contains('target-row'));
-
- // Keyboard shortcuts are still moving the file cursor, not the diff
- // cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- flushAsynchronousOperations();
- assert.isTrue(diffStops[10].classList.contains('target-row'));
- assert.isFalse(diffStops[11].classList.contains('target-row'));
-
- // The file cusor is now at 1.
- assert.equal(element.$.fileCursor.index, 1);
- MockInteractions.keyUpOn(element, 73, null, 'i');
- flushAsynchronousOperations();
-
- diffs = renderAndGetNewDiffs(1);
- // Two diffs should be rendered.
- assert.equal(diffs.length, 2);
- const diffStopsFirst = diffs[0].getCursorStops();
- const diffStopsSecond = diffs[1].getCursorStops();
-
- // The line on the first diff is stil selected
- assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
- assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
- });
-
- test('cursor with toggle all files', () => {
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- flushAsynchronousOperations();
-
- const diffs = renderAndGetNewDiffs(0);
- const diffStops = diffs[0].getCursorStops();
-
- // 1 diff should be rendered.
- assert.equal(diffs.length, 3);
-
- // No line number is selected.
- assert.isFalse(diffStops[10].classList.contains('target-row'));
-
- // Tapping content on a line selects the line number.
- MockInteractions.tap(Polymer.dom(
- diffStops[10]).querySelectorAll('.contentText')[0]);
- flushAsynchronousOperations();
- assert.isTrue(diffStops[10].classList.contains('target-row'));
-
- // Keyboard shortcuts are still moving the file cursor, not the diff
- // cursor.
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- flushAsynchronousOperations();
- assert.isFalse(diffStops[10].classList.contains('target-row'));
- assert.isTrue(diffStops[11].classList.contains('target-row'));
-
- // The file cusor is still at 0.
- assert.equal(element.$.fileCursor.index, 0);
- });
-
- suite('n key presses', () => {
- let nKeySpy;
- let nextCommentStub;
- let nextChunkStub;
- let fileRows;
-
- setup(() => {
- sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
- nKeySpy = sandbox.spy(element, '_handleNextChunk');
- nextCommentStub = sandbox.stub(element.$.diffCursor,
- 'moveToNextCommentThread');
- nextChunkStub = sandbox.stub(element.$.diffCursor,
- 'moveToNextChunk');
- fileRows =
- Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)');
- });
-
- test('n key with some files expanded and no shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(nextChunkStub.callCount, 1);
-
- // Handle N key should return before calling diff cursor functions.
- MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
- assert.isTrue(nKeySpy.called);
- assert.isFalse(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 2);
- assert.equal(element.filesExpanded, 'some');
- });
-
- test('n key with some files expanded and shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
- flushAsynchronousOperations();
- assert.equal(nextChunkStub.callCount, 1);
-
- MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
- assert.isTrue(nKeySpy.called);
- assert.isTrue(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 1);
- assert.equal(element.filesExpanded, 'some');
- });
-
- test('n key without all files expanded and shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
- flushAsynchronousOperations();
-
- MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
- assert.isTrue(nKeySpy.called);
- assert.isFalse(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 2);
- assert.isTrue(element._showInlineDiffs);
- });
-
- test('n key without all files expanded and no shift key', () => {
- MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
- flushAsynchronousOperations();
-
- MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
- assert.isTrue(nKeySpy.called);
- assert.isTrue(nextCommentStub.called);
-
- // This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 1);
- assert.isTrue(element._showInlineDiffs);
- });
- });
-
- test('_openSelectedFile behavior', () => {
- const _filesByPath = element._filesByPath;
- element.set('_filesByPath', {});
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- // Noop when there are no files.
- element._openSelectedFile();
- assert.isFalse(navStub.called);
-
- element.set('_filesByPath', _filesByPath);
- flushAsynchronousOperations();
- // Navigates when a file is selected.
- element._openSelectedFile();
- assert.isTrue(navStub.called);
- });
-
- test('_displayLine', () => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false);
- sandbox.stub(element, 'modifierPressed', () => false);
- element._showInlineDiffs = true;
- const mockEvent = {preventDefault() {}};
-
- element._displayLine = false;
- element._handleCursorNext(mockEvent);
- assert.isTrue(element._displayLine);
-
- element._displayLine = false;
- element._handleCursorPrev(mockEvent);
- assert.isTrue(element._displayLine);
-
- element._displayLine = true;
- element._handleEscKey(mockEvent);
- assert.isFalse(element._displayLine);
- });
-
- suite('editMode behavior', () => {
- test('reviewed checkbox', () => {
- element._reviewFile.restore();
- const saveReviewStub = sandbox.stub(element, '_saveReviewedState');
-
- element.editMode = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isTrue(saveReviewStub.calledOnce);
-
- element.editMode = true;
- flushAsynchronousOperations();
-
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isTrue(saveReviewStub.calledOnce);
- });
-
- test('_getReviewedFiles does not call API', () => {
- const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles');
- element.editMode = true;
- return element._getReviewedFiles().then(files => {
- assert.equal(files.length, 0);
- assert.isFalse(apiSpy.called);
- });
- });
- });
-
- test('editing actions', () => {
- // Edit controls are guarded behind a dom-if initially and not rendered.
- assert.isNotOk(Polymer.dom(element.root)
- .querySelector('gr-edit-file-controls'));
-
- element.editMode = true;
- flushAsynchronousOperations();
-
- // Commit message should not have edit controls.
- const editControls =
- Array.from(
- Polymer.dom(element.root)
- .querySelectorAll('.row:not(.header-row)'))
- .map(row => row.querySelector('gr-edit-file-controls'));
- assert.isTrue(editControls[0].classList.contains('invisible'));
- });
-
- test('reloadCommentsForThreadWithRootId', () => {
- // Expand the commit message diff
- MockInteractions.keyUpOn(element, 73, 'shift', 'i');
- const diffs = renderAndGetNewDiffs(0);
- flushAsynchronousOperations();
-
- // Two comment threads should be generated by renderAndGetNewDiffs
- const threadEls = diffs[0].getThreadEls();
- assert.equal(threadEls.length, 2);
- const threadElsByRootId = new Map(
- threadEls.map(threadEl => [threadEl.rootId, threadEl]));
-
- const thread1 = threadElsByRootId.get('503008e2_0ab203ee');
- assert.equal(thread1.comments.length, 1);
- assert.equal(thread1.comments[0].message, 'a comment');
- assert.equal(thread1.comments[0].line, 10);
-
- const thread2 = threadElsByRootId.get('ecf0b9fa_fe1a5f62');
- assert.equal(thread2.comments.length, 2);
- assert.isTrue(thread2.comments[0].unresolved);
- assert.equal(thread2.comments[0].message, 'another comment');
- assert.equal(thread2.comments[0].line, 20);
-
- const commentStub =
- sandbox.stub(element.changeComments, 'getCommentsForThread');
- const commentStubRes1 = [
- {
- patch_set: 2,
- id: '503008e2_0ab203ee',
- line: 20,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'edited text',
- unresolved: false,
- },
- ];
- const commentStubRes2 = [
- {
- patch_set: 2,
- id: 'ecf0b9fa_fe1a5f62',
- line: 20,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'another comment',
- unresolved: true,
- },
- {
- patch_set: 2,
- id: '503008e2_0ab203ee',
- line: 10,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-14 22:07:43.000000000',
- message: 'response',
- unresolved: true,
- },
- {
- patch_set: 2,
- id: '503008e2_0ab203ef',
- line: 20,
- in_reply_to: '503008e2_0ab203ee',
- updated: '2018-02-15 22:07:43.000000000',
- message: 'a third comment in the thread',
- unresolved: true,
- },
- ];
- commentStub.withArgs('503008e2_0ab203ee').returns(
- commentStubRes1);
- commentStub.withArgs('ecf0b9fa_fe1a5f62').returns(
- commentStubRes2);
-
- // Reload comments from the first comment thread, which should have a
- // an updated message and a toggled resolve state.
- element.reloadCommentsForThreadWithRootId('503008e2_0ab203ee',
- '/COMMIT_MSG');
- assert.equal(thread1.comments.length, 1);
- assert.isFalse(thread1.comments[0].unresolved);
- assert.equal(thread1.comments[0].message, 'edited text');
-
- // Reload comments from the second comment thread, which should have a new
- // reply.
- element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
- '/COMMIT_MSG');
- assert.equal(thread2.comments.length, 3);
-
- const commentStubCount = commentStub.callCount;
- const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls');
-
- // Should not be getting threads when the file is not expanded.
- element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62',
- 'other/file');
- assert.isFalse(getThreadsSpy.called);
- assert.equal(commentStubCount, commentStub.callCount);
-
- // Should be query selecting diffs when the file is expanded.
- // Should not be fetching change comments when the rootId is not found
- // to match.
- element.reloadCommentsForThreadWithRootId('acf0b9fa_fe1a5f62',
- '/COMMIT_MSG');
- assert.isTrue(getThreadsSpy.called);
- assert.equal(commentStubCount, commentStub.callCount);
- });
+ // Should be query selecting diffs when the file is expanded.
+ // Should not be fetching change comments when the rootId is not found
+ // to match.
+ element.reloadCommentsForThreadWithRootId('acf0b9fa_fe1a5f62',
+ '/COMMIT_MSG');
+ assert.isTrue(getThreadsSpy.called);
+ assert.equal(commentStubCount, commentStub.callCount);
});
- a11ySuite('basic');
});
+ a11ySuite('basic');
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index 01c9b6e..bdf8c1d 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -14,98 +14,109 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-included-in-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrIncludedInDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-included-in-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the user presses the close button.
+ *
+ * @event close
*/
- class GrIncludedInDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-included-in-dialog'; }
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
- static get properties() {
- return {
+ static get properties() {
+ return {
+ /** @type {?} */
+ changeNum: {
+ type: Object,
+ observer: '_resetData',
+ },
/** @type {?} */
- changeNum: {
- type: Object,
- observer: '_resetData',
- },
- /** @type {?} */
- _includedIn: Object,
- _loaded: {
- type: Boolean,
- value: false,
- },
- _filterText: {
- type: String,
- value: '',
- },
- };
- }
-
- loadData() {
- if (!this.changeNum) { return; }
- this._filterText = '';
- return this.$.restAPI.getChangeIncludedIn(this.changeNum).then(
- configs => {
- if (!configs) { return; }
- this._includedIn = configs;
- this._loaded = true;
- });
- }
-
- _resetData() {
- this._includedIn = null;
- this._loaded = false;
- }
-
- _computeGroups(includedIn, filterText) {
- if (!includedIn) { return []; }
-
- const filter = item => !filterText.length ||
- item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
-
- const groups = [
- {title: 'Branches', items: includedIn.branches.filter(filter)},
- {title: 'Tags', items: includedIn.tags.filter(filter)},
- ];
- if (includedIn.external) {
- for (const externalKey of Object.keys(includedIn.external)) {
- groups.push({
- title: externalKey,
- items: includedIn.external[externalKey].filter(filter),
- });
- }
- }
- return groups.filter(g => g.items.length);
- }
-
- _handleCloseTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('close', null, {bubbles: false});
- }
-
- _computeLoadingClass(loaded) {
- return loaded ? 'loading loaded' : 'loading';
- }
-
- _onFilterChanged() {
- this.debounce('filter-change', () => {
- this._filterText = this.$.filterInput.bindValue;
- }, 100);
- }
+ _includedIn: Object,
+ _loaded: {
+ type: Boolean,
+ value: false,
+ },
+ _filterText: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrIncludedInDialog.is, GrIncludedInDialog);
-})();
+ loadData() {
+ if (!this.changeNum) { return; }
+ this._filterText = '';
+ return this.$.restAPI.getChangeIncludedIn(this.changeNum).then(
+ configs => {
+ if (!configs) { return; }
+ this._includedIn = configs;
+ this._loaded = true;
+ });
+ }
+
+ _resetData() {
+ this._includedIn = null;
+ this._loaded = false;
+ }
+
+ _computeGroups(includedIn, filterText) {
+ if (!includedIn) { return []; }
+
+ const filter = item => !filterText.length ||
+ item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
+
+ const groups = [
+ {title: 'Branches', items: includedIn.branches.filter(filter)},
+ {title: 'Tags', items: includedIn.tags.filter(filter)},
+ ];
+ if (includedIn.external) {
+ for (const externalKey of Object.keys(includedIn.external)) {
+ groups.push({
+ title: externalKey,
+ items: includedIn.external[externalKey].filter(filter),
+ });
+ }
+ }
+ return groups.filter(g => g.items.length);
+ }
+
+ _handleCloseTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('close', null, {bubbles: false});
+ }
+
+ _computeLoadingClass(loaded) {
+ return loaded ? 'loading loaded' : 'loading';
+ }
+
+ _onFilterChanged() {
+ this.debounce('filter-change', () => {
+ this._filterText = this.$.filterInput.bindValue;
+ }, 100);
+ }
+}
+
+customElements.define(GrIncludedInDialog.is, GrIncludedInDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
index 075b41e..b7d455c 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
@@ -1,29 +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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-included-in-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
background-color: var(--dialog-background-color);
@@ -76,25 +69,14 @@
<header>
<h1 id="title">Included In:</h1>
<span class="closeButtonContainer">
- <gr-button id="closeButton"
- link
- on-click="_handleCloseTap">Close</gr-button>
+ <gr-button id="closeButton" link="" on-click="_handleCloseTap">Close</gr-button>
</span>
- <iron-input
- placeholder="Filter"
- on-bind-value-changed="_onFilterChanged">
- <input
- id="filterInput"
- is="iron-input"
- placeholder="Filter"
- on-bind-value-changed="_onFilterChanged">
+ <iron-input placeholder="Filter" on-bind-value-changed="_onFilterChanged">
+ <input id="filterInput" is="iron-input" placeholder="Filter" on-bind-value-changed="_onFilterChanged">
</iron-input>
</header>
- <div class$="[[_computeLoadingClass(_loaded)]]">Loading...</div>
- <template
- is="dom-repeat"
- items="[[_computeGroups(_includedIn, _filterText)]]"
- as="group">
+ <div class\$="[[_computeLoadingClass(_loaded)]]">Loading...</div>
+ <template is="dom-repeat" items="[[_computeGroups(_includedIn, _filterText)]]" as="group">
<div>
<span>[[group.title]]:</span>
<ul>
@@ -105,6 +87,4 @@
</div>
</template>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-included-in-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
index bec6c7b..deb00bd 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-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-included-in-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-included-in-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-included-in-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-included-in-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,54 +40,56 @@
</template>
</test-fixture>
-<script>
- suite('gr-included-in-dialog', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-included-in-dialog.js';
+suite('gr-included-in-dialog', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('_computeGroups', () => {
- const includedIn = {branches: [], tags: []};
- let filterText = '';
- assert.deepEqual(element._computeGroups(includedIn, filterText), []);
-
- includedIn.branches.push('master', 'development', 'stable-2.0');
- includedIn.tags.push('v1.9', 'v2.0', 'v2.1');
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
- {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
- ]);
-
- includedIn.external = {};
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
- {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
- ]);
-
- includedIn.external.foo = ['abc', 'def', 'ghi'];
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
- {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
- {title: 'foo', items: ['abc', 'def', 'ghi']},
- ]);
-
- filterText = 'v2';
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Tags', items: ['v2.0', 'v2.1']},
- ]);
-
- // Filtering is case-insensitive.
- filterText = 'V2';
- assert.deepEqual(element._computeGroups(includedIn, filterText), [
- {title: 'Tags', items: ['v2.0', 'v2.1']},
- ]);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('_computeGroups', () => {
+ const includedIn = {branches: [], tags: []};
+ let filterText = '';
+ assert.deepEqual(element._computeGroups(includedIn, filterText), []);
+
+ includedIn.branches.push('master', 'development', 'stable-2.0');
+ includedIn.tags.push('v1.9', 'v2.0', 'v2.1');
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+ {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+ ]);
+
+ includedIn.external = {};
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+ {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+ ]);
+
+ includedIn.external.foo = ['abc', 'def', 'ghi'];
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
+ {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
+ {title: 'foo', items: ['abc', 'def', 'ghi']},
+ ]);
+
+ filterText = 'v2';
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Tags', items: ['v2.0', 'v2.1']},
+ ]);
+
+ // Filtering is case-insensitive.
+ filterText = 'V2';
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Tags', items: ['v2.0', 'v2.1']},
+ ]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index 0316428..8541840 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -14,167 +14,176 @@
* 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 GrLabelScoreRow extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-label-score-row'; }
+import '@polymer/iron-selector/iron-selector.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../../styles/gr-voting-styles.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-label-score-row_html.js';
+
+/** @extends Polymer.Element */
+class GrLabelScoreRow extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-label-score-row'; }
+ /**
+ * Fired when any label is changed.
+ *
+ * @event labels-changed
+ */
+
+ static get properties() {
+ return {
/**
- * Fired when any label is changed.
- *
- * @event labels-changed
+ * @type {{ name: string }}
*/
+ label: Object,
+ labels: Object,
+ name: {
+ type: String,
+ reflectToAttribute: true,
+ },
+ permittedLabels: Object,
+ labelValues: Object,
+ _selectedValueText: {
+ type: String,
+ value: 'No value selected',
+ },
+ _items: {
+ type: Array,
+ computed: '_computePermittedLabelValues(permittedLabels, label.name)',
+ },
+ };
+ }
- static get properties() {
- return {
- /**
- * @type {{ name: string }}
- */
- label: Object,
- labels: Object,
- name: {
- type: String,
- reflectToAttribute: true,
- },
- permittedLabels: Object,
- labelValues: Object,
- _selectedValueText: {
- type: String,
- value: 'No value selected',
- },
- _items: {
- type: Array,
- computed: '_computePermittedLabelValues(permittedLabels, label.name)',
- },
- };
+ get selectedItem() {
+ if (!this._ironSelector) { return undefined; }
+ return this._ironSelector.selectedItem;
+ }
+
+ get selectedValue() {
+ if (!this._ironSelector) { return undefined; }
+ return this._ironSelector.selected;
+ }
+
+ setSelectedValue(value) {
+ // The selector may not be present if it’s not at the latest patch set.
+ if (!this._ironSelector) { return; }
+ this._ironSelector.select(value);
+ }
+
+ get _ironSelector() {
+ return this.$ && this.$.labelSelector;
+ }
+
+ _computeBlankItems(permittedLabels, label, side) {
+ if (!permittedLabels || !permittedLabels[label] ||
+ !permittedLabels[label].length || !this.labelValues ||
+ !Object.keys(this.labelValues).length) {
+ return [];
}
-
- get selectedItem() {
- if (!this._ironSelector) { return undefined; }
- return this._ironSelector.selectedItem;
+ const startPosition = this.labelValues[parseInt(
+ permittedLabels[label][0], 10)];
+ if (side === 'start') {
+ return new Array(startPosition);
}
+ const endPosition = this.labelValues[parseInt(
+ permittedLabels[label][permittedLabels[label].length - 1], 10)];
+ return new Array(Object.keys(this.labelValues).length - endPosition - 1);
+ }
- get selectedValue() {
- if (!this._ironSelector) { return undefined; }
- return this._ironSelector.selected;
- }
-
- setSelectedValue(value) {
- // The selector may not be present if it’s not at the latest patch set.
- if (!this._ironSelector) { return; }
- this._ironSelector.select(value);
- }
-
- get _ironSelector() {
- return this.$ && this.$.labelSelector;
- }
-
- _computeBlankItems(permittedLabels, label, side) {
- if (!permittedLabels || !permittedLabels[label] ||
- !permittedLabels[label].length || !this.labelValues ||
- !Object.keys(this.labelValues).length) {
- return [];
- }
- const startPosition = this.labelValues[parseInt(
- permittedLabels[label][0], 10)];
- if (side === 'start') {
- return new Array(startPosition);
- }
- const endPosition = this.labelValues[parseInt(
- permittedLabels[label][permittedLabels[label].length - 1], 10)];
- return new Array(Object.keys(this.labelValues).length - endPosition - 1);
- }
-
- _getLabelValue(labels, permittedLabels, label) {
- if (label.value) {
- return label.value;
- } else if (labels[label.name].hasOwnProperty('default_value') &&
- permittedLabels.hasOwnProperty(label.name)) {
- // default_value is an int, convert it to string label, e.g. "+1".
- return permittedLabels[label.name].find(
- value => parseInt(value, 10) === labels[label.name].default_value);
- }
- }
-
- /**
- * Maps the label value to exactly one of: min, max, positive, negative,
- * neutral. Used for the 'vote' attribute, because we don't want to
- * interfere with <iron-selector> using the 'class' attribute for setting
- * 'iron-selected'.
- */
- _computeVoteAttribute(value, index, totalItems) {
- if (value < 0 && index === 0) {
- return 'min';
- } else if (value < 0) {
- return 'negative';
- } else if (value > 0 && index === totalItems - 1) {
- return 'max';
- } else if (value > 0) {
- return 'positive';
- } else {
- return 'neutral';
- }
- }
-
- _computeLabelValue(labels, permittedLabels, label) {
- if ([labels, permittedLabels, label].some(arg => arg === undefined)) {
- return null;
- }
- if (!labels[label.name]) { return null; }
- const labelValue = this._getLabelValue(labels, permittedLabels, label);
- const len = permittedLabels[label.name] != null ?
- permittedLabels[label.name].length : 0;
- for (let i = 0; i < len; i++) {
- const val = permittedLabels[label.name][i];
- if (val === labelValue) {
- return val;
- }
- }
- return null;
- }
-
- _setSelectedValueText(e) {
- // Needed because when the selected item changes, it first changes to
- // nothing and then to the new item.
- if (!e.target.selectedItem) { return; }
- this._selectedValueText = e.target.selectedItem.getAttribute('title');
- // Needed to update the style of the selected button.
- this.updateStyles();
- const name = e.target.selectedItem.dataset.name;
- const value = e.target.selectedItem.dataset.value;
- this.dispatchEvent(new CustomEvent(
- 'labels-changed',
- {detail: {name, value}, bubbles: true, composed: true}));
- }
-
- _computeAnyPermittedLabelValues(permittedLabels, label) {
- return permittedLabels && permittedLabels.hasOwnProperty(label) &&
- permittedLabels[label].length;
- }
-
- _computeHiddenClass(permittedLabels, label) {
- return !this._computeAnyPermittedLabelValues(permittedLabels, label) ?
- 'hidden' : '';
- }
-
- _computePermittedLabelValues(permittedLabels, label) {
- // Polymer 2: check for undefined
- if ([permittedLabels, label].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return permittedLabels[label];
- }
-
- _computeLabelValueTitle(labels, label, value) {
- return labels[label] &&
- labels[label].values &&
- labels[label].values[value];
+ _getLabelValue(labels, permittedLabels, label) {
+ if (label.value) {
+ return label.value;
+ } else if (labels[label.name].hasOwnProperty('default_value') &&
+ permittedLabels.hasOwnProperty(label.name)) {
+ // default_value is an int, convert it to string label, e.g. "+1".
+ return permittedLabels[label.name].find(
+ value => parseInt(value, 10) === labels[label.name].default_value);
}
}
- customElements.define(GrLabelScoreRow.is, GrLabelScoreRow);
-})();
+ /**
+ * Maps the label value to exactly one of: min, max, positive, negative,
+ * neutral. Used for the 'vote' attribute, because we don't want to
+ * interfere with <iron-selector> using the 'class' attribute for setting
+ * 'iron-selected'.
+ */
+ _computeVoteAttribute(value, index, totalItems) {
+ if (value < 0 && index === 0) {
+ return 'min';
+ } else if (value < 0) {
+ return 'negative';
+ } else if (value > 0 && index === totalItems - 1) {
+ return 'max';
+ } else if (value > 0) {
+ return 'positive';
+ } else {
+ return 'neutral';
+ }
+ }
+
+ _computeLabelValue(labels, permittedLabels, label) {
+ if ([labels, permittedLabels, label].some(arg => arg === undefined)) {
+ return null;
+ }
+ if (!labels[label.name]) { return null; }
+ const labelValue = this._getLabelValue(labels, permittedLabels, label);
+ const len = permittedLabels[label.name] != null ?
+ permittedLabels[label.name].length : 0;
+ for (let i = 0; i < len; i++) {
+ const val = permittedLabels[label.name][i];
+ if (val === labelValue) {
+ return val;
+ }
+ }
+ return null;
+ }
+
+ _setSelectedValueText(e) {
+ // Needed because when the selected item changes, it first changes to
+ // nothing and then to the new item.
+ if (!e.target.selectedItem) { return; }
+ this._selectedValueText = e.target.selectedItem.getAttribute('title');
+ // Needed to update the style of the selected button.
+ this.updateStyles();
+ const name = e.target.selectedItem.dataset.name;
+ const value = e.target.selectedItem.dataset.value;
+ this.dispatchEvent(new CustomEvent(
+ 'labels-changed',
+ {detail: {name, value}, bubbles: true, composed: true}));
+ }
+
+ _computeAnyPermittedLabelValues(permittedLabels, label) {
+ return permittedLabels && permittedLabels.hasOwnProperty(label) &&
+ permittedLabels[label].length;
+ }
+
+ _computeHiddenClass(permittedLabels, label) {
+ return !this._computeAnyPermittedLabelValues(permittedLabels, label) ?
+ 'hidden' : '';
+ }
+
+ _computePermittedLabelValues(permittedLabels, label) {
+ // Polymer 2: check for undefined
+ if ([permittedLabels, label].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return permittedLabels[label];
+ }
+
+ _computeLabelValueTitle(labels, label, value) {
+ return labels[label] &&
+ labels[label].values &&
+ labels[label].values[value];
+ }
+}
+
+customElements.define(GrLabelScoreRow.is, GrLabelScoreRow);
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
index 50c01aa..1b5b425 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
@@ -1,28 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-selector/iron-selector.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../../styles/gr-voting-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-label-score-row">
- <template>
+export const htmlTemplate = html`
<style include="gr-voting-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -99,42 +93,23 @@
</style>
<span class="labelNameCell">[[label.name]]</span>
<div class="buttonsCell">
- <template is="dom-repeat"
- items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]"
- as="value">
- <span class="placeholder" data-label$="[[label.name]]"></span>
+ <template is="dom-repeat" items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]" as="value">
+ <span class="placeholder" data-label\$="[[label.name]]"></span>
</template>
- <iron-selector
- id="labelSelector"
- attr-for-selected="data-value"
- selected="[[_computeLabelValue(labels, permittedLabels, label)]]"
- hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"
- on-selected-item-changed="_setSelectedValueText">
- <template is="dom-repeat"
- items="[[_items]]"
- as="value">
- <gr-button
- vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
- has-tooltip
- data-name$="[[label.name]]"
- data-value$="[[value]]"
- title$="[[_computeLabelValueTitle(labels, label.name, value)]]">
+ <iron-selector id="labelSelector" attr-for-selected="data-value" selected="[[_computeLabelValue(labels, permittedLabels, label)]]" hidden\$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]" on-selected-item-changed="_setSelectedValueText">
+ <template is="dom-repeat" items="[[_items]]" as="value">
+ <gr-button vote\$="[[_computeVoteAttribute(value, index, _items.length)]]" has-tooltip="" data-name\$="[[label.name]]" data-value\$="[[value]]" title\$="[[_computeLabelValueTitle(labels, label.name, value)]]">
[[value]]</gr-button>
</template>
</iron-selector>
- <template is="dom-repeat"
- items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]"
- as="value">
- <span class="placeholder" data-label$="[[label.name]]"></span>
+ <template is="dom-repeat" items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]" as="value">
+ <span class="placeholder" data-label\$="[[label.name]]"></span>
</template>
- <span class="labelMessage"
- hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
+ <span class="labelMessage" hidden\$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]">
You don't have permission to edit this label.
</span>
</div>
- <div class$="selectedValueCell [[_computeHiddenClass(permittedLabels, label.name)]]">
+ <div class\$="selectedValueCell [[_computeHiddenClass(permittedLabels, label.name)]]">
<span id="selectedValueLabel">[[_selectedValueText]]</span>
</div>
- </template>
- <script src="gr-label-score-row.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index b10b932..18ba2a5 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-label-score-row</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="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/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-label-score-row.html">
+<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-label-score-row.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-label-score-row.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,340 +41,343 @@
</template>
</test-fixture>
-<script>
- suite('gr-label-row-score 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-label-score-row.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-label-row-score tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.labels = {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
},
- 'Verified': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
+ }],
+ },
+ 'Verified': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
},
- };
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
+ value: 1,
+ }],
+ },
+ };
+
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-2',
+ '-1',
+ ' 0',
+ '+1',
+ '+2',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+
+ element.labelValues = {'0': 2, '1': 3, '2': 4, '-2': 0, '-1': 1};
+
+ element.label = {
+ name: 'Verified',
+ value: '+1',
+ };
+
+ flush(done);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('label picker', () => {
+ const labelsChangedHandler = sandbox.stub();
+ element.addEventListener('labels-changed', labelsChangedHandler);
+ assert.ok(element.$.labelSelector);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector(
+ 'gr-button[data-value="-1"]'));
+ flushAsynchronousOperations();
+ assert.strictEqual(element.selectedValue, '-1');
+ assert.strictEqual(element.selectedItem
+ .textContent.trim(), '-1');
+ assert.strictEqual(
+ element.$.selectedValueLabel.textContent.trim(), 'bad');
+ const detail = labelsChangedHandler.args[0][0].detail;
+ assert.equal(detail.name, 'Verified');
+ assert.equal(detail.value, '-1');
+ });
+
+ test('_computeVoteAttribute', () => {
+ let value = 1;
+ let index = 0;
+ const totalItems = 5;
+ // positive and first position
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'positive');
+ // negative and first position
+ value = -1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'min');
+ // negative but not first position
+ index = 1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'negative');
+ // neutral
+ value = 0;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'neutral');
+ // positive but not last position
+ value = 1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'positive');
+ // positive and last position
+ index = 4;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'max');
+ // negative and last position
+ value = -1;
+ assert.equal(element._computeVoteAttribute(value, index,
+ totalItems), 'negative');
+ });
+
+ test('correct item is selected', () => {
+ // 1 should be the value of the selected item
+ assert.strictEqual(element.$.labelSelector.selected, '+1');
+ assert.strictEqual(
+ element.$.labelSelector.selectedItem
+ .textContent.trim(), '+1');
+ assert.strictEqual(
+ element.$.selectedValueLabel.textContent.trim(), 'good');
+ });
+
+ test('do not display tooltips on touch devices', () => {
+ const verifiedBtn = element.shadowRoot
+ .querySelector(
+ 'iron-selector > gr-button[data-value="-1"]');
+
+ // On touch devices, tooltips should not be shown.
+ verifiedBtn._isTouchDevice = true;
+ verifiedBtn._handleShowTooltip();
+ assert.isNotOk(verifiedBtn._tooltip);
+ verifiedBtn._handleHideTooltip();
+ assert.isNotOk(verifiedBtn._tooltip);
+
+ // On other devices, tooltips should be shown.
+ verifiedBtn._isTouchDevice = false;
+ verifiedBtn._handleShowTooltip();
+ assert.isOk(verifiedBtn._tooltip);
+ verifiedBtn._handleHideTooltip();
+ assert.isNotOk(verifiedBtn._tooltip);
+ });
+
+ test('_computeLabelValue', () => {
+ assert.strictEqual(element._computeLabelValue(element.labels,
+ element.permittedLabels,
+ element.label), '+1');
+ });
+
+ test('_computeBlankItems', () => {
+ element.labelValues = {
+ '-2': 0,
+ '-1': 1,
+ '0': 2,
+ '1': 3,
+ '2': 4,
+ };
+
+ assert.strictEqual(element._computeBlankItems(element.permittedLabels,
+ 'Code-Review').length, 0);
+
+ assert.strictEqual(element._computeBlankItems(element.permittedLabels,
+ 'Verified').length, 1);
+ });
+
+ test('labelValues returns no keys', () => {
+ element.labelValues = {};
+
+ assert.deepEqual(element._computeBlankItems(element.permittedLabels,
+ 'Code-Review'), []);
+ });
+
+ test('changes in label score are reflected in the DOM', () => {
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ 'Verified': {
+ values: {
+ ' 0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ };
+ const selector = element.$.labelSelector;
+ element.set('label', {name: 'Verified', value: ' 0'});
+ flushAsynchronousOperations();
+ assert.strictEqual(selector.selected, ' 0');
+ assert.strictEqual(
+ element.$.selectedValueLabel.textContent.trim(), 'No score');
+ });
+
+ test('without permitted labels', () => {
+ element.permittedLabels = {
+ Verified: [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ flushAsynchronousOperations();
+ assert.isOk(element.$.labelSelector);
+ assert.isFalse(element.$.labelSelector.hidden);
+
+ element.permittedLabels = {};
+ flushAsynchronousOperations();
+ assert.isOk(element.$.labelSelector);
+ assert.isTrue(element.$.labelSelector.hidden);
+
+ element.permittedLabels = {Verified: []};
+ flushAsynchronousOperations();
+ assert.isOk(element.$.labelSelector);
+ assert.isTrue(element.$.labelSelector.hidden);
+ });
+
+ test('asymetrical labels', done => {
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-2',
+ '-1',
+ ' 0',
+ '+1',
+ '+2',
+ ],
+ 'Verified': [
+ ' 0',
+ '+1',
+ ],
+ };
+ flush(() => {
+ assert.strictEqual(element.$.labelSelector
+ .items.length, 2);
+ assert.strictEqual(
+ dom(element.root).querySelectorAll('.placeholder').length,
+ 3);
element.permittedLabels = {
'Code-Review': [
+ ' 0',
+ '+1',
+ ],
+ 'Verified': [
'-2',
'-1',
' 0',
'+1',
'+2',
],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
-
- element.labelValues = {'0': 2, '1': 3, '2': 4, '-2': 0, '-1': 1};
-
- element.label = {
- name: 'Verified',
- value: '+1',
- };
-
- flush(done);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('label picker', () => {
- const labelsChangedHandler = sandbox.stub();
- element.addEventListener('labels-changed', labelsChangedHandler);
- assert.ok(element.$.labelSelector);
- MockInteractions.tap(element.shadowRoot
- .querySelector(
- 'gr-button[data-value="-1"]'));
- flushAsynchronousOperations();
- assert.strictEqual(element.selectedValue, '-1');
- assert.strictEqual(element.selectedItem
- .textContent.trim(), '-1');
- assert.strictEqual(
- element.$.selectedValueLabel.textContent.trim(), 'bad');
- const detail = labelsChangedHandler.args[0][0].detail;
- assert.equal(detail.name, 'Verified');
- assert.equal(detail.value, '-1');
- });
-
- test('_computeVoteAttribute', () => {
- let value = 1;
- let index = 0;
- const totalItems = 5;
- // positive and first position
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'positive');
- // negative and first position
- value = -1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'min');
- // negative but not first position
- index = 1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'negative');
- // neutral
- value = 0;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'neutral');
- // positive but not last position
- value = 1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'positive');
- // positive and last position
- index = 4;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'max');
- // negative and last position
- value = -1;
- assert.equal(element._computeVoteAttribute(value, index,
- totalItems), 'negative');
- });
-
- test('correct item is selected', () => {
- // 1 should be the value of the selected item
- assert.strictEqual(element.$.labelSelector.selected, '+1');
- assert.strictEqual(
- element.$.labelSelector.selectedItem
- .textContent.trim(), '+1');
- assert.strictEqual(
- element.$.selectedValueLabel.textContent.trim(), 'good');
- });
-
- test('do not display tooltips on touch devices', () => {
- const verifiedBtn = element.shadowRoot
- .querySelector(
- 'iron-selector > gr-button[data-value="-1"]');
-
- // On touch devices, tooltips should not be shown.
- verifiedBtn._isTouchDevice = true;
- verifiedBtn._handleShowTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
- verifiedBtn._handleHideTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
-
- // On other devices, tooltips should be shown.
- verifiedBtn._isTouchDevice = false;
- verifiedBtn._handleShowTooltip();
- assert.isOk(verifiedBtn._tooltip);
- verifiedBtn._handleHideTooltip();
- assert.isNotOk(verifiedBtn._tooltip);
- });
-
- test('_computeLabelValue', () => {
- assert.strictEqual(element._computeLabelValue(element.labels,
- element.permittedLabels,
- element.label), '+1');
- });
-
- test('_computeBlankItems', () => {
- element.labelValues = {
- '-2': 0,
- '-1': 1,
- '0': 2,
- '1': 3,
- '2': 4,
- };
-
- assert.strictEqual(element._computeBlankItems(element.permittedLabels,
- 'Code-Review').length, 0);
-
- assert.strictEqual(element._computeBlankItems(element.permittedLabels,
- 'Verified').length, 1);
- });
-
- test('labelValues returns no keys', () => {
- element.labelValues = {};
-
- assert.deepEqual(element._computeBlankItems(element.permittedLabels,
- 'Code-Review'), []);
- });
-
- test('changes in label score are reflected in the DOM', () => {
- element.labels = {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- 'Verified': {
- values: {
- ' 0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- };
- const selector = element.$.labelSelector;
- element.set('label', {name: 'Verified', value: ' 0'});
- flushAsynchronousOperations();
- assert.strictEqual(selector.selected, ' 0');
- assert.strictEqual(
- element.$.selectedValueLabel.textContent.trim(), 'No score');
- });
-
- test('without permitted labels', () => {
- element.permittedLabels = {
- Verified: [
- '-1',
- ' 0',
- '+1',
- ],
- };
- flushAsynchronousOperations();
- assert.isOk(element.$.labelSelector);
- assert.isFalse(element.$.labelSelector.hidden);
-
- element.permittedLabels = {};
- flushAsynchronousOperations();
- assert.isOk(element.$.labelSelector);
- assert.isTrue(element.$.labelSelector.hidden);
-
- element.permittedLabels = {Verified: []};
- flushAsynchronousOperations();
- assert.isOk(element.$.labelSelector);
- assert.isTrue(element.$.labelSelector.hidden);
- });
-
- test('asymetrical labels', done => {
- element.permittedLabels = {
- 'Code-Review': [
- '-2',
- '-1',
- ' 0',
- '+1',
- '+2',
- ],
- 'Verified': [
- ' 0',
- '+1',
- ],
};
flush(() => {
assert.strictEqual(element.$.labelSelector
- .items.length, 2);
+ .items.length, 5);
assert.strictEqual(
- Polymer.dom(element.root).querySelectorAll('.placeholder').length,
- 3);
-
- element.permittedLabels = {
- 'Code-Review': [
- ' 0',
- '+1',
- ],
- 'Verified': [
- '-2',
- '-1',
- ' 0',
- '+1',
- '+2',
- ],
- };
- flush(() => {
- assert.strictEqual(element.$.labelSelector
- .items.length, 5);
- assert.strictEqual(
- Polymer.dom(element.root).querySelectorAll('.placeholder').length,
- 0);
- done();
- });
+ dom(element.root).querySelectorAll('.placeholder').length,
+ 0);
+ done();
});
});
-
- test('default_value', () => {
- element.permittedLabels = {
- Verified: [
- '-1',
- ' 0',
- '+1',
- ],
- };
- element.labels = {
- Verified: {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: -1,
- },
- };
- element.label = {
- name: 'Verified',
- value: null,
- };
- flushAsynchronousOperations();
- assert.strictEqual(element.selectedValue, '-1');
- });
-
- test('default_value is null if not permitted', () => {
- element.permittedLabels = {
- Verified: [
- '-1',
- ' 0',
- '+1',
- ],
- };
- element.labels = {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: -1,
- },
- };
- element.label = {
- name: 'Code-Review',
- value: null,
- };
- flushAsynchronousOperations();
- assert.isNull(element.selectedValue);
- });
});
+
+ test('default_value', () => {
+ element.permittedLabels = {
+ Verified: [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ element.labels = {
+ Verified: {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: -1,
+ },
+ };
+ element.label = {
+ name: 'Verified',
+ value: null,
+ };
+ flushAsynchronousOperations();
+ assert.strictEqual(element.selectedValue, '-1');
+ });
+
+ test('default_value is null if not permitted', () => {
+ element.permittedLabels = {
+ Verified: [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: -1,
+ },
+ };
+ element.label = {
+ name: 'Code-Review',
+ value: null,
+ };
+ flushAsynchronousOperations();
+ assert.isNull(element.selectedValue);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
index dbfdb6a..2d6825b 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -14,135 +14,143 @@
* 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 GrLabelScores extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-label-scores'; }
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-label-score-row/gr-label-score-row.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-label-scores_html.js';
- static get properties() {
- return {
- _labels: {
- type: Array,
- computed: '_computeLabels(change.labels.*, account)',
- },
- permittedLabels: {
- type: Object,
- observer: '_computeColumns',
- },
- /** @type {?} */
- change: Object,
- /** @type {?} */
- account: Object,
+/** @extends Polymer.Element */
+class GrLabelScores extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _labelValues: Object,
- };
- }
+ static get is() { return 'gr-label-scores'; }
- getLabelValues() {
- const labels = {};
- for (const label in this.permittedLabels) {
- if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
+ static get properties() {
+ return {
+ _labels: {
+ type: Array,
+ computed: '_computeLabels(change.labels.*, account)',
+ },
+ permittedLabels: {
+ type: Object,
+ observer: '_computeColumns',
+ },
+ /** @type {?} */
+ change: Object,
+ /** @type {?} */
+ account: Object,
- const selectorEl = this.shadowRoot
- .querySelector(`gr-label-score-row[name="${label}"]`);
- if (!selectorEl) { continue; }
-
- // The user may have not voted on this label.
- if (!selectorEl.selectedItem) { continue; }
-
- const selectedVal = parseInt(selectorEl.selectedValue, 10);
-
- // Only send the selection if the user changed it.
- let prevVal = this._getVoteForAccount(this.change.labels, label,
- this.account);
- if (prevVal !== null) {
- prevVal = parseInt(prevVal, 10);
- }
- if (selectedVal !== prevVal) {
- labels[label] = selectedVal;
- }
- }
- return labels;
- }
-
- _getStringLabelValue(labels, labelName, numberValue) {
- for (const k in labels[labelName].values) {
- if (parseInt(k, 10) === numberValue) {
- return k;
- }
- }
- return numberValue;
- }
-
- _getVoteForAccount(labels, labelName, account) {
- const votes = labels[labelName];
- if (votes.all && votes.all.length > 0) {
- for (let i = 0; i < votes.all.length; i++) {
- if (votes.all[i]._account_id == account._account_id) {
- return this._getStringLabelValue(
- labels, labelName, votes.all[i].value);
- }
- }
- }
- return null;
- }
-
- _computeLabels(labelRecord, account) {
- // Polymer 2: check for undefined
- if ([labelRecord, account].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const labelsObj = labelRecord.base;
- if (!labelsObj) { return []; }
- return Object.keys(labelsObj).sort()
- .map(key => {
- return {
- name: key,
- value: this._getVoteForAccount(labelsObj, key, this.account),
- };
- });
- }
-
- _computeColumns(permittedLabels) {
- const labels = Object.keys(permittedLabels);
- const values = {};
- for (const label of labels) {
- for (const value of permittedLabels[label]) {
- values[parseInt(value, 10)] = true;
- }
- }
-
- const orderedValues = Object.keys(values).sort((a, b) => a - b);
-
- for (let i = 0; i < orderedValues.length; i++) {
- values[orderedValues[i]] = i;
- }
- this._labelValues = values;
- }
-
- _changeIsMerged(changeStatus) {
- return changeStatus === 'MERGED';
- }
-
- /**
- * @param {string|undefined} label
- * @param {Object|undefined} permittedLabels
- * @return {string}
- */
- _computeLabelAccessClass(label, permittedLabels) {
- if (label == null || permittedLabels == null) {
- return '';
- }
-
- return permittedLabels.hasOwnProperty(label) &&
- permittedLabels[label].length ? 'access' : 'no-access';
- }
+ _labelValues: Object,
+ };
}
- customElements.define(GrLabelScores.is, GrLabelScores);
-})();
+ getLabelValues() {
+ const labels = {};
+ for (const label in this.permittedLabels) {
+ if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
+
+ const selectorEl = this.shadowRoot
+ .querySelector(`gr-label-score-row[name="${label}"]`);
+ if (!selectorEl) { continue; }
+
+ // The user may have not voted on this label.
+ if (!selectorEl.selectedItem) { continue; }
+
+ const selectedVal = parseInt(selectorEl.selectedValue, 10);
+
+ // Only send the selection if the user changed it.
+ let prevVal = this._getVoteForAccount(this.change.labels, label,
+ this.account);
+ if (prevVal !== null) {
+ prevVal = parseInt(prevVal, 10);
+ }
+ if (selectedVal !== prevVal) {
+ labels[label] = selectedVal;
+ }
+ }
+ return labels;
+ }
+
+ _getStringLabelValue(labels, labelName, numberValue) {
+ for (const k in labels[labelName].values) {
+ if (parseInt(k, 10) === numberValue) {
+ return k;
+ }
+ }
+ return numberValue;
+ }
+
+ _getVoteForAccount(labels, labelName, account) {
+ const votes = labels[labelName];
+ if (votes.all && votes.all.length > 0) {
+ for (let i = 0; i < votes.all.length; i++) {
+ if (votes.all[i]._account_id == account._account_id) {
+ return this._getStringLabelValue(
+ labels, labelName, votes.all[i].value);
+ }
+ }
+ }
+ return null;
+ }
+
+ _computeLabels(labelRecord, account) {
+ // Polymer 2: check for undefined
+ if ([labelRecord, account].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const labelsObj = labelRecord.base;
+ if (!labelsObj) { return []; }
+ return Object.keys(labelsObj).sort()
+ .map(key => {
+ return {
+ name: key,
+ value: this._getVoteForAccount(labelsObj, key, this.account),
+ };
+ });
+ }
+
+ _computeColumns(permittedLabels) {
+ const labels = Object.keys(permittedLabels);
+ const values = {};
+ for (const label of labels) {
+ for (const value of permittedLabels[label]) {
+ values[parseInt(value, 10)] = true;
+ }
+ }
+
+ const orderedValues = Object.keys(values).sort((a, b) => a - b);
+
+ for (let i = 0; i < orderedValues.length; i++) {
+ values[orderedValues[i]] = i;
+ }
+ this._labelValues = values;
+ }
+
+ _changeIsMerged(changeStatus) {
+ return changeStatus === 'MERGED';
+ }
+
+ /**
+ * @param {string|undefined} label
+ * @param {Object|undefined} permittedLabels
+ * @return {string}
+ */
+ _computeLabelAccessClass(label, permittedLabels) {
+ if (label == null || permittedLabels == null) {
+ return '';
+ }
+
+ return permittedLabels.hasOwnProperty(label) &&
+ permittedLabels[label].length ? 'access' : 'no-access';
+ }
+}
+
+customElements.define(GrLabelScores.is, GrLabelScores);
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
index 8a8a9d5..b9c53c3 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
@@ -1,27 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-label-score-row/gr-label-score-row.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-label-scores">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.scoresTable {
display: table;
@@ -44,19 +39,10 @@
</style>
<div class="scoresTable">
<template is="dom-repeat" items="[[_labels]]" as="label">
- <gr-label-score-row
- class$="[[_computeLabelAccessClass(label.name, permittedLabels)]]"
- label="[[label]]"
- name="[[label.name]]"
- labels="[[change.labels]]"
- permitted-labels="[[permittedLabels]]"
- label-values="[[_labelValues]]"></gr-label-score-row>
+ <gr-label-score-row class\$="[[_computeLabelAccessClass(label.name, permittedLabels)]]" label="[[label]]" name="[[label.name]]" labels="[[change.labels]]" permitted-labels="[[permittedLabels]]" label-values="[[_labelValues]]"></gr-label-score-row>
</template>
</div>
- <div class="mergedMessage"
- hidden$="[[!_changeIsMerged(change.status)]]">
+ <div class="mergedMessage" hidden\$="[[!_changeIsMerged(change.status)]]">
Because this change has been merged, votes may not be decreased.
</div>
- </template>
- <script src="gr-label-scores.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
index 9e93110..b6075ee 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_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-label-scores</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-label-scores.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-label-scores.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-label-scores.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,166 +40,167 @@
</template>
</test-fixture>
-<script>
- suite('gr-label-scores 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-label-scores.js';
+suite('gr-label-scores tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- });
- element = fixture('basic');
- element.change = {
- _number: '123',
- labels: {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+ element = fixture('basic');
+ element.change = {
+ _number: '123',
+ labels: {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
},
- 'Verified': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
value: 1,
- all: [{
- _account_id: 123,
- value: 1,
- }],
- },
+ }],
},
- };
+ 'Verified': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ value: 1,
+ all: [{
+ _account_id: 123,
+ value: 1,
+ }],
+ },
+ },
+ };
- element.account = {
- _account_id: 123,
- };
+ element.account = {
+ _account_id: 123,
+ };
- element.permittedLabels = {
- 'Code-Review': [
- '-2',
- '-1',
- ' 0',
- '+1',
- '+2',
- ],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
- flush(done);
- });
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-2',
+ '-1',
+ ' 0',
+ '+1',
+ '+2',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ flush(done);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('get and set label scores', () => {
- for (const label in element.permittedLabels) {
- if (element.permittedLabels.hasOwnProperty(label)) {
- const row = element.shadowRoot
- .querySelector('gr-label-score-row[name="' + label + '"]');
- row.setSelectedValue(-1);
- }
+ test('get and set label scores', () => {
+ for (const label in element.permittedLabels) {
+ if (element.permittedLabels.hasOwnProperty(label)) {
+ const row = element.shadowRoot
+ .querySelector('gr-label-score-row[name="' + label + '"]');
+ row.setSelectedValue(-1);
}
- assert.deepEqual(element.getLabelValues(), {
- 'Code-Review': -1,
- 'Verified': -1,
- });
- });
-
- test('_getVoteForAccount', () => {
- const labelName = 'Code-Review';
- assert.strictEqual(element._getVoteForAccount(
- element.change.labels, labelName, element.account),
- '+1');
- });
-
- test('_computeColumns', () => {
- element._computeColumns(element.permittedLabels);
- assert.deepEqual(element._labelValues, {
- '-2': 0,
- '-1': 1,
- '0': 2,
- '1': 3,
- '2': 4,
- });
- });
-
- test('_computeLabelAccessClass undefined case', () => {
- assert.strictEqual(
- element._computeLabelAccessClass(undefined, undefined), '');
- assert.strictEqual(
- element._computeLabelAccessClass('', undefined), '');
- assert.strictEqual(
- element._computeLabelAccessClass(undefined, {}), '');
- });
-
- test('_computeLabelAccessClass has access', () => {
- assert.strictEqual(
- element._computeLabelAccessClass('foo', {foo: ['']}), 'access');
- });
-
- test('_computeLabelAccessClass no access', () => {
- assert.strictEqual(
- element._computeLabelAccessClass('zap', {foo: ['']}), 'no-access');
- });
-
- test('changes in label score are reflected in _labels', () => {
- element.change = {
- _number: '123',
- labels: {
- 'Code-Review': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- 'Verified': {
- values: {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- },
- default_value: 0,
- },
- },
- };
- assert.deepEqual(element._labels [
- {name: 'Code-Review', value: null},
- {name: 'Verified', value: null}
- ]);
- element.set(['change', 'labels', 'Verified', 'all'],
- [{_account_id: 123, value: 1}]);
- assert.deepEqual(element._labels, [
- {name: 'Code-Review', value: null},
- {name: 'Verified', value: '+1'},
- ]);
+ }
+ assert.deepEqual(element.getLabelValues(), {
+ 'Code-Review': -1,
+ 'Verified': -1,
});
});
+
+ test('_getVoteForAccount', () => {
+ const labelName = 'Code-Review';
+ assert.strictEqual(element._getVoteForAccount(
+ element.change.labels, labelName, element.account),
+ '+1');
+ });
+
+ test('_computeColumns', () => {
+ element._computeColumns(element.permittedLabels);
+ assert.deepEqual(element._labelValues, {
+ '-2': 0,
+ '-1': 1,
+ '0': 2,
+ '1': 3,
+ '2': 4,
+ });
+ });
+
+ test('_computeLabelAccessClass undefined case', () => {
+ assert.strictEqual(
+ element._computeLabelAccessClass(undefined, undefined), '');
+ assert.strictEqual(
+ element._computeLabelAccessClass('', undefined), '');
+ assert.strictEqual(
+ element._computeLabelAccessClass(undefined, {}), '');
+ });
+
+ test('_computeLabelAccessClass has access', () => {
+ assert.strictEqual(
+ element._computeLabelAccessClass('foo', {foo: ['']}), 'access');
+ });
+
+ test('_computeLabelAccessClass no access', () => {
+ assert.strictEqual(
+ element._computeLabelAccessClass('zap', {foo: ['']}), 'no-access');
+ });
+
+ test('changes in label score are reflected in _labels', () => {
+ element.change = {
+ _number: '123',
+ labels: {
+ 'Code-Review': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ 'Verified': {
+ values: {
+ '0': 'No score',
+ '+1': 'good',
+ '+2': 'excellent',
+ '-1': 'bad',
+ '-2': 'terrible',
+ },
+ default_value: 0,
+ },
+ },
+ };
+ assert.deepEqual(element._labels [
+ ({name: 'Code-Review', value: null}, {name: 'Verified', value: null})
+ ]);
+ element.set(['change', 'labels', 'Verified', 'all'],
+ [{_account_id: 123, value: 1}]);
+ assert.deepEqual(element._labels, [
+ {name: 'Code-Review', value: null},
+ {name: 'Verified', value: '+1'},
+ ]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 13a4213..e5cbaf1 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.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,379 +14,396 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
- const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-account-label/gr-account-label.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-formatted-text/gr-formatted-text.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-voting-styles.js';
+import '../gr-comment-list/gr-comment-list.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-message_html.js';
+
+const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
+const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrMessage extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-message'; }
+ /**
+ * Fired when this message's reply link is tapped.
+ *
+ * @event reply
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the message's timestamp is tapped.
+ *
+ * @event message-anchor-tap
*/
- class GrMessage extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-message'; }
- /**
- * Fired when this message's reply link is tapped.
- *
- * @event reply
- */
- /**
- * Fired when the message's timestamp is tapped.
- *
- * @event message-anchor-tap
- */
+ /**
+ * Fired when a change message is deleted.
+ *
+ * @event change-message-deleted
+ */
- /**
- * Fired when a change message is deleted.
- *
- * @event change-message-deleted
- */
+ static get properties() {
+ return {
+ changeNum: Number,
+ /** @type {?} */
+ message: Object,
+ author: {
+ type: Object,
+ computed: '_computeAuthor(message)',
+ },
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ config: Object,
+ hideAutomated: {
+ type: Boolean,
+ value: false,
+ },
+ hidden: {
+ type: Boolean,
+ computed: '_computeIsHidden(hideAutomated, isAutomated)',
+ reflectToAttribute: true,
+ },
+ isAutomated: {
+ type: Boolean,
+ computed: '_computeIsAutomated(message)',
+ },
+ showOnBehalfOf: {
+ type: Boolean,
+ computed: '_computeShowOnBehalfOf(message)',
+ },
+ showReplyButton: {
+ type: Boolean,
+ computed: '_computeShowReplyButton(message, _loggedIn)',
+ },
+ projectName: {
+ type: String,
+ observer: '_projectNameChanged',
+ },
- static get properties() {
- return {
- changeNum: Number,
- /** @type {?} */
- message: Object,
- author: {
- type: Object,
- computed: '_computeAuthor(message)',
- },
- comments: {
- type: Object,
- observer: '_commentsChanged',
- },
- config: Object,
- hideAutomated: {
- type: Boolean,
- value: false,
- },
- hidden: {
- type: Boolean,
- computed: '_computeIsHidden(hideAutomated, isAutomated)',
- reflectToAttribute: true,
- },
- isAutomated: {
- type: Boolean,
- computed: '_computeIsAutomated(message)',
- },
- showOnBehalfOf: {
- type: Boolean,
- computed: '_computeShowOnBehalfOf(message)',
- },
- showReplyButton: {
- type: Boolean,
- computed: '_computeShowReplyButton(message, _loggedIn)',
- },
- projectName: {
- type: String,
- observer: '_projectNameChanged',
- },
+ /**
+ * A mapping from label names to objects representing the minimum and
+ * maximum possible values for that label.
+ */
+ labelExtremes: Object,
- /**
- * A mapping from label names to objects representing the minimum and
- * maximum possible values for that label.
- */
- labelExtremes: Object,
+ /**
+ * @type {{ commentlinks: Array }}
+ */
+ _projectConfig: Object,
+ // Computed property needed to trigger Polymer value observing.
+ _expanded: {
+ type: Object,
+ computed: '_computeExpanded(message.expanded)',
+ },
+ _messageContentExpanded: {
+ type: String,
+ computed:
+ '_computeMessageContentExpanded(message.message, message.tag)',
+ },
+ _messageContentCollapsed: {
+ type: String,
+ computed:
+ '_computeMessageContentCollapsed(message.message, message.tag)',
+ },
+ _commentCountText: {
+ type: Number,
+ computed: '_computeCommentCountText(comments)',
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ _isDeletingChangeMsg: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /**
- * @type {{ commentlinks: Array }}
- */
- _projectConfig: Object,
- // Computed property needed to trigger Polymer value observing.
- _expanded: {
- type: Object,
- computed: '_computeExpanded(message.expanded)',
- },
- _messageContentExpanded: {
- type: String,
- computed:
- '_computeMessageContentExpanded(message.message, message.tag)',
- },
- _messageContentCollapsed: {
- type: String,
- computed:
- '_computeMessageContentCollapsed(message.message, message.tag)',
- },
- _commentCountText: {
- type: Number,
- computed: '_computeCommentCountText(comments)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _isAdmin: {
- type: Boolean,
- value: false,
- },
- _isDeletingChangeMsg: {
- type: Boolean,
- value: false,
- },
- };
- }
+ static get observers() {
+ return [
+ '_updateExpandedClass(message.expanded)',
+ ];
+ }
- static get observers() {
- return [
- '_updateExpandedClass(message.expanded)',
- ];
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('click',
+ e => this._handleClick(e));
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('click',
- e => this._handleClick(e));
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.restAPI.getConfig().then(config => {
+ this.config = config;
+ });
+ this.$.restAPI.getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ this.$.restAPI.getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin;
+ });
+ }
- /** @override */
- ready() {
- super.ready();
- this.$.restAPI.getConfig().then(config => {
- this.config = config;
- });
- this.$.restAPI.getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
- this.$.restAPI.getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- });
- }
-
- _updateExpandedClass(expanded) {
- if (expanded) {
- this.classList.add('expanded');
- } else {
- this.classList.remove('expanded');
- }
- }
-
- _computeCommentCountText(comments) {
- if (!comments) return undefined;
- let count = 0;
- for (const file in comments) {
- if (comments.hasOwnProperty(file)) {
- const commentArray = comments[file] || [];
- count += commentArray.length;
- }
- }
- if (count === 0) {
- return undefined;
- } else if (count === 1) {
- return '1 comment';
- } else {
- return `${count} comments`;
- }
- }
-
- _computeMessageContentExpanded(content, tag) {
- return this._computeMessageContent(content, tag, true);
- }
-
- _computeMessageContentCollapsed(content, tag) {
- return this._computeMessageContent(content, tag, false);
- }
-
- _computeMessageContent(content, tag, isExpanded) {
- content = content || '';
- tag = tag || '';
- const isNewPatchSet = tag.endsWith(':newPatchSet') ||
- tag.endsWith(':newWipPatchSet');
- const lines = content.split('\n');
- const filteredLines = lines.filter(line => {
- if (!isExpanded && line.startsWith('>')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comment)')) {
- return false;
- }
- if (line.startsWith('(') && line.endsWith(' comments)')) {
- return false;
- }
- if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
- return false;
- }
- return true;
- });
- const mappedLines = filteredLines.map(line => {
- // The change message formatting is not very consistent, so
- // unfortunately we have to do a bit of tweaking here:
- // Labels should be stripped from lines like this:
- // Patch Set 29: Verified+1
- // Rebase messages (which have a ':newPatchSet' tag) should be kept on
- // lines like this:
- // Patch Set 27: Patch Set 26 was rebased
- if (isNewPatchSet) {
- line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
- }
- return line;
- });
- return mappedLines.join('\n').trim();
- }
-
- _isMessageContentEmpty() {
- return !this._messageContentExpanded
- || this._messageContentExpanded.length === 0;
- }
-
- _computeAuthor(message) {
- return message.author || message.updated_by;
- }
-
- _computeShowOnBehalfOf(message) {
- const author = message.author || message.updated_by;
- return !!(author && message.real_author &&
- author._account_id != message.real_author._account_id);
- }
-
- _computeShowReplyButton(message, loggedIn) {
- return message && !!message.message && loggedIn &&
- !this._computeIsAutomated(message);
- }
-
- _computeExpanded(expanded) {
- return expanded;
- }
-
- /**
- * If there is no value set on the message object as to whether _expanded
- * should be true or not, then _expanded is set to true if there are
- * inline comments (otherwise false).
- */
- _commentsChanged(value) {
- if (this.message && this.message.expanded === undefined) {
- this.set('message.expanded', Object.keys(value || {}).length > 0);
- }
- }
-
- _handleClick(e) {
- if (this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', true);
- }
-
- _handleAuthorClick(e) {
- if (!this.message.expanded) { return; }
- e.stopPropagation();
- this.set('message.expanded', false);
- }
-
- _computeIsAutomated(message) {
- return !!(message.reviewer ||
- this._computeIsReviewerUpdate(message) ||
- (message.tag && message.tag.startsWith('autogenerated')));
- }
-
- _computeIsHidden(hideAutomated, isAutomated) {
- return hideAutomated && isAutomated;
- }
-
- _computeIsReviewerUpdate(event) {
- return event.type === 'REVIEWER_UPDATE';
- }
-
- _getScores(message, labelExtremes) {
- if (!message || !message.message || !labelExtremes) {
- return [];
- }
- const line = message.message.split('\n', 1)[0];
- const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
- if (!line.match(patchSetPrefix)) {
- return [];
- }
- const scoresRaw = line.split(patchSetPrefix)[1];
- if (!scoresRaw) {
- return [];
- }
- return scoresRaw.split(' ')
- .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
- .filter(ms =>
- ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2]))
- .map(ms => {
- const label = ms[2];
- const value = ms[1] === '-' ? 'removed' : ms[3];
- return {label, value};
- });
- }
-
- _computeScoreClass(score, labelExtremes) {
- // Polymer 2: check for undefined
- if ([score, labelExtremes].some(arg => arg === undefined)) {
- return '';
- }
- if (score.value === 'removed') {
- return 'removed';
- }
- const classes = [];
- if (score.value > 0) {
- classes.push('positive');
- } else if (score.value < 0) {
- classes.push('negative');
- }
- const extremes = labelExtremes[score.label];
- if (extremes) {
- const intScore = parseInt(score.value, 10);
- if (intScore === extremes.max) {
- classes.push('max');
- } else if (intScore === extremes.min) {
- classes.push('min');
- }
- }
- return classes.join(' ');
- }
-
- _computeClass(expanded) {
- const classes = [];
- classes.push(expanded ? 'expanded' : 'collapsed');
- return classes.join(' ');
- }
-
- _handleAnchorClick(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('message-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {id: this.message.id},
- }));
- }
-
- _handleReplyTap(e) {
- e.preventDefault();
- this.fire('reply', {message: this.message});
- }
-
- _handleDeleteMessage(e) {
- e.preventDefault();
- if (!this.message || !this.message.id) return;
- this._isDeletingChangeMsg = true;
- this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id)
- .then(() => {
- this._isDeletingChangeMsg = false;
- this.fire('change-message-deleted', {message: this.message});
- });
- }
-
- _projectNameChanged(name) {
- this.$.restAPI.getProjectConfig(name).then(config => {
- this._projectConfig = config;
- });
- }
-
- _computeExpandToggleIcon(expanded) {
- return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
- }
-
- _toggleExpanded(e) {
- e.stopPropagation();
- this.set('message.expanded', !this.message.expanded);
+ _updateExpandedClass(expanded) {
+ if (expanded) {
+ this.classList.add('expanded');
+ } else {
+ this.classList.remove('expanded');
}
}
- customElements.define(GrMessage.is, GrMessage);
-})();
+ _computeCommentCountText(comments) {
+ if (!comments) return undefined;
+ let count = 0;
+ for (const file in comments) {
+ if (comments.hasOwnProperty(file)) {
+ const commentArray = comments[file] || [];
+ count += commentArray.length;
+ }
+ }
+ if (count === 0) {
+ return undefined;
+ } else if (count === 1) {
+ return '1 comment';
+ } else {
+ return `${count} comments`;
+ }
+ }
+
+ _computeMessageContentExpanded(content, tag) {
+ return this._computeMessageContent(content, tag, true);
+ }
+
+ _computeMessageContentCollapsed(content, tag) {
+ return this._computeMessageContent(content, tag, false);
+ }
+
+ _computeMessageContent(content, tag, isExpanded) {
+ content = content || '';
+ tag = tag || '';
+ const isNewPatchSet = tag.endsWith(':newPatchSet') ||
+ tag.endsWith(':newWipPatchSet');
+ const lines = content.split('\n');
+ const filteredLines = lines.filter(line => {
+ if (!isExpanded && line.startsWith('>')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comment)')) {
+ return false;
+ }
+ if (line.startsWith('(') && line.endsWith(' comments)')) {
+ return false;
+ }
+ if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) {
+ return false;
+ }
+ return true;
+ });
+ const mappedLines = filteredLines.map(line => {
+ // The change message formatting is not very consistent, so
+ // unfortunately we have to do a bit of tweaking here:
+ // Labels should be stripped from lines like this:
+ // Patch Set 29: Verified+1
+ // Rebase messages (which have a ':newPatchSet' tag) should be kept on
+ // lines like this:
+ // Patch Set 27: Patch Set 26 was rebased
+ if (isNewPatchSet) {
+ line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1');
+ }
+ return line;
+ });
+ return mappedLines.join('\n').trim();
+ }
+
+ _isMessageContentEmpty() {
+ return !this._messageContentExpanded
+ || this._messageContentExpanded.length === 0;
+ }
+
+ _computeAuthor(message) {
+ return message.author || message.updated_by;
+ }
+
+ _computeShowOnBehalfOf(message) {
+ const author = message.author || message.updated_by;
+ return !!(author && message.real_author &&
+ author._account_id != message.real_author._account_id);
+ }
+
+ _computeShowReplyButton(message, loggedIn) {
+ return message && !!message.message && loggedIn &&
+ !this._computeIsAutomated(message);
+ }
+
+ _computeExpanded(expanded) {
+ return expanded;
+ }
+
+ /**
+ * If there is no value set on the message object as to whether _expanded
+ * should be true or not, then _expanded is set to true if there are
+ * inline comments (otherwise false).
+ */
+ _commentsChanged(value) {
+ if (this.message && this.message.expanded === undefined) {
+ this.set('message.expanded', Object.keys(value || {}).length > 0);
+ }
+ }
+
+ _handleClick(e) {
+ if (this.message.expanded) { return; }
+ e.stopPropagation();
+ this.set('message.expanded', true);
+ }
+
+ _handleAuthorClick(e) {
+ if (!this.message.expanded) { return; }
+ e.stopPropagation();
+ this.set('message.expanded', false);
+ }
+
+ _computeIsAutomated(message) {
+ return !!(message.reviewer ||
+ this._computeIsReviewerUpdate(message) ||
+ (message.tag && message.tag.startsWith('autogenerated')));
+ }
+
+ _computeIsHidden(hideAutomated, isAutomated) {
+ return hideAutomated && isAutomated;
+ }
+
+ _computeIsReviewerUpdate(event) {
+ return event.type === 'REVIEWER_UPDATE';
+ }
+
+ _getScores(message, labelExtremes) {
+ if (!message || !message.message || !labelExtremes) {
+ return [];
+ }
+ const line = message.message.split('\n', 1)[0];
+ const patchSetPrefix = PATCH_SET_PREFIX_PATTERN;
+ if (!line.match(patchSetPrefix)) {
+ return [];
+ }
+ const scoresRaw = line.split(patchSetPrefix)[1];
+ if (!scoresRaw) {
+ return [];
+ }
+ return scoresRaw.split(' ')
+ .map(s => s.match(LABEL_TITLE_SCORE_PATTERN))
+ .filter(ms =>
+ ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2]))
+ .map(ms => {
+ const label = ms[2];
+ const value = ms[1] === '-' ? 'removed' : ms[3];
+ return {label, value};
+ });
+ }
+
+ _computeScoreClass(score, labelExtremes) {
+ // Polymer 2: check for undefined
+ if ([score, labelExtremes].some(arg => arg === undefined)) {
+ return '';
+ }
+ if (score.value === 'removed') {
+ return 'removed';
+ }
+ const classes = [];
+ if (score.value > 0) {
+ classes.push('positive');
+ } else if (score.value < 0) {
+ classes.push('negative');
+ }
+ const extremes = labelExtremes[score.label];
+ if (extremes) {
+ const intScore = parseInt(score.value, 10);
+ if (intScore === extremes.max) {
+ classes.push('max');
+ } else if (intScore === extremes.min) {
+ classes.push('min');
+ }
+ }
+ return classes.join(' ');
+ }
+
+ _computeClass(expanded) {
+ const classes = [];
+ classes.push(expanded ? 'expanded' : 'collapsed');
+ return classes.join(' ');
+ }
+
+ _handleAnchorClick(e) {
+ e.preventDefault();
+ this.dispatchEvent(new CustomEvent('message-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {id: this.message.id},
+ }));
+ }
+
+ _handleReplyTap(e) {
+ e.preventDefault();
+ this.fire('reply', {message: this.message});
+ }
+
+ _handleDeleteMessage(e) {
+ e.preventDefault();
+ if (!this.message || !this.message.id) return;
+ this._isDeletingChangeMsg = true;
+ this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id)
+ .then(() => {
+ this._isDeletingChangeMsg = false;
+ this.fire('change-message-deleted', {message: this.message});
+ });
+ }
+
+ _projectNameChanged(name) {
+ this.$.restAPI.getProjectConfig(name).then(config => {
+ this._projectConfig = config;
+ });
+ }
+
+ _computeExpandToggleIcon(expanded) {
+ return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ }
+
+ _toggleExpanded(e) {
+ e.stopPropagation();
+ this.set('message.expanded', !this.message.expanded);
+ }
+}
+
+customElements.define(GrMessage.is, GrMessage);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
index f7ef7e6..49ced2a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -1,36 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-account-label/gr-account-label.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.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="../../../styles/gr-voting-styles.html">
-
-<link rel="import" href="../gr-comment-list/gr-comment-list.html">
-
-<dom-module id="gr-message">
- <template>
+export const htmlTemplate = html`
<style include="gr-voting-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -193,16 +179,16 @@
}
}
</style>
- <div class$="[[_computeClass(_expanded)]]">
+ <div class\$="[[_computeClass(_expanded)]]">
<div class="contentContainer">
<div class="author" on-click="_handleAuthorClick">
- <span hidden$="[[!showOnBehalfOf]]">
+ <span hidden\$="[[!showOnBehalfOf]]">
<span class="name">[[message.real_author.name]]</span>
on behalf of
</span>
<gr-account-label account="[[author]]" class="authorLabel"></gr-account-label>
<template is="dom-repeat" items="[[_getScores(message, labelExtremes)]]" as="score">
- <span class$="score [[_computeScoreClass(score, labelExtremes)]]">
+ <span class\$="score [[_computeScoreClass(score, labelExtremes)]]">
[[score.label]] [[score.value]]
</span>
</template>
@@ -216,32 +202,18 @@
<template is="dom-if" if="[[message.message]]">
<div class="content">
<div class="message hideOnOpen">[[_messageContentCollapsed]]</div>
- <gr-formatted-text
- no-trailing-margin
- class="message hideOnCollapsed"
- content="[[_messageContentExpanded]]"
- config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
+ <gr-formatted-text no-trailing-margin="" class="message hideOnCollapsed" content="[[_messageContentExpanded]]" config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
<template is="dom-if" if="[[!_isMessageContentEmpty()]]">
- <div class="replyActionContainer" hidden$="[[!showReplyButton]]" hidden>
- <gr-button
- class="replyBtn"
- link small on-click="_handleReplyTap">
+ <div class="replyActionContainer" hidden\$="[[!showReplyButton]]" hidden="">
+ <gr-button class="replyBtn" link="" small="" on-click="_handleReplyTap">
Reply
</gr-button>
- <gr-button
- disabled$=[[_isDeletingChangeMsg]]
- class="deleteBtn" hidden$="[[!_isAdmin]]" hidden
- link small on-click="_handleDeleteMessage">
+ <gr-button disabled\$="[[_isDeletingChangeMsg]]" class="deleteBtn" hidden\$="[[!_isAdmin]]" hidden="" link="" small="" on-click="_handleDeleteMessage">
Delete
</gr-button>
</div>
</template>
- <gr-comment-list
- comments="[[comments]]"
- change-num="[[changeNum]]"
- patch-num="[[message._revision_number]]"
- project-name="[[projectName]]"
- project-config="[[_projectConfig]]"></gr-comment-list>
+ <gr-comment-list comments="[[comments]]" change-num="[[changeNum]]" patch-num="[[message._revision_number]]" project-name="[[projectName]]" project-config="[[_projectConfig]]"></gr-comment-list>
</div>
</template>
<template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
@@ -249,8 +221,7 @@
<template is="dom-repeat" items="[[message.updates]]" as="update">
<div class="updateCategory">
[[update.message]]
- <template
- is="dom-repeat" items="[[update.reviewers]]" as="reviewer">
+ <template is="dom-repeat" items="[[update.reviewers]]" as="reviewer">
<gr-account-chip account="[[reviewer]]">
</gr-account-chip>
</template>
@@ -264,29 +235,17 @@
</template>
<template is="dom-if" if="[[!message.id]]">
<span class="date">
- <gr-date-formatter
- has-tooltip
- show-date-and-time
- date-str="[[message.date]]"></gr-date-formatter>
+ <gr-date-formatter has-tooltip="" show-date-and-time="" date-str="[[message.date]]"></gr-date-formatter>
</span>
</template>
<template is="dom-if" if="[[message.id]]">
<span class="date" on-click="_handleAnchorClick">
- <gr-date-formatter
- has-tooltip
- show-date-and-time
- date-str="[[message.date]]"></gr-date-formatter>
+ <gr-date-formatter has-tooltip="" show-date-and-time="" date-str="[[message.date]]"></gr-date-formatter>
</span>
</template>
- <iron-icon
- id="expandToggle"
- on-click="_toggleExpanded"
- title="Toggle expanded state"
- icon="[[_computeExpandToggleIcon(_expanded)]]"></iron-icon>
+ <iron-icon id="expandToggle" on-click="_toggleExpanded" title="Toggle expanded state" icon="[[_computeExpandToggleIcon(_expanded)]]"></iron-icon>
</span>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-message.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index f22f17e..04f12c2f 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_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-message</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-message.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-message.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-message.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,386 +40,389 @@
</template>
</test-fixture>
-<script>
- suite('gr-message tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-message.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-message tests', () => {
+ let element;
- suite('when admin and logged in', () => {
- setup(done => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getPreferences() { return Promise.resolve({}); },
- getConfig() { return Promise.resolve({}); },
- getIsAdmin() { return Promise.resolve(true); },
- deleteChangeCommitMessage() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- flush(done);
+ suite('when admin and logged in', () => {
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getPreferences() { return Promise.resolve({}); },
+ getConfig() { return Promise.resolve({}); },
+ getIsAdmin() { return Promise.resolve(true); },
+ deleteChangeCommitMessage() { return Promise.resolve({}); },
});
+ element = fixture('basic');
+ flush(done);
+ });
- test('reply event', done => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('reply event', done => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
- element.addEventListener('reply', e => {
- assert.deepEqual(e.detail.message, element.message);
- done();
- });
- flushAsynchronousOperations();
- assert.isFalse(
- element.shadowRoot.querySelector('.replyActionContainer').hidden
- );
- MockInteractions.tap(element.shadowRoot.querySelector('.replyBtn'));
+ element.addEventListener('reply', e => {
+ assert.deepEqual(e.detail.message, element.message);
+ done();
});
+ flushAsynchronousOperations();
+ assert.isFalse(
+ element.shadowRoot.querySelector('.replyActionContainer').hidden
+ );
+ MockInteractions.tap(element.shadowRoot.querySelector('.replyBtn'));
+ });
- test('can see delete button', () => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('can see delete button', () => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
- flushAsynchronousOperations();
- assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').hidden);
+ flushAsynchronousOperations();
+ assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').hidden);
+ });
+
+ test('delete change message', done => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
+
+ element.addEventListener('change-message-deleted', e => {
+ assert.deepEqual(e.detail.message, element.message);
+ assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').disabled);
+ done();
});
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot.querySelector('.deleteBtn'));
+ assert.isTrue(element.shadowRoot.querySelector('.deleteBtn').disabled);
+ });
- test('delete change message', done => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('autogenerated prefix hiding', () => {
+ element.message = {
+ tag: 'autogenerated:gerrit:test',
+ updated: '2016-01-12 20:24:49.448000000',
+ };
- element.addEventListener('change-message-deleted', e => {
- assert.deepEqual(e.detail.message, element.message);
- assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').disabled);
- done();
- });
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot.querySelector('.deleteBtn'));
- assert.isTrue(element.shadowRoot.querySelector('.deleteBtn').disabled);
- });
+ assert.isTrue(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('autogenerated prefix hiding', () => {
- element.message = {
- tag: 'autogenerated:gerrit:test',
- updated: '2016-01-12 20:24:49.448000000',
- };
+ element.hideAutomated = true;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.hidden);
+ });
- element.hideAutomated = true;
+ test('reviewer message treated as autogenerated', () => {
+ element.message = {
+ tag: 'autogenerated:gerrit:test',
+ updated: '2016-01-12 20:24:49.448000000',
+ reviewer: {},
+ };
- assert.isTrue(element.hidden);
- });
+ assert.isTrue(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('reviewer message treated as autogenerated', () => {
- element.message = {
- tag: 'autogenerated:gerrit:test',
- updated: '2016-01-12 20:24:49.448000000',
- reviewer: {},
- };
+ element.hideAutomated = true;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.hidden);
+ });
- element.hideAutomated = true;
+ test('batch reviewer message treated as autogenerated', () => {
+ element.message = {
+ type: 'REVIEWER_UPDATE',
+ updated: '2016-01-12 20:24:49.448000000',
+ reviewer: {},
+ };
- assert.isTrue(element.hidden);
- });
+ assert.isTrue(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('batch reviewer message treated as autogenerated', () => {
- element.message = {
- type: 'REVIEWER_UPDATE',
- updated: '2016-01-12 20:24:49.448000000',
- reviewer: {},
- };
+ element.hideAutomated = true;
- assert.isTrue(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isTrue(element.hidden);
+ });
- element.hideAutomated = true;
+ test('tag that is not autogenerated prefix does not hide', () => {
+ element.message = {
+ tag: 'something',
+ updated: '2016-01-12 20:24:49.448000000',
+ };
- assert.isTrue(element.hidden);
- });
+ assert.isFalse(element.isAutomated);
+ assert.isFalse(element.hidden);
- test('tag that is not autogenerated prefix does not hide', () => {
- element.message = {
- tag: 'something',
- updated: '2016-01-12 20:24:49.448000000',
- };
+ element.hideAutomated = true;
- assert.isFalse(element.isAutomated);
- assert.isFalse(element.hidden);
+ assert.isFalse(element.hidden);
+ });
- element.hideAutomated = true;
+ test('reply button hidden unless logged in', () => {
+ const message = {
+ message: 'Uploaded patch set 1.',
+ };
+ assert.isFalse(element._computeShowReplyButton(message, false));
+ assert.isTrue(element._computeShowReplyButton(message, true));
+ });
- assert.isFalse(element.hidden);
- });
+ test('_computeShowOnBehalfOf', () => {
+ const message = {
+ message: '...',
+ };
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.author = {_account_id: 1115495};
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.real_author = {_account_id: 1115495};
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.real_author._account_id = 123456;
+ assert.isOk(element._computeShowOnBehalfOf(message));
+ message.updated_by = message.author;
+ delete message.author;
+ assert.isOk(element._computeShowOnBehalfOf(message));
+ delete message.updated_by;
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ });
- test('reply button hidden unless logged in', () => {
- const message = {
- message: 'Uploaded patch set 1.',
- };
- assert.isFalse(element._computeShowReplyButton(message, false));
- assert.isTrue(element._computeShowReplyButton(message, true));
- });
-
- test('_computeShowOnBehalfOf', () => {
- const message = {
- message: '...',
- };
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- message.author = {_account_id: 1115495};
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- message.real_author = {_account_id: 1115495};
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- message.real_author._account_id = 123456;
- assert.isOk(element._computeShowOnBehalfOf(message));
- message.updated_by = message.author;
- delete message.author;
- assert.isOk(element._computeShowOnBehalfOf(message));
- delete message.updated_by;
- assert.isNotOk(element._computeShowOnBehalfOf(message));
- });
-
- ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
- test(`${label} ignored for color voting`, () => {
- element.message = {
- author: {},
- expanded: false,
- message: `Patch Set 1: ${label}+1`,
- };
- assert.isNotOk(
- Polymer.dom(element.root).querySelector('.negativeVote'));
- assert.isNotOk(
- Polymer.dom(element.root).querySelector('.positiveVote'));
- });
- });
-
- test('clicking on date link fires event', () => {
- element.message = {
- type: 'REVIEWER_UPDATE',
- updated: '2016-01-12 20:24:49.448000000',
- reviewer: {},
- id: '47c43261_55aa2c41',
- };
- flushAsynchronousOperations();
- const stub = sinon.stub();
- element.addEventListener('message-anchor-tap', stub);
- const dateEl = element.shadowRoot
- .querySelector('.date');
- assert.ok(dateEl);
- MockInteractions.tap(dateEl);
-
- assert.isTrue(stub.called);
- assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
- });
-
- suite('compute messages', () => {
- test('empty', () => {
- assert.equal(element._computeMessageContent('', '', true), '');
- assert.equal(element._computeMessageContent('', '', false), '');
- });
-
- test('new patchset', () => {
- const original = 'Uploaded patch set 1.';
- const tag = 'autogenerated:gerrit:newPatchSet';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, original);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, original);
- });
-
- test('new patchset rebased', () => {
- const original = 'Patch Set 27: Patch Set 26 was rebased';
- const tag = 'autogenerated:gerrit:newPatchSet';
- const expected = 'Patch Set 26 was rebased';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
-
- test('ready for review', () => {
- const original = 'Patch Set 1:\n\nThis change is ready for review.';
- const tag = undefined;
- const expected = 'This change is ready for review.';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
-
- test('vote', () => {
- const original = 'Patch Set 1: Code-Style+1';
- const tag = undefined;
- const expected = '';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
-
- test('comments', () => {
- const original = 'Patch Set 1:\n\n(3 comments)';
- const tag = undefined;
- const expected = '';
- let actual = element._computeMessageContent(original, tag, true);
- assert.equal(actual, expected);
- actual = element._computeMessageContent(original, tag, false);
- assert.equal(actual, expected);
- });
- });
-
- test('votes', () => {
+ ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
+ test(`${label} ignored for color voting`, () => {
element.message = {
author: {},
expanded: false,
- message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
+ message: `Patch Set 1: ${label}+1`,
};
- element.labelExtremes = {
- 'Verified': {max: 1, min: -1},
- 'Code-Review': {max: 2, min: -2},
- 'Trybot-Label3': {max: 3, min: 0},
- };
- flushAsynchronousOperations();
- const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
- assert.equal(scoreChips.length, 3);
-
- assert.isTrue(scoreChips[0].classList.contains('positive'));
- assert.isTrue(scoreChips[0].classList.contains('max'));
-
- assert.isTrue(scoreChips[1].classList.contains('negative'));
- assert.isTrue(scoreChips[1].classList.contains('min'));
-
- assert.isTrue(scoreChips[2].classList.contains('positive'));
- assert.isFalse(scoreChips[2].classList.contains('min'));
- });
-
- test('removed votes', () => {
- element.message = {
- author: {},
- expanded: false,
- message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
- };
- element.labelExtremes = {
- 'Verified': {max: 1, min: -1},
- 'Code-Review': {max: 2, min: -2},
- 'Commit-Queue': {max: 3, min: 0},
- };
- flushAsynchronousOperations();
- const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
- assert.equal(scoreChips.length, 3);
-
- assert.isTrue(scoreChips[1].classList.contains('removed'));
- assert.isTrue(scoreChips[2].classList.contains('removed'));
- });
-
- test('false negative vote', () => {
- element.message = {
- author: {},
- expanded: false,
- message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
- };
- element.labelExtremes = {};
- const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
- assert.equal(scoreChips.length, 0);
+ assert.isNotOk(
+ dom(element.root).querySelector('.negativeVote'));
+ assert.isNotOk(
+ dom(element.root).querySelector('.positiveVote'));
});
});
- suite('when not logged in', () => {
- setup(done => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getPreferences() { return Promise.resolve({}); },
- getConfig() { return Promise.resolve({}); },
- getIsAdmin() { return Promise.resolve(false); },
- deleteChangeCommitMessage() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- flush(done);
+ test('clicking on date link fires event', () => {
+ element.message = {
+ type: 'REVIEWER_UPDATE',
+ updated: '2016-01-12 20:24:49.448000000',
+ reviewer: {},
+ id: '47c43261_55aa2c41',
+ };
+ flushAsynchronousOperations();
+ const stub = sinon.stub();
+ element.addEventListener('message-anchor-tap', stub);
+ const dateEl = element.shadowRoot
+ .querySelector('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+
+ assert.isTrue(stub.called);
+ assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
+ });
+
+ suite('compute messages', () => {
+ test('empty', () => {
+ assert.equal(element._computeMessageContent('', '', true), '');
+ assert.equal(element._computeMessageContent('', '', false), '');
});
- test('reply and delete button should be hidden', () => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ test('new patchset', () => {
+ const original = 'Uploaded patch set 1.';
+ const tag = 'autogenerated:gerrit:newPatchSet';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, original);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, original);
+ });
- flushAsynchronousOperations();
- assert.isTrue(
- element.shadowRoot.querySelector('.replyActionContainer').hidden
- );
- assert.isTrue(
- element.shadowRoot.querySelector('.deleteBtn').hidden
- );
+ test('new patchset rebased', () => {
+ const original = 'Patch Set 27: Patch Set 26 was rebased';
+ const tag = 'autogenerated:gerrit:newPatchSet';
+ const expected = 'Patch Set 26 was rebased';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
+ });
+
+ test('ready for review', () => {
+ const original = 'Patch Set 1:\n\nThis change is ready for review.';
+ const tag = undefined;
+ const expected = 'This change is ready for review.';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
+ });
+
+ test('vote', () => {
+ const original = 'Patch Set 1: Code-Style+1';
+ const tag = undefined;
+ const expected = '';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
+ });
+
+ test('comments', () => {
+ const original = 'Patch Set 1:\n\n(3 comments)';
+ const tag = undefined;
+ const expected = '';
+ let actual = element._computeMessageContent(original, tag, true);
+ assert.equal(actual, expected);
+ actual = element._computeMessageContent(original, tag, false);
+ assert.equal(actual, expected);
});
});
- suite('when logged in but not admin', () => {
- setup(done => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getConfig() { return Promise.resolve({}); },
- getIsAdmin() { return Promise.resolve(false); },
- deleteChangeCommitMessage() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- flush(done);
- });
+ test('votes', () => {
+ element.message = {
+ author: {},
+ expanded: false,
+ message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
+ };
+ element.labelExtremes = {
+ 'Verified': {max: 1, min: -1},
+ 'Code-Review': {max: 2, min: -2},
+ 'Trybot-Label3': {max: 3, min: 0},
+ };
+ flushAsynchronousOperations();
+ const scoreChips = dom(element.root).querySelectorAll('.score');
+ assert.equal(scoreChips.length, 3);
- test('can see reply but not delete button', () => {
- element.message = {
- id: '47c43261_55aa2c41',
- author: {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- },
- date: '2016-01-12 20:24:49.448000000',
- message: 'Uploaded patch set 1.',
- _revision_number: 1,
- };
+ assert.isTrue(scoreChips[0].classList.contains('positive'));
+ assert.isTrue(scoreChips[0].classList.contains('max'));
- flushAsynchronousOperations();
- assert.isFalse(
- element.shadowRoot.querySelector('.replyActionContainer').hidden
- );
- assert.isTrue(
- element.shadowRoot.querySelector('.deleteBtn').hidden
- );
- });
+ assert.isTrue(scoreChips[1].classList.contains('negative'));
+ assert.isTrue(scoreChips[1].classList.contains('min'));
+
+ assert.isTrue(scoreChips[2].classList.contains('positive'));
+ assert.isFalse(scoreChips[2].classList.contains('min'));
+ });
+
+ test('removed votes', () => {
+ element.message = {
+ author: {},
+ expanded: false,
+ message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
+ };
+ element.labelExtremes = {
+ 'Verified': {max: 1, min: -1},
+ 'Code-Review': {max: 2, min: -2},
+ 'Commit-Queue': {max: 3, min: 0},
+ };
+ flushAsynchronousOperations();
+ const scoreChips = dom(element.root).querySelectorAll('.score');
+ assert.equal(scoreChips.length, 3);
+
+ assert.isTrue(scoreChips[1].classList.contains('removed'));
+ assert.isTrue(scoreChips[2].classList.contains('removed'));
+ });
+
+ test('false negative vote', () => {
+ element.message = {
+ author: {},
+ expanded: false,
+ message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
+ };
+ element.labelExtremes = {};
+ const scoreChips = dom(element.root).querySelectorAll('.score');
+ assert.equal(scoreChips.length, 0);
});
});
+
+ suite('when not logged in', () => {
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getPreferences() { return Promise.resolve({}); },
+ getConfig() { return Promise.resolve({}); },
+ getIsAdmin() { return Promise.resolve(false); },
+ deleteChangeCommitMessage() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ flush(done);
+ });
+
+ test('reply and delete button should be hidden', () => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
+
+ flushAsynchronousOperations();
+ assert.isTrue(
+ element.shadowRoot.querySelector('.replyActionContainer').hidden
+ );
+ assert.isTrue(
+ element.shadowRoot.querySelector('.deleteBtn').hidden
+ );
+ });
+ });
+
+ suite('when logged in but not admin', () => {
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getConfig() { return Promise.resolve({}); },
+ getIsAdmin() { return Promise.resolve(false); },
+ deleteChangeCommitMessage() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ flush(done);
+ });
+
+ test('can see reply but not delete button', () => {
+ element.message = {
+ id: '47c43261_55aa2c41',
+ author: {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ },
+ date: '2016-01-12 20:24:49.448000000',
+ message: 'Uploaded patch set 1.',
+ _revision_number: 1,
+ };
+
+ flushAsynchronousOperations();
+ assert.isFalse(
+ element.shadowRoot.querySelector('.replyActionContainer').hidden
+ );
+ assert.isTrue(
+ element.shadowRoot.querySelector('.deleteBtn').hidden
+ );
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 6f74e4b..eaac988 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.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,416 +14,429 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MAX_INITIAL_SHOWN_MESSAGES = 20;
- const MESSAGES_INCREMENT = 5;
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../gr-message/gr-message.js';
+import '../../../styles/shared-styles.js';
+import {flush, 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-messages-list_html.js';
- const ReportingEvent = {
- SHOW_ALL: 'show-all-messages',
- SHOW_MORE: 'show-more-messages',
- };
+const MAX_INITIAL_SHOWN_MESSAGES = 20;
+const MESSAGES_INCREMENT = 5;
- /**
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
- */
- class GrMessagesList extends Polymer.mixinBehaviors( [
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-messages-list'; }
+const ReportingEvent = {
+ SHOW_ALL: 'show-all-messages',
+ SHOW_MORE: 'show-more-messages',
+};
- static get properties() {
- return {
- changeNum: Number,
- messages: {
- type: Array,
- value() { return []; },
- },
- reviewerUpdates: {
- type: Array,
- value() { return []; },
- },
- changeComments: Object,
- projectName: String,
- showReplyButtons: {
- type: Boolean,
- value: false,
- },
- labels: Object,
+/**
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrMessagesList extends mixinBehaviors( [
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _expanded: {
- type: Boolean,
- value: false,
- observer: '_expandedChanged',
- },
+ static get is() { return 'gr-messages-list'; }
- _expandCollapseTitle: {
- type: String,
- },
+ static get properties() {
+ return {
+ changeNum: Number,
+ messages: {
+ type: Array,
+ value() { return []; },
+ },
+ reviewerUpdates: {
+ type: Array,
+ value() { return []; },
+ },
+ changeComments: Object,
+ projectName: String,
+ showReplyButtons: {
+ type: Boolean,
+ value: false,
+ },
+ labels: Object,
- _hideAutomated: {
- type: Boolean,
- value: false,
- },
- /**
- * The messages after processing and including merged reviewer updates.
- */
- _processedMessages: {
- type: Array,
- computed: '_computeItems(messages, reviewerUpdates)',
- observer: '_processedMessagesChanged',
- },
- /**
- * The subset of _processedMessages that is visible to the user.
- */
- _visibleMessages: {
- type: Array,
- value() { return []; },
- },
+ _expanded: {
+ type: Boolean,
+ value: false,
+ observer: '_expandedChanged',
+ },
- _labelExtremes: {
- type: Object,
- computed: '_computeLabelExtremes(labels.*)',
- },
- };
- }
+ _expandCollapseTitle: {
+ type: String,
+ },
- scrollToMessage(messageID) {
- let el = this.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]');
- // If the message is hidden, expand the hidden messages back to that
- // point.
- if (!el) {
- let index;
- for (index = 0; index < this._processedMessages.length; index++) {
- if (this._processedMessages[index].id === messageID) {
- break;
- }
- }
- if (index === this._processedMessages.length) { return; }
+ _hideAutomated: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * The messages after processing and including merged reviewer updates.
+ */
+ _processedMessages: {
+ type: Array,
+ computed: '_computeItems(messages, reviewerUpdates)',
+ observer: '_processedMessagesChanged',
+ },
+ /**
+ * The subset of _processedMessages that is visible to the user.
+ */
+ _visibleMessages: {
+ type: Array,
+ value() { return []; },
+ },
- const newMessages = this._processedMessages.slice(index,
- -this._visibleMessages.length);
- // Add newMessages to the beginning of _visibleMessages.
- this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
- // Allow the dom-repeat to stamp.
- Polymer.dom.flush();
- el = this.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]');
- }
+ _labelExtremes: {
+ type: Object,
+ computed: '_computeLabelExtremes(labels.*)',
+ },
+ };
+ }
- el.set('message.expanded', true);
- let top = el.offsetTop;
- for (let offsetParent = el.offsetParent;
- offsetParent;
- offsetParent = offsetParent.offsetParent) {
- top += offsetParent.offsetTop;
- }
- window.scrollTo(0, top);
- this._highlightEl(el);
- }
-
- _isAutomated(message) {
- return !!(message.reviewer ||
- (message.tag && message.tag.startsWith('autogenerated')));
- }
-
- _computeItems(messages, reviewerUpdates) {
- // Polymer 2: check for undefined
- if ([messages, reviewerUpdates].some(arg => arg === undefined)) {
- return [];
- }
-
- messages = messages || [];
- reviewerUpdates = reviewerUpdates || [];
- let mi = 0;
- let ri = 0;
- let result = [];
- let mDate;
- let rDate;
- for (let i = 0; i < messages.length; i++) {
- messages[i]._index = i;
- }
-
- while (mi < messages.length || ri < reviewerUpdates.length) {
- if (mi >= messages.length) {
- result = result.concat(reviewerUpdates.slice(ri));
+ scrollToMessage(messageID) {
+ let el = this.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]');
+ // If the message is hidden, expand the hidden messages back to that
+ // point.
+ if (!el) {
+ let index;
+ for (index = 0; index < this._processedMessages.length; index++) {
+ if (this._processedMessages[index].id === messageID) {
break;
}
- if (ri >= reviewerUpdates.length) {
- result = result.concat(messages.slice(mi));
- break;
- }
- mDate = mDate || util.parseDate(messages[mi].date);
- rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
- if (rDate < mDate) {
- result.push(reviewerUpdates[ri++]);
- rDate = null;
- } else {
- result.push(messages[mi++]);
- mDate = null;
- }
}
- return result;
- }
+ if (index === this._processedMessages.length) { return; }
- _expandedChanged(exp) {
- if (this._processedMessages) {
- for (let i = 0; i < this._processedMessages.length; i++) {
- this._processedMessages[i].expanded = exp;
- }
- }
- // _visibleMessages is a subarray of _processedMessages
- // _processedMessages contains all items from _visibleMessages
- // At this point all _visibleMessages.expanded values are set,
- // and notifyPath must be used to notify Polymer about changes.
- if (this._visibleMessages) {
- for (let i = 0; i < this._visibleMessages.length; i++) {
- this.notifyPath(`_visibleMessages.${i}.expanded`);
- }
- }
-
- if (this._expanded) {
- this._expandCollapseTitle = this.createTitle(
- this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
- } else {
- this._expandCollapseTitle = this.createTitle(
- this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
- }
- }
-
- _highlightEl(el) {
- const highlightedEls =
- Polymer.dom(this.root).querySelectorAll('.highlighted');
- for (const highlighedEl of highlightedEls) {
- highlighedEl.classList.remove('highlighted');
- }
- function handleAnimationEnd() {
- el.removeEventListener('animationend', handleAnimationEnd);
- el.classList.remove('highlighted');
- }
- el.addEventListener('animationend', handleAnimationEnd);
- el.classList.add('highlighted');
- }
-
- /**
- * @param {boolean} expand
- */
- handleExpandCollapse(expand) {
- this._expanded = expand;
- }
-
- _handleExpandCollapseTap(e) {
- e.preventDefault();
- this.handleExpandCollapse(!this._expanded);
- }
-
- _handleAnchorClick(e) {
- this.scrollToMessage(e.detail.id);
- }
-
- _hasAutomatedMessages(messages) {
- if (!messages) { return false; }
- for (const message of messages) {
- if (this._isAutomated(message)) {
- return true;
- }
- }
- return false;
- }
-
- _computeExpandCollapseMessage(expanded) {
- return expanded ? 'Collapse all' : 'Expand all';
- }
-
- /**
- * Computes message author's file comments for change's message.
- * Method uses this.messages to find next message and relies on messages
- * to be sorted by date field descending.
- *
- * @param {!Object} changeComments changeComment object, which includes
- * a method to get all published comments (including robot comments),
- * which returns a Hash of arrays of comments, filename as key.
- * @param {!Object} message
- * @return {!Object} Hash of arrays of comments, filename as key.
- */
- _computeCommentsForMessage(changeComments, message) {
- if ([changeComments, message].some(arg => arg === undefined)) {
- return [];
- }
- const comments = changeComments.getAllPublishedComments();
- if (message._index === undefined || !comments || !this.messages) {
- return [];
- }
- const messages = this.messages || [];
- const index = message._index;
- const authorId = message.author && message.author._account_id;
- const mDate = util.parseDate(message.date).getTime();
- // NB: Messages array has oldest messages first.
- let nextMDate;
- if (index > 0) {
- for (let i = index - 1; i >= 0; i--) {
- if (messages[i] && messages[i].author &&
- messages[i].author._account_id === authorId) {
- nextMDate = util.parseDate(messages[i].date).getTime();
- break;
- }
- }
- }
- const msgComments = {};
- for (const file in comments) {
- if (!comments.hasOwnProperty(file)) { continue; }
- const fileComments = comments[file];
- for (let i = 0; i < fileComments.length; i++) {
- if (fileComments[i].author &&
- fileComments[i].author._account_id !== authorId) {
- continue;
- }
- const cDate = util.parseDate(fileComments[i].updated).getTime();
- if (cDate <= mDate) {
- if (nextMDate && cDate <= nextMDate) {
- continue;
- }
- msgComments[file] = msgComments[file] || [];
- msgComments[file].push(fileComments[i]);
- }
- }
- }
- return msgComments;
- }
-
- /**
- * Returns the number of messages to splice to the beginning of
- * _visibleMessages. This is the minimum of the total number of messages
- * remaining in the list and the number of messages needed to display five
- * more visible messages in the list.
- */
- _getDelta(visibleMessages, messages, hideAutomated) {
- if ([visibleMessages, messages].some(arg => arg === undefined)) {
- return 0;
- }
-
- let delta = MESSAGES_INCREMENT;
- const msgsRemaining = messages.length - visibleMessages.length;
-
- if (hideAutomated) {
- let counter = 0;
- let i;
- for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) {
- if (!this._isAutomated(messages[i - 1])) { counter++; }
- }
- delta = msgsRemaining - i;
- }
- return Math.min(msgsRemaining, delta);
- }
-
- /**
- * Gets the number of messages that would be visible, but do not currently
- * exist in _visibleMessages.
- */
- _numRemaining(visibleMessages, messages, hideAutomated) {
- if ([visibleMessages, messages].some(arg => arg === undefined)) {
- return 0;
- }
-
- if (hideAutomated) {
- return this._getHumanMessages(messages).length -
- this._getHumanMessages(visibleMessages).length;
- }
- return messages.length - visibleMessages.length;
- }
-
- _computeIncrementText(visibleMessages, messages, hideAutomated) {
- let delta = this._getDelta(visibleMessages, messages, hideAutomated);
- delta = Math.min(
- this._numRemaining(visibleMessages, messages, hideAutomated), delta);
- return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more';
- }
-
- _getHumanMessages(messages) {
- return messages.filter(msg => !this._isAutomated(msg));
- }
-
- _computeShowHideTextHidden(visibleMessages, messages,
- hideAutomated) {
- if ([visibleMessages, messages].some(arg => arg === undefined)) {
- return 0;
- }
-
- if (hideAutomated) {
- messages = this._getHumanMessages(messages);
- visibleMessages = this._getHumanMessages(visibleMessages);
- }
- return visibleMessages.length >= messages.length;
- }
-
- _handleShowAllTap() {
- this._visibleMessages = this._processedMessages;
- this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
- }
-
- _handleIncrementShownMessages() {
- const delta = this._getDelta(this._visibleMessages,
- this._processedMessages, this._hideAutomated);
- const len = this._visibleMessages.length;
- const newMessages = this._processedMessages.slice(-(len + delta), -len);
- // Add newMessages to the beginning of _visibleMessages
+ const newMessages = this._processedMessages.slice(index,
+ -this._visibleMessages.length);
+ // Add newMessages to the beginning of _visibleMessages.
this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
- this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
+ // Allow the dom-repeat to stamp.
+ flush();
+ el = this.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]');
}
- _processedMessagesChanged(messages) {
- if (messages) {
- this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES);
+ el.set('message.expanded', true);
+ let top = el.offsetTop;
+ for (let offsetParent = el.offsetParent;
+ offsetParent;
+ offsetParent = offsetParent.offsetParent) {
+ top += offsetParent.offsetTop;
+ }
+ window.scrollTo(0, top);
+ this._highlightEl(el);
+ }
- if (messages.length === 0) return;
- const tags = messages.map(message => message.tag || message.type ||
- (message.comments ? 'comments' : 'none'));
- const tagsCounted = tags.reduce((acc, val) => {
- acc[val] = (acc[val] || 0) + 1;
- return acc;
- }, {all: messages.length});
- this.$.reporting.reportInteraction('messages-count', tagsCounted);
+ _isAutomated(message) {
+ return !!(message.reviewer ||
+ (message.tag && message.tag.startsWith('autogenerated')));
+ }
+
+ _computeItems(messages, reviewerUpdates) {
+ // Polymer 2: check for undefined
+ if ([messages, reviewerUpdates].some(arg => arg === undefined)) {
+ return [];
+ }
+
+ messages = messages || [];
+ reviewerUpdates = reviewerUpdates || [];
+ let mi = 0;
+ let ri = 0;
+ let result = [];
+ let mDate;
+ let rDate;
+ for (let i = 0; i < messages.length; i++) {
+ messages[i]._index = i;
+ }
+
+ while (mi < messages.length || ri < reviewerUpdates.length) {
+ if (mi >= messages.length) {
+ result = result.concat(reviewerUpdates.slice(ri));
+ break;
+ }
+ if (ri >= reviewerUpdates.length) {
+ result = result.concat(messages.slice(mi));
+ break;
+ }
+ mDate = mDate || util.parseDate(messages[mi].date);
+ rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
+ if (rDate < mDate) {
+ result.push(reviewerUpdates[ri++]);
+ rDate = null;
+ } else {
+ result.push(messages[mi++]);
+ mDate = null;
+ }
+ }
+ return result;
+ }
+
+ _expandedChanged(exp) {
+ if (this._processedMessages) {
+ for (let i = 0; i < this._processedMessages.length; i++) {
+ this._processedMessages[i].expanded = exp;
+ }
+ }
+ // _visibleMessages is a subarray of _processedMessages
+ // _processedMessages contains all items from _visibleMessages
+ // At this point all _visibleMessages.expanded values are set,
+ // and notifyPath must be used to notify Polymer about changes.
+ if (this._visibleMessages) {
+ for (let i = 0; i < this._visibleMessages.length; i++) {
+ this.notifyPath(`_visibleMessages.${i}.expanded`);
}
}
- _computeNumMessagesText(visibleMessages, messages,
- hideAutomated) {
- const total =
- this._numRemaining(visibleMessages, messages, hideAutomated);
- return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages';
- }
-
- _computeIncrementHidden(visibleMessages, messages,
- hideAutomated) {
- const total =
- this._numRemaining(visibleMessages, messages, hideAutomated);
- return total <= this._getDelta(visibleMessages, messages, hideAutomated);
- }
-
- /**
- * Compute a mapping from label name to objects representing the minimum and
- * maximum possible values for that label.
- */
- _computeLabelExtremes(labelRecord) {
- const extremes = {};
- const labels = labelRecord.base;
- if (!labels) { return extremes; }
- for (const key of Object.keys(labels)) {
- if (!labels[key] || !labels[key].values) { continue; }
- const values = Object.keys(labels[key].values)
- .map(v => parseInt(v, 10));
- values.sort((a, b) => a - b);
- if (!values.length) { continue; }
- extremes[key] = {min: values[0], max: values[values.length - 1]};
- }
- return extremes;
+ if (this._expanded) {
+ this._expandCollapseTitle = this.createTitle(
+ this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
+ } else {
+ this._expandCollapseTitle = this.createTitle(
+ this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
}
}
- customElements.define(GrMessagesList.is, GrMessagesList);
-})();
+ _highlightEl(el) {
+ const highlightedEls =
+ dom(this.root).querySelectorAll('.highlighted');
+ for (const highlighedEl of highlightedEls) {
+ highlighedEl.classList.remove('highlighted');
+ }
+ function handleAnimationEnd() {
+ el.removeEventListener('animationend', handleAnimationEnd);
+ el.classList.remove('highlighted');
+ }
+ el.addEventListener('animationend', handleAnimationEnd);
+ el.classList.add('highlighted');
+ }
+
+ /**
+ * @param {boolean} expand
+ */
+ handleExpandCollapse(expand) {
+ this._expanded = expand;
+ }
+
+ _handleExpandCollapseTap(e) {
+ e.preventDefault();
+ this.handleExpandCollapse(!this._expanded);
+ }
+
+ _handleAnchorClick(e) {
+ this.scrollToMessage(e.detail.id);
+ }
+
+ _hasAutomatedMessages(messages) {
+ if (!messages) { return false; }
+ for (const message of messages) {
+ if (this._isAutomated(message)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ _computeExpandCollapseMessage(expanded) {
+ return expanded ? 'Collapse all' : 'Expand all';
+ }
+
+ /**
+ * Computes message author's file comments for change's message.
+ * Method uses this.messages to find next message and relies on messages
+ * to be sorted by date field descending.
+ *
+ * @param {!Object} changeComments changeComment object, which includes
+ * a method to get all published comments (including robot comments),
+ * which returns a Hash of arrays of comments, filename as key.
+ * @param {!Object} message
+ * @return {!Object} Hash of arrays of comments, filename as key.
+ */
+ _computeCommentsForMessage(changeComments, message) {
+ if ([changeComments, message].some(arg => arg === undefined)) {
+ return [];
+ }
+ const comments = changeComments.getAllPublishedComments();
+ if (message._index === undefined || !comments || !this.messages) {
+ return [];
+ }
+ const messages = this.messages || [];
+ const index = message._index;
+ const authorId = message.author && message.author._account_id;
+ const mDate = util.parseDate(message.date).getTime();
+ // NB: Messages array has oldest messages first.
+ let nextMDate;
+ if (index > 0) {
+ for (let i = index - 1; i >= 0; i--) {
+ if (messages[i] && messages[i].author &&
+ messages[i].author._account_id === authorId) {
+ nextMDate = util.parseDate(messages[i].date).getTime();
+ break;
+ }
+ }
+ }
+ const msgComments = {};
+ for (const file in comments) {
+ if (!comments.hasOwnProperty(file)) { continue; }
+ const fileComments = comments[file];
+ for (let i = 0; i < fileComments.length; i++) {
+ if (fileComments[i].author &&
+ fileComments[i].author._account_id !== authorId) {
+ continue;
+ }
+ const cDate = util.parseDate(fileComments[i].updated).getTime();
+ if (cDate <= mDate) {
+ if (nextMDate && cDate <= nextMDate) {
+ continue;
+ }
+ msgComments[file] = msgComments[file] || [];
+ msgComments[file].push(fileComments[i]);
+ }
+ }
+ }
+ return msgComments;
+ }
+
+ /**
+ * Returns the number of messages to splice to the beginning of
+ * _visibleMessages. This is the minimum of the total number of messages
+ * remaining in the list and the number of messages needed to display five
+ * more visible messages in the list.
+ */
+ _getDelta(visibleMessages, messages, hideAutomated) {
+ if ([visibleMessages, messages].some(arg => arg === undefined)) {
+ return 0;
+ }
+
+ let delta = MESSAGES_INCREMENT;
+ const msgsRemaining = messages.length - visibleMessages.length;
+
+ if (hideAutomated) {
+ let counter = 0;
+ let i;
+ for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) {
+ if (!this._isAutomated(messages[i - 1])) { counter++; }
+ }
+ delta = msgsRemaining - i;
+ }
+ return Math.min(msgsRemaining, delta);
+ }
+
+ /**
+ * Gets the number of messages that would be visible, but do not currently
+ * exist in _visibleMessages.
+ */
+ _numRemaining(visibleMessages, messages, hideAutomated) {
+ if ([visibleMessages, messages].some(arg => arg === undefined)) {
+ return 0;
+ }
+
+ if (hideAutomated) {
+ return this._getHumanMessages(messages).length -
+ this._getHumanMessages(visibleMessages).length;
+ }
+ return messages.length - visibleMessages.length;
+ }
+
+ _computeIncrementText(visibleMessages, messages, hideAutomated) {
+ let delta = this._getDelta(visibleMessages, messages, hideAutomated);
+ delta = Math.min(
+ this._numRemaining(visibleMessages, messages, hideAutomated), delta);
+ return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more';
+ }
+
+ _getHumanMessages(messages) {
+ return messages.filter(msg => !this._isAutomated(msg));
+ }
+
+ _computeShowHideTextHidden(visibleMessages, messages,
+ hideAutomated) {
+ if ([visibleMessages, messages].some(arg => arg === undefined)) {
+ return 0;
+ }
+
+ if (hideAutomated) {
+ messages = this._getHumanMessages(messages);
+ visibleMessages = this._getHumanMessages(visibleMessages);
+ }
+ return visibleMessages.length >= messages.length;
+ }
+
+ _handleShowAllTap() {
+ this._visibleMessages = this._processedMessages;
+ this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
+ }
+
+ _handleIncrementShownMessages() {
+ const delta = this._getDelta(this._visibleMessages,
+ this._processedMessages, this._hideAutomated);
+ const len = this._visibleMessages.length;
+ const newMessages = this._processedMessages.slice(-(len + delta), -len);
+ // Add newMessages to the beginning of _visibleMessages
+ this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
+ this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
+ }
+
+ _processedMessagesChanged(messages) {
+ if (messages) {
+ this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES);
+
+ if (messages.length === 0) return;
+ const tags = messages.map(message => message.tag || message.type ||
+ (message.comments ? 'comments' : 'none'));
+ const tagsCounted = tags.reduce((acc, val) => {
+ acc[val] = (acc[val] || 0) + 1;
+ return acc;
+ }, {all: messages.length});
+ this.$.reporting.reportInteraction('messages-count', tagsCounted);
+ }
+ }
+
+ _computeNumMessagesText(visibleMessages, messages,
+ hideAutomated) {
+ const total =
+ this._numRemaining(visibleMessages, messages, hideAutomated);
+ return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages';
+ }
+
+ _computeIncrementHidden(visibleMessages, messages,
+ hideAutomated) {
+ const total =
+ this._numRemaining(visibleMessages, messages, hideAutomated);
+ return total <= this._getDelta(visibleMessages, messages, hideAutomated);
+ }
+
+ /**
+ * Compute a mapping from label name to objects representing the minimum and
+ * maximum possible values for that label.
+ */
+ _computeLabelExtremes(labelRecord) {
+ const extremes = {};
+ const labels = labelRecord.base;
+ if (!labels) { return extremes; }
+ for (const key of Object.keys(labels)) {
+ if (!labels[key] || !labels[key].values) { continue; }
+ const values = Object.keys(labels[key].values)
+ .map(v => parseInt(v, 10));
+ values.sort((a, b) => a - b);
+ if (!values.length) { continue; }
+ extremes[key] = {min: values[0], max: values[values.length - 1]};
+ }
+ return extremes;
+ }
+}
+
+customElements.define(GrMessagesList.is, GrMessagesList);
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
index 60ec6b0..3e9f7b5 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_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="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../gr-message/gr-message.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-messages-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host,
.messageListControls {
@@ -75,55 +67,27 @@
}
</style>
<div class="header">
- <span
- id="automatedMessageToggleContainer"
- class="container"
- hidden$="[[!_hasAutomatedMessages(messages)]]">
- <paper-toggle-button
- id="automatedMessageToggle"
- checked="{{_hideAutomated}}"></paper-toggle-button>Only comments
+ <span id="automatedMessageToggleContainer" class="container" hidden\$="[[!_hasAutomatedMessages(messages)]]">
+ <paper-toggle-button id="automatedMessageToggle" checked="{{_hideAutomated}}"></paper-toggle-button>Only comments
<span class="transparent separator"></span>
</span>
- <gr-button
- id="collapse-messages"
- link
- title="[[_expandCollapseTitle]]"
- on-click="_handleExpandCollapseTap">
+ <gr-button id="collapse-messages" link="" title="[[_expandCollapseTitle]]" on-click="_handleExpandCollapseTap">
[[_computeExpandCollapseMessage(_expanded)]]
</gr-button>
</div>
- <span
- id="messageControlsContainer"
- hidden$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
- <gr-button id="oldMessagesBtn" link on-click="_handleShowAllTap">
+ <span id="messageControlsContainer" hidden\$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
+ <gr-button id="oldMessagesBtn" link="" on-click="_handleShowAllTap">
[[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
</gr-button>
- <span
- class="container"
- hidden$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
+ <span class="container" hidden\$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
<span class="transparent separator"></span>
- <gr-button id="incrementMessagesBtn" link
- on-click="_handleIncrementShownMessages">
+ <gr-button id="incrementMessagesBtn" link="" on-click="_handleIncrementShownMessages">
[[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]
</gr-button>
</span>
</span>
- <template
- is="dom-repeat"
- items="[[_visibleMessages]]"
- as="message">
- <gr-message
- change-num="[[changeNum]]"
- message="[[message]]"
- comments="[[_computeCommentsForMessage(changeComments, message)]]"
- hide-automated="[[_hideAutomated]]"
- project-name="[[projectName]]"
- show-reply-button="[[showReplyButtons]]"
- on-message-anchor-tap="_handleAnchorClick"
- label-extremes="[[_labelExtremes]]"
- data-message-id$="[[message.id]]"></gr-message>
+ <template is="dom-repeat" items="[[_visibleMessages]]" as="message">
+ <gr-message change-num="[[changeNum]]" message="[[message]]" comments="[[_computeCommentsForMessage(changeComments, message)]]" hide-automated="[[_hideAutomated]]" project-name="[[projectName]]" show-reply-button="[[showReplyButtons]]" on-message-anchor-tap="_handleAnchorClick" label-extremes="[[_labelExtremes]]" data-message-id\$="[[message.id]]"></gr-message>
</template>
<gr-reporting id="reporting" category="message-list"></gr-reporting>
- </template>
- <script src="gr-messages-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 2ee2a81..961abb9 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -19,17 +19,24 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-messages-list</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="../../diff/gr-comment-api/gr-comment-api.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="../../diff/gr-comment-api/gr-comment-api.js"></script>
-<link rel="import" href="gr-messages-list.html">
+<script type="module" src="./gr-messages-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import './gr-messages-list.js';
+import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+void(0);
+</script>
<dom-module id="comment-api-mock">
<template>
@@ -38,7 +45,7 @@
change-comments="[[_changeComments]]"></gr-messages-list>
<gr-comment-api id="commentAPI"></gr-comment-api>
</template>
- <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
+ <script type="module" src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
</dom-module>
<test-fixture id="basic">
@@ -49,574 +56,579 @@
</template>
</test-fixture>
-<script>
- const randomMessage = function(opt_params) {
- const params = opt_params || {};
- const author1 = {
- _account_id: 1115495,
- name: 'Andrew Bonventre',
- email: 'andybons@chromium.org',
- };
- return {
- id: params.id || Math.random().toString(),
- date: params.date || '2016-01-12 20:28:33.038000',
- message: params.message || Math.random().toString(),
- _revision_number: params._revision_number || 1,
- author: params.author || author1,
- };
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../diff/gr-comment-api/gr-comment-api.js';
+import './gr-messages-list.js';
+import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+const randomMessage = function(opt_params) {
+ const params = opt_params || {};
+ const author1 = {
+ _account_id: 1115495,
+ name: 'Andrew Bonventre',
+ email: 'andybons@chromium.org',
+ };
+ return {
+ id: params.id || Math.random().toString(),
+ date: params.date || '2016-01-12 20:28:33.038000',
+ message: params.message || Math.random().toString(),
+ _revision_number: params._revision_number || 1,
+ author: params.author || author1,
+ };
+};
+
+const randomAutomated = function(opt_params) {
+ return Object.assign({tag: 'autogenerated:gerrit:replace'},
+ randomMessage(opt_params));
+};
+
+suite('gr-messages-list tests', () => {
+ let element;
+ let messages;
+ let sandbox;
+ let commentApiWrapper;
+
+ const getMessages = function() {
+ return dom(element.root).querySelectorAll('gr-message');
};
- const randomAutomated = function(opt_params) {
- return Object.assign({tag: 'autogenerated:gerrit:replace'},
- randomMessage(opt_params));
+ const author = {
+ _account_id: 42,
+ name: 'Marvin the Paranoid Android',
+ email: 'marvin@sirius.org',
};
- suite('gr-messages-list tests', async () => {
- await readyToTest();
+ const comments = {
+ file1: [
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: '6505d749_f0bec0aa',
+ line: 62,
+ id: '6505d749_10ed44b2',
+ patch_set: 2,
+ author: {
+ email: 'some@email.com',
+ _account_id: 123,
+ },
+ },
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: 'c5912363_6b820105',
+ line: 42,
+ id: '450a935e_0f1c05db',
+ patch_set: 2,
+ author,
+ },
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: '6505d749_f0bec0aa',
+ line: 62,
+ id: '6505d749_10ed44b2',
+ patch_set: 2,
+ author,
+ },
+ ],
+ file2: [
+ {
+ message: 'message text',
+ updated: '2016-09-27 00:18:03.000000000',
+ in_reply_to: 'c5912363_4b7d450a',
+ line: 132,
+ id: '450a935e_4f260d25',
+ patch_set: 2,
+ author,
+ },
+ ],
+ };
+
+ suite('basic tests', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getDiffComments() { return Promise.resolve(comments); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+ sandbox = sinon.sandbox.create();
+ messages = _.times(3, randomMessage);
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.messagesList;
+ element.messages = messages;
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ return commentApiWrapper.loadComments();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('show some old messages', () => {
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ element.messages = _.times(26, randomMessage);
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ assert.equal(getMessages().length, 20);
+ assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+ .trim(), 'SHOW 5 MORE');
+ MockInteractions.tap(element.$.incrementMessagesBtn);
+ flushAsynchronousOperations();
+
+ assert.equal(getMessages().length, 25);
+ assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
+ .trim(), 'SHOW 1 MORE');
+ MockInteractions.tap(element.$.incrementMessagesBtn);
+ flushAsynchronousOperations();
+
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ assert.equal(getMessages().length, 26);
+ });
+
+ test('show all old messages', () => {
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ element.messages = _.times(26, randomMessage);
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ assert.equal(getMessages().length, 20);
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW ALL 6 MESSAGES');
+ MockInteractions.tap(element.$.oldMessagesBtn);
+ flushAsynchronousOperations();
+
+ assert.equal(getMessages().length, 26);
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ });
+
+ test('message count respects automated', () => {
+ element.messages = _.times(10, randomAutomated)
+ .concat(_.times(11, randomMessage));
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ flushAsynchronousOperations();
+
+ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
+ });
+
+ test('message count still respects non-automated on toggle', () => {
+ element.messages = _.times(10, randomMessage)
+ .concat(_.times(11, randomAutomated));
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
+ 'SHOW 1 MESSAGE');
+ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
+ });
+
+ test('show all messages respects expand', () => {
+ element.messages = _.times(10, randomAutomated)
+ .concat(_.times(11, randomMessage));
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages')); // Expand all.
+ flushAsynchronousOperations();
+
+ let messages = getMessages();
+ assert.equal(messages.length, 20);
+ for (const message of messages) {
+ assert.isTrue(message._expanded);
+ }
+
+ MockInteractions.tap(element.$.oldMessagesBtn);
+ flushAsynchronousOperations();
+
+ messages = getMessages();
+ assert.equal(messages.length, 21);
+ for (const message of messages) {
+ assert.isTrue(message._expanded);
+ }
+ });
+
+ test('show all messages respects collapse', () => {
+ element.messages = _.times(10, randomAutomated)
+ .concat(_.times(11, randomMessage));
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages')); // Expand all.
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages')); // Collapse all.
+ flushAsynchronousOperations();
+
+ let messages = getMessages();
+ assert.equal(messages.length, 20);
+ for (const message of messages) {
+ assert.isFalse(message._expanded);
+ }
+
+ MockInteractions.tap(element.$.oldMessagesBtn);
+ flushAsynchronousOperations();
+
+ messages = getMessages();
+ assert.equal(messages.length, 21);
+ for (const message of messages) {
+ assert.isFalse(message._expanded);
+ }
+ });
+
+ test('expand/collapse all', () => {
+ let allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ message._expanded = false;
+ }
+ MockInteractions.tap(allMessageEls[1]);
+ assert.isTrue(allMessageEls[1]._expanded);
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isTrue(message._expanded);
+ }
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isFalse(message._expanded);
+ }
+ });
+
+ test('expand/collapse from external keypress', () => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ let allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isTrue(message._expanded);
+ }
+
+ // Expand/collapse all text also changes.
+ assert.equal(element.shadowRoot
+ .querySelector('#collapse-messages').textContent.trim(),
+ 'Collapse all');
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('#collapse-messages'));
+ allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ assert.isFalse(message._expanded);
+ }
+ // Expand/collapse all text also changes.
+ assert.equal(element.shadowRoot
+ .querySelector('#collapse-messages').textContent.trim(),
+ 'Expand all');
+ });
+
+ test('hide messages does not appear when no automated messages', () => {
+ assert.isOk(element.shadowRoot
+ .querySelector('#automatedMessageToggleContainer[hidden]'));
+ });
+
+ test('scroll to message', () => {
+ const allMessageEls = getMessages();
+ for (const message of allMessageEls) {
+ message.set('message.expanded', false);
+ }
+
+ const scrollToStub = sandbox.stub(window, 'scrollTo');
+ const highlightStub = sandbox.stub(element, '_highlightEl');
+
+ element.scrollToMessage('invalid');
+
+ for (const message of allMessageEls) {
+ assert.isFalse(message._expanded,
+ 'expected gr-message to not be expanded');
+ }
+
+ const messageID = messages[1].id;
+ element.scrollToMessage(messageID);
+ assert.isTrue(
+ element.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]')
+ ._expanded);
+
+ assert.isTrue(scrollToStub.calledOnce);
+ assert.isTrue(highlightStub.calledOnce);
+ });
+
+ test('scroll to message offscreen', () => {
+ const scrollToStub = sandbox.stub(window, 'scrollTo');
+ const highlightStub = sandbox.stub(element, '_highlightEl');
+ element.messages = _.times(25, randomMessage);
+ flushAsynchronousOperations();
+ assert.isFalse(scrollToStub.called);
+ assert.isFalse(highlightStub.called);
+
+ const messageID = element.messages[1].id;
+ element.scrollToMessage(messageID);
+ assert.isTrue(scrollToStub.calledOnce);
+ assert.isTrue(highlightStub.calledOnce);
+ assert.equal(element._visibleMessages.length, 24);
+ assert.isTrue(
+ element.shadowRoot
+ .querySelector('[data-message-id="' + messageID + '"]')
+ ._expanded);
+ });
+
+ test('messages', () => {
+ const messages = [].concat(
+ randomMessage(),
+ {
+ _index: 5,
+ _revision_number: 4,
+ message: 'Uploaded patch set 4.',
+ date: '2016-09-28 13:36:33.000000000',
+ author,
+ id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
+ },
+ {
+ _index: 6,
+ _revision_number: 4,
+ message: 'Patch Set 4:\n\n(6 comments)',
+ date: '2016-09-28 13:36:33.000000000',
+ author,
+ id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5',
+ }
+ );
+ element.messages = messages;
+ const isAuthor = function(author, message) {
+ return message.author._account_id === author._account_id;
+ };
+ const isMarvin = isAuthor.bind(null, author);
+ flushAsynchronousOperations();
+ const messageElements = getMessages();
+ assert.equal(messageElements.length, messages.length);
+ assert.deepEqual(messageElements[1].message, messages[1]);
+ assert.deepEqual(messageElements[2].message, messages[2]);
+ assert.deepEqual(messageElements[1].comments.file1,
+ comments.file1.filter(isMarvin));
+ assert.deepEqual(messageElements[1].comments.file2,
+ comments.file2.filter(isMarvin));
+ assert.deepEqual(messageElements[2].comments, {});
+ });
+
+ test('messages without author do not throw', () => {
+ const messages = [{
+ _index: 5,
+ _revision_number: 4,
+ message: 'Uploaded patch set 4.',
+ date: '2016-09-28 13:36:33.000000000',
+ id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
+ }];
+ element.messages = messages;
+ flushAsynchronousOperations();
+ const messageEls = getMessages();
+ assert.equal(messageEls.length, 1);
+ assert.equal(messageEls[0].message.message, messages[0].message);
+ });
+
+ test('hide increment text if increment >= total remaining', () => {
+ // Test with stubbed return values, as _numRemaining and _getDelta have
+ // their own tests.
+ sandbox.stub(element, '_getDelta').returns(5);
+ const remainingStub = sandbox.stub(element, '_numRemaining').returns(6);
+ assert.isFalse(element._computeIncrementHidden(null, null, null));
+ remainingStub.restore();
+
+ sandbox.stub(element, '_numRemaining').returns(4);
+ assert.isTrue(element._computeIncrementHidden(null, null, null));
+ });
+ });
+
+ suite('gr-messages-list automate tests', () => {
let element;
let messages;
let sandbox;
let commentApiWrapper;
const getMessages = function() {
- return Polymer.dom(element.root).querySelectorAll('gr-message');
+ return dom(element.root).querySelectorAll('gr-message');
+ };
+ const getHiddenMessages = function() {
+ return dom(element.root).querySelectorAll('gr-message[hidden]');
};
- const author = {
- _account_id: 42,
- name: 'Marvin the Paranoid Android',
- email: 'marvin@sirius.org',
+ const randomMessageReviewer = {
+ reviewer: {},
+ date: '2016-01-13 20:30:33.038000',
};
- const comments = {
- file1: [
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: '6505d749_f0bec0aa',
- line: 62,
- id: '6505d749_10ed44b2',
- patch_set: 2,
- author: {
- email: 'some@email.com',
- _account_id: 123,
- },
- },
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: 'c5912363_6b820105',
- line: 42,
- id: '450a935e_0f1c05db',
- patch_set: 2,
- author,
- },
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: '6505d749_f0bec0aa',
- line: 62,
- id: '6505d749_10ed44b2',
- patch_set: 2,
- author,
- },
- ],
- file2: [
- {
- message: 'message text',
- updated: '2016-09-27 00:18:03.000000000',
- in_reply_to: 'c5912363_4b7d450a',
- line: 132,
- id: '450a935e_4f260d25',
- patch_set: 2,
- author,
- },
- ],
- };
-
- suite('basic tests', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- getDiffComments() { return Promise.resolve(comments); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- });
- sandbox = sinon.sandbox.create();
- messages = _.times(3, randomMessage);
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.messagesList;
- element.messages = messages;
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- return commentApiWrapper.loadComments();
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
});
- teardown(() => {
- sandbox.restore();
- });
+ sandbox = sinon.sandbox.create();
+ messages = _.times(2, randomAutomated);
+ messages.push(randomMessageReviewer);
- test('show some old messages', () => {
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- element.messages = _.times(26, randomMessage);
- flushAsynchronousOperations();
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.messagesList;
+ sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
+ element.messages = messages;
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- assert.equal(getMessages().length, 20);
- assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
- .trim(), 'SHOW 5 MORE');
- MockInteractions.tap(element.$.incrementMessagesBtn);
- flushAsynchronousOperations();
-
- assert.equal(getMessages().length, 25);
- assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase()
- .trim(), 'SHOW 1 MORE');
- MockInteractions.tap(element.$.incrementMessagesBtn);
- flushAsynchronousOperations();
-
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- assert.equal(getMessages().length, 26);
- });
-
- test('show all old messages', () => {
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- element.messages = _.times(26, randomMessage);
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- assert.equal(getMessages().length, 20);
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW ALL 6 MESSAGES');
- MockInteractions.tap(element.$.oldMessagesBtn);
- flushAsynchronousOperations();
-
- assert.equal(getMessages().length, 26);
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- });
-
- test('message count respects automated', () => {
- element.messages = _.times(10, randomAutomated)
- .concat(_.times(11, randomMessage));
- flushAsynchronousOperations();
-
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW 1 MESSAGE');
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- MockInteractions.tap(element.$.automatedMessageToggle);
- flushAsynchronousOperations();
-
- assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden'));
- });
-
- test('message count still respects non-automated on toggle', () => {
- element.messages = _.times(10, randomMessage)
- .concat(_.times(11, randomAutomated));
- flushAsynchronousOperations();
-
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW 1 MESSAGE');
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- MockInteractions.tap(element.$.automatedMessageToggle);
- flushAsynchronousOperations();
-
- assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(),
- 'SHOW 1 MESSAGE');
- assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden'));
- });
-
- test('show all messages respects expand', () => {
- element.messages = _.times(10, randomAutomated)
- .concat(_.times(11, randomMessage));
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages')); // Expand all.
- flushAsynchronousOperations();
-
- let messages = getMessages();
- assert.equal(messages.length, 20);
- for (const message of messages) {
- assert.isTrue(message._expanded);
- }
-
- MockInteractions.tap(element.$.oldMessagesBtn);
- flushAsynchronousOperations();
-
- messages = getMessages();
- assert.equal(messages.length, 21);
- for (const message of messages) {
- assert.isTrue(message._expanded);
- }
- });
-
- test('show all messages respects collapse', () => {
- element.messages = _.times(10, randomAutomated)
- .concat(_.times(11, randomMessage));
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages')); // Expand all.
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages')); // Collapse all.
- flushAsynchronousOperations();
-
- let messages = getMessages();
- assert.equal(messages.length, 20);
- for (const message of messages) {
- assert.isFalse(message._expanded);
- }
-
- MockInteractions.tap(element.$.oldMessagesBtn);
- flushAsynchronousOperations();
-
- messages = getMessages();
- assert.equal(messages.length, 21);
- for (const message of messages) {
- assert.isFalse(message._expanded);
- }
- });
-
- test('expand/collapse all', () => {
- let allMessageEls = getMessages();
- for (const message of allMessageEls) {
- message._expanded = false;
- }
- MockInteractions.tap(allMessageEls[1]);
- assert.isTrue(allMessageEls[1]._expanded);
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isTrue(message._expanded);
- }
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isFalse(message._expanded);
- }
- });
-
- test('expand/collapse from external keypress', () => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- let allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isTrue(message._expanded);
- }
-
- // Expand/collapse all text also changes.
- assert.equal(element.shadowRoot
- .querySelector('#collapse-messages').textContent.trim(),
- 'Collapse all');
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('#collapse-messages'));
- allMessageEls = getMessages();
- for (const message of allMessageEls) {
- assert.isFalse(message._expanded);
- }
- // Expand/collapse all text also changes.
- assert.equal(element.shadowRoot
- .querySelector('#collapse-messages').textContent.trim(),
- 'Expand all');
- });
-
- test('hide messages does not appear when no automated messages', () => {
- assert.isOk(element.shadowRoot
- .querySelector('#automatedMessageToggleContainer[hidden]'));
- });
-
- test('scroll to message', () => {
- const allMessageEls = getMessages();
- for (const message of allMessageEls) {
- message.set('message.expanded', false);
- }
-
- const scrollToStub = sandbox.stub(window, 'scrollTo');
- const highlightStub = sandbox.stub(element, '_highlightEl');
-
- element.scrollToMessage('invalid');
-
- for (const message of allMessageEls) {
- assert.isFalse(message._expanded,
- 'expected gr-message to not be expanded');
- }
-
- const messageID = messages[1].id;
- element.scrollToMessage(messageID);
- assert.isTrue(
- element.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]')
- ._expanded);
-
- assert.isTrue(scrollToStub.calledOnce);
- assert.isTrue(highlightStub.calledOnce);
- });
-
- test('scroll to message offscreen', () => {
- const scrollToStub = sandbox.stub(window, 'scrollTo');
- const highlightStub = sandbox.stub(element, '_highlightEl');
- element.messages = _.times(25, randomMessage);
- flushAsynchronousOperations();
- assert.isFalse(scrollToStub.called);
- assert.isFalse(highlightStub.called);
-
- const messageID = element.messages[1].id;
- element.scrollToMessage(messageID);
- assert.isTrue(scrollToStub.calledOnce);
- assert.isTrue(highlightStub.calledOnce);
- assert.equal(element._visibleMessages.length, 24);
- assert.isTrue(
- element.shadowRoot
- .querySelector('[data-message-id="' + messageID + '"]')
- ._expanded);
- });
-
- test('messages', () => {
- const messages = [].concat(
- randomMessage(),
- {
- _index: 5,
- _revision_number: 4,
- message: 'Uploaded patch set 4.',
- date: '2016-09-28 13:36:33.000000000',
- author,
- id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
- },
- {
- _index: 6,
- _revision_number: 4,
- message: 'Patch Set 4:\n\n(6 comments)',
- date: '2016-09-28 13:36:33.000000000',
- author,
- id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5',
- }
- );
- element.messages = messages;
- const isAuthor = function(author, message) {
- return message.author._account_id === author._account_id;
- };
- const isMarvin = isAuthor.bind(null, author);
- flushAsynchronousOperations();
- const messageElements = getMessages();
- assert.equal(messageElements.length, messages.length);
- assert.deepEqual(messageElements[1].message, messages[1]);
- assert.deepEqual(messageElements[2].message, messages[2]);
- assert.deepEqual(messageElements[1].comments.file1,
- comments.file1.filter(isMarvin));
- assert.deepEqual(messageElements[1].comments.file2,
- comments.file2.filter(isMarvin));
- assert.deepEqual(messageElements[2].comments, {});
- });
-
- test('messages without author do not throw', () => {
- const messages = [{
- _index: 5,
- _revision_number: 4,
- message: 'Uploaded patch set 4.',
- date: '2016-09-28 13:36:33.000000000',
- id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
- }];
- element.messages = messages;
- flushAsynchronousOperations();
- const messageEls = getMessages();
- assert.equal(messageEls.length, 1);
- assert.equal(messageEls[0].message.message, messages[0].message);
- });
-
- test('hide increment text if increment >= total remaining', () => {
- // Test with stubbed return values, as _numRemaining and _getDelta have
- // their own tests.
- sandbox.stub(element, '_getDelta').returns(5);
- const remainingStub = sandbox.stub(element, '_numRemaining').returns(6);
- assert.isFalse(element._computeIncrementHidden(null, null, null));
- remainingStub.restore();
-
- sandbox.stub(element, '_numRemaining').returns(4);
- assert.isTrue(element._computeIncrementHidden(null, null, null));
- });
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ return commentApiWrapper.loadComments();
});
- suite('gr-messages-list automate tests', () => {
- let element;
- let messages;
- let sandbox;
- let commentApiWrapper;
+ teardown(() => {
+ sandbox.restore();
+ });
- const getMessages = function() {
- return Polymer.dom(element.root).querySelectorAll('gr-message');
- };
- const getHiddenMessages = function() {
- return Polymer.dom(element.root).querySelectorAll('gr-message[hidden]');
- };
+ test('hide autogenerated button is not hidden', () => {
+ assert.isNotOk(element.shadowRoot
+ .querySelector('#automatedMessageToggle[hidden]'));
+ });
- const randomMessageReviewer = {
- reviewer: {},
- date: '2016-01-13 20:30:33.038000',
- };
+ test('autogenerated messages are not hidden initially', () => {
+ const allHiddenMessageEls = getHiddenMessages();
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
+ // There are no hidden messages.
+ assert.isFalse(!!allHiddenMessageEls.length);
+ });
+
+ test('autogenerated messages hidden after comments only toggle', () => {
+ let allHiddenMessageEls = getHiddenMessages();
+
+ element._hideAutomated = false;
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ flushAsynchronousOperations();
+ const allMessageEls = getMessages();
+ allHiddenMessageEls = getHiddenMessages();
+
+ // Autogenerated messages are now hidden.
+ assert.equal(allHiddenMessageEls.length, allMessageEls.length);
+ });
+
+ test('autogenerated messages not hidden after comments only toggle',
+ () => {
+ let allHiddenMessageEls = getHiddenMessages();
+
+ element._hideAutomated = true;
+ MockInteractions.tap(element.$.automatedMessageToggle);
+ allHiddenMessageEls = getHiddenMessages();
+
+ // Autogenerated messages are now hidden.
+ assert.isFalse(!!allHiddenMessageEls.length);
});
- sandbox = sinon.sandbox.create();
- messages = _.times(2, randomAutomated);
- messages.push(randomMessageReviewer);
+ test('_getDelta', () => {
+ let messages = [randomMessage()];
+ assert.equal(element._getDelta([], messages, false), 1);
+ assert.equal(element._getDelta([], messages, true), 1);
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.messagesList;
- sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
- element.messages = messages;
+ messages = _.times(7, randomMessage);
+ assert.equal(element._getDelta([], messages, false), 5);
+ assert.equal(element._getDelta([], messages, true), 5);
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- return commentApiWrapper.loadComments();
- });
+ messages = _.times(4, randomMessage)
+ .concat(_.times(2, randomAutomated))
+ .concat(_.times(3, randomMessage));
- teardown(() => {
- sandbox.restore();
- });
+ const dummyArr = _.times(2, randomMessage);
+ assert.equal(element._getDelta(dummyArr, messages, false), 5);
+ assert.equal(element._getDelta(dummyArr, messages, true), 7);
+ });
- test('hide autogenerated button is not hidden', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('#automatedMessageToggle[hidden]'));
- });
+ test('_getHumanMessages', () => {
+ assert.equal(
+ element._getHumanMessages(_.times(5, randomAutomated)).length, 0);
+ assert.equal(
+ element._getHumanMessages(_.times(5, randomMessage)).length, 5);
- test('autogenerated messages are not hidden initially', () => {
- const allHiddenMessageEls = getHiddenMessages();
+ let messages = _.shuffle(_.times(5, randomMessage)
+ .concat(_.times(5, randomAutomated)));
+ messages = element._getHumanMessages(messages);
+ assert.equal(messages.length, 5);
+ assert.isFalse(element._hasAutomatedMessages(messages));
+ });
- // There are no hidden messages.
- assert.isFalse(!!allHiddenMessageEls.length);
- });
-
- test('autogenerated messages hidden after comments only toggle', () => {
- let allHiddenMessageEls = getHiddenMessages();
-
- element._hideAutomated = false;
- MockInteractions.tap(element.$.automatedMessageToggle);
- flushAsynchronousOperations();
- const allMessageEls = getMessages();
- allHiddenMessageEls = getHiddenMessages();
-
- // Autogenerated messages are now hidden.
- assert.equal(allHiddenMessageEls.length, allMessageEls.length);
- });
-
- test('autogenerated messages not hidden after comments only toggle',
- () => {
- let allHiddenMessageEls = getHiddenMessages();
-
- element._hideAutomated = true;
- MockInteractions.tap(element.$.automatedMessageToggle);
- allHiddenMessageEls = getHiddenMessages();
-
- // Autogenerated messages are now hidden.
- assert.isFalse(!!allHiddenMessageEls.length);
+ test('initially show only 20 messages', () => {
+ sandbox.stub(element.$.reporting, 'reportInteraction',
+ (eventName, details) => {
+ assert.equal(typeof(eventName), 'string');
+ if (details) {
+ assert.equal(typeof(details), 'object');
+ }
});
+ const messages = Array.from(Array(23).keys())
+ .map(() => {
+ return {};
+ });
+ element._processedMessagesChanged(messages);
- test('_getDelta', () => {
- let messages = [randomMessage()];
- assert.equal(element._getDelta([], messages, false), 1);
- assert.equal(element._getDelta([], messages, true), 1);
+ assert.equal(element._visibleMessages.length, 20);
+ });
- messages = _.times(7, randomMessage);
- assert.equal(element._getDelta([], messages, false), 5);
- assert.equal(element._getDelta([], messages, true), 5);
+ test('_computeLabelExtremes', () => {
+ const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
- messages = _.times(4, randomMessage)
- .concat(_.times(2, randomAutomated))
- .concat(_.times(3, randomMessage));
+ element.labels = null;
+ assert.isTrue(computeSpy.calledOnce);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- const dummyArr = _.times(2, randomMessage);
- assert.equal(element._getDelta(dummyArr, messages, false), 5);
- assert.equal(element._getDelta(dummyArr, messages, true), 7);
- });
+ element.labels = {};
+ assert.isTrue(computeSpy.calledTwice);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- test('_getHumanMessages', () => {
- assert.equal(
- element._getHumanMessages(_.times(5, randomAutomated)).length, 0);
- assert.equal(
- element._getHumanMessages(_.times(5, randomMessage)).length, 5);
+ element.labels = {'my-label': {}};
+ assert.isTrue(computeSpy.calledThrice);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- let messages = _.shuffle(_.times(5, randomMessage)
- .concat(_.times(5, randomAutomated)));
- messages = element._getHumanMessages(messages);
- assert.equal(messages.length, 5);
- assert.isFalse(element._hasAutomatedMessages(messages));
- });
+ element.labels = {'my-label': {values: {}}};
+ assert.equal(computeSpy.callCount, 4);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {});
- test('initially show only 20 messages', () => {
- sandbox.stub(element.$.reporting, 'reportInteraction',
- (eventName, details) => {
- assert.equal(typeof(eventName), 'string');
- if (details) {
- assert.equal(typeof(details), 'object');
- }
- });
- const messages = Array.from(Array(23).keys())
- .map(() => {
- return {};
- });
- element._processedMessagesChanged(messages);
+ element.labels = {'my-label': {values: {'-12': {}}}};
+ assert.equal(computeSpy.callCount, 5);
+ assert.deepEqual(computeSpy.lastCall.returnValue,
+ {'my-label': {min: -12, max: -12}});
- assert.equal(element._visibleMessages.length, 20);
- });
+ element.labels = {
+ 'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}},
+ };
+ assert.equal(computeSpy.callCount, 6);
+ assert.deepEqual(computeSpy.lastCall.returnValue,
+ {'my-label': {min: -2, max: 2}});
- test('_computeLabelExtremes', () => {
- const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
-
- element.labels = null;
- assert.isTrue(computeSpy.calledOnce);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {};
- assert.isTrue(computeSpy.calledTwice);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {'my-label': {}};
- assert.isTrue(computeSpy.calledThrice);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {'my-label': {values: {}}};
- assert.equal(computeSpy.callCount, 4);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
-
- element.labels = {'my-label': {values: {'-12': {}}}};
- assert.equal(computeSpy.callCount, 5);
- assert.deepEqual(computeSpy.lastCall.returnValue,
- {'my-label': {min: -12, max: -12}});
-
- element.labels = {
- 'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}},
- };
- assert.equal(computeSpy.callCount, 6);
- assert.deepEqual(computeSpy.lastCall.returnValue,
- {'my-label': {min: -2, max: 2}});
-
- element.labels = {
- 'my-label': {values: {'-12': {}}},
- 'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}},
- };
- assert.equal(computeSpy.callCount, 7);
- assert.deepEqual(computeSpy.lastCall.returnValue, {
- 'my-label': {min: -12, max: -12},
- 'other-label': {min: -1, max: 1},
- });
+ element.labels = {
+ 'my-label': {values: {'-12': {}}},
+ 'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}},
+ };
+ assert.equal(computeSpy.callCount, 7);
+ assert.deepEqual(computeSpy.lastCall.returnValue, {
+ 'my-label': {min: -12, max: -12},
+ 'other-label': {min: -1, max: 1},
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index d4a2398..c4af481 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -14,384 +14,397 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import {flush} 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-related-changes-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrRelatedChangesList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-related-changes-list'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when a new section is loaded so that the change view can determine
+ * a show more button is needed, sometimes before all the sections finish
+ * loading.
+ *
+ * @event new-section-loaded
*/
- class GrRelatedChangesList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-related-changes-list'; }
- /**
- * Fired when a new section is loaded so that the change view can determine
- * a show more button is needed, sometimes before all the sections finish
- * loading.
- *
- * @event new-section-loaded
- */
- static get properties() {
- return {
- change: Object,
- hasParent: {
- type: Boolean,
- notify: true,
- value: false,
- },
- patchNum: String,
- parentChange: Object,
- hidden: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- loading: {
- type: Boolean,
- notify: true,
- },
- mergeable: Boolean,
- _connectedRevisions: {
- type: Array,
- computed: '_computeConnectedRevisions(change, patchNum, ' +
- '_relatedResponse.changes)',
- },
- /** @type {?} */
- _relatedResponse: {
- type: Object,
- value() { return {changes: []}; },
- },
- /** @type {?} */
- _submittedTogether: {
- type: Object,
- value() { return {changes: []}; },
- },
- _conflicts: {
- type: Array,
- value() { return []; },
- },
- _cherryPicks: {
- type: Array,
- value() { return []; },
- },
- _sameTopic: {
- type: Array,
- value() { return []; },
- },
- };
- }
-
- static get observers() {
- return [
- '_resultsChanged(_relatedResponse, _submittedTogether, ' +
- '_conflicts, _cherryPicks, _sameTopic)',
- ];
- }
-
- clear() {
- this.loading = true;
- this.hidden = true;
-
- this._relatedResponse = {changes: []};
- this._submittedTogether = {changes: []};
- this._conflicts = [];
- this._cherryPicks = [];
- this._sameTopic = [];
- }
-
- reload() {
- if (!this.change || !this.patchNum) {
- return Promise.resolve();
- }
- this.loading = true;
- const promises = [
- this._getRelatedChanges().then(response => {
- this._relatedResponse = response;
- this._fireReloadEvent();
- this.hasParent = this._calculateHasParent(this.change.change_id,
- response.changes);
- }),
- this._getSubmittedTogether().then(response => {
- this._submittedTogether = response;
- this._fireReloadEvent();
- }),
- this._getCherryPicks().then(response => {
- this._cherryPicks = response;
- this._fireReloadEvent();
- }),
- ];
-
- // Get conflicts if change is open and is mergeable.
- if (this.changeIsOpen(this.change) && this.mergeable) {
- promises.push(this._getConflicts().then(response => {
- // Because the server doesn't always return a response and the
- // template expects an array, always return an array.
- this._conflicts = response ? response : [];
- this._fireReloadEvent();
- }));
- }
-
- promises.push(this._getServerConfig().then(config => {
- if (this.change.topic && !config.change.submit_whole_topic) {
- return this._getChangesWithSameTopic().then(response => {
- this._sameTopic = response;
- });
- } else {
- this._sameTopic = [];
- }
- return this._sameTopic;
- }));
-
- return Promise.all(promises).then(() => {
- this.loading = false;
- });
- }
-
- _fireReloadEvent() {
- // The listener on the change computes height of the related changes
- // section, so they have to be rendered first, and inside a dom-repeat,
- // that requires a flush.
- Polymer.dom.flush();
- this.dispatchEvent(new CustomEvent('new-section-loaded'));
- }
-
- /**
- * Determines whether or not the given change has a parent change. If there
- * is a relation chain, and the change id is not the last item of the
- * relation chain, there is a parent.
- *
- * @param {number} currentChangeId
- * @param {!Array} relatedChanges
- * @return {boolean}
- */
- _calculateHasParent(currentChangeId, relatedChanges) {
- return relatedChanges.length > 0 &&
- relatedChanges[relatedChanges.length - 1].change_id !==
- currentChangeId;
- }
-
- _getRelatedChanges() {
- return this.$.restAPI.getRelatedChanges(this.change._number,
- this.patchNum);
- }
-
- _getSubmittedTogether() {
- return this.$.restAPI.getChangesSubmittedTogether(this.change._number);
- }
-
- _getServerConfig() {
- return this.$.restAPI.getConfig();
- }
-
- _getConflicts() {
- return this.$.restAPI.getChangeConflicts(this.change._number);
- }
-
- _getCherryPicks() {
- return this.$.restAPI.getChangeCherryPicks(this.change.project,
- this.change.change_id, this.change._number);
- }
-
- _getChangesWithSameTopic() {
- return this.$.restAPI.getChangesWithSameTopic(this.change.topic,
- this.change._number);
- }
-
- /**
- * @param {number} changeNum
- * @param {string} project
- * @param {number=} opt_patchNum
- * @return {string}
- */
- _computeChangeURL(changeNum, project, opt_patchNum) {
- return Gerrit.Nav.getUrlForChangeById(changeNum, project, opt_patchNum);
- }
-
- _computeChangeContainerClass(currentChange, relatedChange) {
- const classes = ['changeContainer'];
- if ([relatedChange, currentChange].some(arg => arg === undefined)) {
- return classes;
- }
- if (this._changesEqual(relatedChange, currentChange)) {
- classes.push('thisChange');
- }
- return classes.join(' ');
- }
-
- /**
- * Do the given objects describe the same change? Compares the changes by
- * their numbers.
- *
- * @see /Documentation/rest-api-changes.html#change-info
- * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
- * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo
- * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo
- * @return {boolean}
- */
- _changesEqual(a, b) {
- const aNum = this._getChangeNumber(a);
- const bNum = this._getChangeNumber(b);
- return aNum === bNum;
- }
-
- /**
- * Get the change number from either a ChangeInfo (such as those included in
- * SubmittedTogetherInfo responses) or get the change number from a
- * RelatedChangeAndCommitInfo (such as those included in a
- * RelatedChangesInfo response).
- *
- * @see /Documentation/rest-api-changes.html#change-info
- * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
- *
- * @param {!Object} change Either a ChangeInfo or a
- * RelatedChangeAndCommitInfo object.
- * @return {number}
- */
- _getChangeNumber(change) {
- // Default to 0 if change property is not defined.
- if (!change) return 0;
-
- if (change.hasOwnProperty('_change_number')) {
- return change._change_number;
- }
- return change._number;
- }
-
- _computeLinkClass(change) {
- const statuses = [];
- if (change.status == this.ChangeStatus.ABANDONED) {
- statuses.push('strikethrough');
- }
- if (change.submittable) {
- statuses.push('submittable');
- }
- return statuses.join(' ');
- }
-
- _computeChangeStatusClass(change) {
- const classes = ['status'];
- if (change._revision_number != change._current_revision_number) {
- classes.push('notCurrent');
- } else if (this._isIndirectAncestor(change)) {
- classes.push('indirectAncestor');
- } else if (change.submittable) {
- classes.push('submittable');
- } else if (change.status == this.ChangeStatus.NEW) {
- classes.push('hidden');
- }
- return classes.join(' ');
- }
-
- _computeChangeStatus(change) {
- switch (change.status) {
- case this.ChangeStatus.MERGED:
- return 'Merged';
- case this.ChangeStatus.ABANDONED:
- return 'Abandoned';
- }
- if (change._revision_number != change._current_revision_number) {
- return 'Not current';
- } else if (this._isIndirectAncestor(change)) {
- return 'Indirect ancestor';
- } else if (change.submittable) {
- return 'Submittable';
- }
- return '';
- }
-
- _resultsChanged(related, submittedTogether, conflicts,
- cherryPicks, sameTopic) {
- // Polymer 2: check for undefined
- if ([
- related,
- submittedTogether,
- conflicts,
- cherryPicks,
- sameTopic,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- const results = [
- related && related.changes,
- submittedTogether && submittedTogether.changes,
- conflicts,
- cherryPicks,
- sameTopic,
- ];
- for (let i = 0; i < results.length; i++) {
- if (results[i] && results[i].length > 0) {
- this.hidden = false;
- this.fire('update', null, {bubbles: false});
- return;
- }
- }
- this.hidden = true;
- }
-
- _isIndirectAncestor(change) {
- return !this._connectedRevisions.includes(change.commit.commit);
- }
-
- _computeConnectedRevisions(change, patchNum, relatedChanges) {
- // Polymer 2: check for undefined
- if ([change, patchNum, relatedChanges].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const connected = [];
- let changeRevision;
- if (!change) { return []; }
- for (const rev in change.revisions) {
- if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
- changeRevision = rev;
- }
- }
- const commits = relatedChanges.map(c => c.commit);
- let pos = commits.length - 1;
-
- while (pos >= 0) {
- const commit = commits[pos].commit;
- connected.push(commit);
- if (commit == changeRevision) {
- break;
- }
- pos--;
- }
- while (pos >= 0) {
- for (let i = 0; i < commits[pos].parents.length; i++) {
- if (connected.includes(commits[pos].parents[i].commit)) {
- connected.push(commits[pos].commit);
- break;
- }
- }
- --pos;
- }
- return connected;
- }
-
- _computeSubmittedTogetherClass(submittedTogether) {
- if (!submittedTogether || (
- submittedTogether.changes.length === 0 &&
- !submittedTogether.non_visible_changes)) {
- return 'hidden';
- }
- return '';
- }
-
- _computeNonVisibleChangesNote(n) {
- const noun = n === 1 ? 'change' : 'changes';
- return `(+ ${n} non-visible ${noun})`;
- }
+ static get properties() {
+ return {
+ change: Object,
+ hasParent: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+ patchNum: String,
+ parentChange: Object,
+ hidden: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ loading: {
+ type: Boolean,
+ notify: true,
+ },
+ mergeable: Boolean,
+ _connectedRevisions: {
+ type: Array,
+ computed: '_computeConnectedRevisions(change, patchNum, ' +
+ '_relatedResponse.changes)',
+ },
+ /** @type {?} */
+ _relatedResponse: {
+ type: Object,
+ value() { return {changes: []}; },
+ },
+ /** @type {?} */
+ _submittedTogether: {
+ type: Object,
+ value() { return {changes: []}; },
+ },
+ _conflicts: {
+ type: Array,
+ value() { return []; },
+ },
+ _cherryPicks: {
+ type: Array,
+ value() { return []; },
+ },
+ _sameTopic: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrRelatedChangesList.is, GrRelatedChangesList);
-})();
+ static get observers() {
+ return [
+ '_resultsChanged(_relatedResponse, _submittedTogether, ' +
+ '_conflicts, _cherryPicks, _sameTopic)',
+ ];
+ }
+
+ clear() {
+ this.loading = true;
+ this.hidden = true;
+
+ this._relatedResponse = {changes: []};
+ this._submittedTogether = {changes: []};
+ this._conflicts = [];
+ this._cherryPicks = [];
+ this._sameTopic = [];
+ }
+
+ reload() {
+ if (!this.change || !this.patchNum) {
+ return Promise.resolve();
+ }
+ this.loading = true;
+ const promises = [
+ this._getRelatedChanges().then(response => {
+ this._relatedResponse = response;
+ this._fireReloadEvent();
+ this.hasParent = this._calculateHasParent(this.change.change_id,
+ response.changes);
+ }),
+ this._getSubmittedTogether().then(response => {
+ this._submittedTogether = response;
+ this._fireReloadEvent();
+ }),
+ this._getCherryPicks().then(response => {
+ this._cherryPicks = response;
+ this._fireReloadEvent();
+ }),
+ ];
+
+ // Get conflicts if change is open and is mergeable.
+ if (this.changeIsOpen(this.change) && this.mergeable) {
+ promises.push(this._getConflicts().then(response => {
+ // Because the server doesn't always return a response and the
+ // template expects an array, always return an array.
+ this._conflicts = response ? response : [];
+ this._fireReloadEvent();
+ }));
+ }
+
+ promises.push(this._getServerConfig().then(config => {
+ if (this.change.topic && !config.change.submit_whole_topic) {
+ return this._getChangesWithSameTopic().then(response => {
+ this._sameTopic = response;
+ });
+ } else {
+ this._sameTopic = [];
+ }
+ return this._sameTopic;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this.loading = false;
+ });
+ }
+
+ _fireReloadEvent() {
+ // The listener on the change computes height of the related changes
+ // section, so they have to be rendered first, and inside a dom-repeat,
+ // that requires a flush.
+ flush();
+ this.dispatchEvent(new CustomEvent('new-section-loaded'));
+ }
+
+ /**
+ * Determines whether or not the given change has a parent change. If there
+ * is a relation chain, and the change id is not the last item of the
+ * relation chain, there is a parent.
+ *
+ * @param {number} currentChangeId
+ * @param {!Array} relatedChanges
+ * @return {boolean}
+ */
+ _calculateHasParent(currentChangeId, relatedChanges) {
+ return relatedChanges.length > 0 &&
+ relatedChanges[relatedChanges.length - 1].change_id !==
+ currentChangeId;
+ }
+
+ _getRelatedChanges() {
+ return this.$.restAPI.getRelatedChanges(this.change._number,
+ this.patchNum);
+ }
+
+ _getSubmittedTogether() {
+ return this.$.restAPI.getChangesSubmittedTogether(this.change._number);
+ }
+
+ _getServerConfig() {
+ return this.$.restAPI.getConfig();
+ }
+
+ _getConflicts() {
+ return this.$.restAPI.getChangeConflicts(this.change._number);
+ }
+
+ _getCherryPicks() {
+ return this.$.restAPI.getChangeCherryPicks(this.change.project,
+ this.change.change_id, this.change._number);
+ }
+
+ _getChangesWithSameTopic() {
+ return this.$.restAPI.getChangesWithSameTopic(this.change.topic,
+ this.change._number);
+ }
+
+ /**
+ * @param {number} changeNum
+ * @param {string} project
+ * @param {number=} opt_patchNum
+ * @return {string}
+ */
+ _computeChangeURL(changeNum, project, opt_patchNum) {
+ return Gerrit.Nav.getUrlForChangeById(changeNum, project, opt_patchNum);
+ }
+
+ _computeChangeContainerClass(currentChange, relatedChange) {
+ const classes = ['changeContainer'];
+ if ([relatedChange, currentChange].some(arg => arg === undefined)) {
+ return classes;
+ }
+ if (this._changesEqual(relatedChange, currentChange)) {
+ classes.push('thisChange');
+ }
+ return classes.join(' ');
+ }
+
+ /**
+ * Do the given objects describe the same change? Compares the changes by
+ * their numbers.
+ *
+ * @see /Documentation/rest-api-changes.html#change-info
+ * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
+ * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo
+ * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo
+ * @return {boolean}
+ */
+ _changesEqual(a, b) {
+ const aNum = this._getChangeNumber(a);
+ const bNum = this._getChangeNumber(b);
+ return aNum === bNum;
+ }
+
+ /**
+ * Get the change number from either a ChangeInfo (such as those included in
+ * SubmittedTogetherInfo responses) or get the change number from a
+ * RelatedChangeAndCommitInfo (such as those included in a
+ * RelatedChangesInfo response).
+ *
+ * @see /Documentation/rest-api-changes.html#change-info
+ * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
+ *
+ * @param {!Object} change Either a ChangeInfo or a
+ * RelatedChangeAndCommitInfo object.
+ * @return {number}
+ */
+ _getChangeNumber(change) {
+ // Default to 0 if change property is not defined.
+ if (!change) return 0;
+
+ if (change.hasOwnProperty('_change_number')) {
+ return change._change_number;
+ }
+ return change._number;
+ }
+
+ _computeLinkClass(change) {
+ const statuses = [];
+ if (change.status == this.ChangeStatus.ABANDONED) {
+ statuses.push('strikethrough');
+ }
+ if (change.submittable) {
+ statuses.push('submittable');
+ }
+ return statuses.join(' ');
+ }
+
+ _computeChangeStatusClass(change) {
+ const classes = ['status'];
+ if (change._revision_number != change._current_revision_number) {
+ classes.push('notCurrent');
+ } else if (this._isIndirectAncestor(change)) {
+ classes.push('indirectAncestor');
+ } else if (change.submittable) {
+ classes.push('submittable');
+ } else if (change.status == this.ChangeStatus.NEW) {
+ classes.push('hidden');
+ }
+ return classes.join(' ');
+ }
+
+ _computeChangeStatus(change) {
+ switch (change.status) {
+ case this.ChangeStatus.MERGED:
+ return 'Merged';
+ case this.ChangeStatus.ABANDONED:
+ return 'Abandoned';
+ }
+ if (change._revision_number != change._current_revision_number) {
+ return 'Not current';
+ } else if (this._isIndirectAncestor(change)) {
+ return 'Indirect ancestor';
+ } else if (change.submittable) {
+ return 'Submittable';
+ }
+ return '';
+ }
+
+ _resultsChanged(related, submittedTogether, conflicts,
+ cherryPicks, sameTopic) {
+ // Polymer 2: check for undefined
+ if ([
+ related,
+ submittedTogether,
+ conflicts,
+ cherryPicks,
+ sameTopic,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ const results = [
+ related && related.changes,
+ submittedTogether && submittedTogether.changes,
+ conflicts,
+ cherryPicks,
+ sameTopic,
+ ];
+ for (let i = 0; i < results.length; i++) {
+ if (results[i] && results[i].length > 0) {
+ this.hidden = false;
+ this.fire('update', null, {bubbles: false});
+ return;
+ }
+ }
+ this.hidden = true;
+ }
+
+ _isIndirectAncestor(change) {
+ return !this._connectedRevisions.includes(change.commit.commit);
+ }
+
+ _computeConnectedRevisions(change, patchNum, relatedChanges) {
+ // Polymer 2: check for undefined
+ if ([change, patchNum, relatedChanges].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const connected = [];
+ let changeRevision;
+ if (!change) { return []; }
+ for (const rev in change.revisions) {
+ if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
+ changeRevision = rev;
+ }
+ }
+ const commits = relatedChanges.map(c => c.commit);
+ let pos = commits.length - 1;
+
+ while (pos >= 0) {
+ const commit = commits[pos].commit;
+ connected.push(commit);
+ if (commit == changeRevision) {
+ break;
+ }
+ pos--;
+ }
+ while (pos >= 0) {
+ for (let i = 0; i < commits[pos].parents.length; i++) {
+ if (connected.includes(commits[pos].parents[i].commit)) {
+ connected.push(commits[pos].commit);
+ break;
+ }
+ }
+ --pos;
+ }
+ return connected;
+ }
+
+ _computeSubmittedTogetherClass(submittedTogether) {
+ if (!submittedTogether || (
+ submittedTogether.changes.length === 0 &&
+ !submittedTogether.non_visible_changes)) {
+ return 'hidden';
+ }
+ return '';
+ }
+
+ _computeNonVisibleChangesNote(n) {
+ const noun = n === 1 ? 'change' : 'changes';
+ return `(+ ${n} non-visible ${noun})`;
+ }
+}
+
+customElements.define(GrRelatedChangesList.is, GrRelatedChangesList);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
index 696ffdf..1d8551d 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
@@ -1,30 +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/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-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="../../../styles/shared-styles.html">
-
-<dom-module id="gr-related-changes-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -104,39 +96,27 @@
}
</style>
<div>
- <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
+ <section class="relatedChanges" hidden\$="[[!_relatedResponse.changes.length]]" hidden="">
<h4>Relation chain</h4>
- <template
- is="dom-repeat"
- items="[[_relatedResponse.changes]]"
- as="related">
- <div class$="rightIndent [[_computeChangeContainerClass(change, related)]]">
- <a href$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]"
- class$="[[_computeLinkClass(related)]]"
- title$="[[related.commit.subject]]">
+ <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="related">
+ <div class\$="rightIndent [[_computeChangeContainerClass(change, related)]]">
+ <a href\$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]" class\$="[[_computeLinkClass(related)]]" title\$="[[related.commit.subject]]">
[[related.commit.subject]]
</a>
- <span class$="[[_computeChangeStatusClass(related)]]">
+ <span class\$="[[_computeChangeStatusClass(related)]]">
([[_computeChangeStatus(related)]])
</span>
</div>
</template>
</section>
- <section
- id="submittedTogether"
- class$="[[_computeSubmittedTogetherClass(_submittedTogether)]]">
+ <section id="submittedTogether" class\$="[[_computeSubmittedTogetherClass(_submittedTogether)]]">
<h4>Submitted together</h4>
<template is="dom-repeat" items="[[_submittedTogether.changes]]" as="related">
- <div class$="[[_computeChangeContainerClass(change, related)]]">
- <a href$="[[_computeChangeURL(related._number, related.project)]]"
- class$="[[_computeLinkClass(related)]]"
- title$="[[related.project]]: [[related.branch]]: [[related.subject]]">
+ <div class\$="[[_computeChangeContainerClass(change, related)]]">
+ <a href\$="[[_computeChangeURL(related._number, related.project)]]" class\$="[[_computeLinkClass(related)]]" title\$="[[related.project]]: [[related.branch]]: [[related.subject]]">
[[related.project]]: [[related.branch]]: [[related.subject]]
</a>
- <span
- tabindex="-1"
- title="Submittable"
- class$="submittableCheck [[_computeLinkClass(related)]]">✓</span>
+ <span tabindex="-1" title="Submittable" class\$="submittableCheck [[_computeLinkClass(related)]]">✓</span>
</div>
</template>
<template is="dom-if" if="[[_submittedTogether.non_visible_changes]]">
@@ -145,45 +125,37 @@
</div>
</template>
</section>
- <section hidden$="[[!_sameTopic.length]]" hidden>
+ <section hidden\$="[[!_sameTopic.length]]" hidden="">
<h4>Same topic</h4>
<template is="dom-repeat" items="[[_sameTopic]]" as="change">
<div>
- <a href$="[[_computeChangeURL(change._number, change.project)]]"
- class$="[[_computeLinkClass(change)]]"
- title$="[[change.project]]: [[change.branch]]: [[change.subject]]">
+ <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.project]]: [[change.branch]]: [[change.subject]]">
[[change.project]]: [[change.branch]]: [[change.subject]]
</a>
</div>
</template>
</section>
- <section hidden$="[[!_conflicts.length]]" hidden>
+ <section hidden\$="[[!_conflicts.length]]" hidden="">
<h4>Merge conflicts</h4>
<template is="dom-repeat" items="[[_conflicts]]" as="change">
<div>
- <a href$="[[_computeChangeURL(change._number, change.project)]]"
- class$="[[_computeLinkClass(change)]]"
- title$="[[change.subject]]">
+ <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.subject]]">
[[change.subject]]
</a>
</div>
</template>
</section>
- <section hidden$="[[!_cherryPicks.length]]" hidden>
+ <section hidden\$="[[!_cherryPicks.length]]" hidden="">
<h4>Cherry picks</h4>
<template is="dom-repeat" items="[[_cherryPicks]]" as="change">
<div>
- <a href$="[[_computeChangeURL(change._number, change.project)]]"
- class$="[[_computeLinkClass(change)]]"
- title$="[[change.branch]]: [[change.subject]]">
+ <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.branch]]: [[change.subject]]">
[[change.branch]]: [[change.subject]]
</a>
</div>
</template>
</section>
</div>
- <div hidden$="[[!loading]]">Loading...</div>
+ <div hidden\$="[[!loading]]">Loading...</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-related-changes-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index 9b8ebed..43037af 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_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-related-changes-list</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-related-changes-list.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-related-changes-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-related-changes-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,238 +40,260 @@
</template>
</test-fixture>
-<script>
- suite('gr-related-changes-list tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-related-changes-list.js';
+suite('gr-related-changes-list tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('connected revisions', () => {
+ const change = {
+ revisions: {
+ 'e3c6d60783bfdec9ebae7dcfec4662360433449e': {
+ _number: 1,
+ },
+ '26e5e4c9c7ae31cbd876271cca281ce22b413997': {
+ _number: 2,
+ },
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': {
+ _number: 7,
+ },
+ 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': {
+ _number: 5,
+ },
+ 'd6bcee67570859ccb684873a85cf50b1f0e96fda': {
+ _number: 6,
+ },
+ 'cc960918a7f90388f4a9e05753d0f7b90ad44546': {
+ _number: 3,
+ },
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': {
+ _number: 4,
+ },
+ },
+ };
+ let patchNum = 7;
+ let relatedChanges = [
+ {
+ commit: {
+ commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ parents: [
+ {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ parents: [
+ {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ parents: [
+ {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ parents: [
+ {
+ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ parents: [
+ {
+ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ parents: [
+ {
+ commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75',
+ },
+ ],
+ },
+ },
+ ];
+
+ let connectedChanges =
+ element._computeConnectedRevisions(change, patchNum, relatedChanges);
+ assert.deepEqual(connectedChanges, [
+ '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ ]);
+
+ patchNum = 4;
+ relatedChanges = [
+ {
+ commit: {
+ commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
+ parents: [
+ {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ parents: [
+ {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ parents: [
+ {
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+ parents: [
+ {
+ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ parents: [
+ {
+ commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
+ },
+ ],
+ },
+ },
+ {
+ commit: {
+ commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
+ parents: [
+ {
+ commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c',
+ },
+ ],
+ },
+ },
+ ];
+
+ connectedChanges =
+ element._computeConnectedRevisions(change, patchNum, relatedChanges);
+ assert.deepEqual(connectedChanges, [
+ 'af815dac54318826b7f1fa468acc76349ffc588e',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
+ ]);
+ });
+
+ test('_computeChangeContainerClass', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _change_number: 1};
+ const change3 = {change_id: 123, _number: 2};
+
+ assert.notEqual(element._computeChangeContainerClass(
+ change1, change1).indexOf('thisChange'), -1);
+ assert.equal(element._computeChangeContainerClass(
+ change1, change2).indexOf('thisChange'), -1);
+ assert.equal(element._computeChangeContainerClass(
+ change1, change3).indexOf('thisChange'), -1);
+ });
+
+ test('_changesEqual', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _number: 1};
+ const change3 = {change_id: 123, _number: 2};
+ const change4 = {change_id: 123, _change_number: 1};
+
+ assert.isTrue(element._changesEqual(change1, change1));
+ assert.isFalse(element._changesEqual(change1, change2));
+ assert.isFalse(element._changesEqual(change1, change3));
+ assert.isTrue(element._changesEqual(change2, change4));
+ });
+
+ test('_getChangeNumber', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _change_number: 1};
+ assert.equal(element._getChangeNumber(change1), 0);
+ assert.equal(element._getChangeNumber(change2), 1);
+ });
+
+ test('event for section loaded fires for each section ', () => {
+ const loadedStub = sandbox.stub();
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'NEW',
+ };
+ element.mergeable = true;
+ element.addEventListener('new-section-loaded', loadedStub);
+ sandbox.stub(element, '_getRelatedChanges')
+ .returns(Promise.resolve({changes: []}));
+ sandbox.stub(element, '_getSubmittedTogether')
+ .returns(Promise.resolve());
+ sandbox.stub(element, '_getCherryPicks')
+ .returns(Promise.resolve());
+ sandbox.stub(element, '_getConflicts')
+ .returns(Promise.resolve());
+
+ return element.reload().then(() => {
+ assert.equal(loadedStub.callCount, 4);
+ });
+ });
+
+ suite('_getConflicts resolves undefined', () => {
let element;
- let sandbox;
setup(() => {
element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
- teardown(() => {
- sandbox.restore();
- });
-
- test('connected revisions', () => {
- const change = {
- revisions: {
- 'e3c6d60783bfdec9ebae7dcfec4662360433449e': {
- _number: 1,
- },
- '26e5e4c9c7ae31cbd876271cca281ce22b413997': {
- _number: 2,
- },
- 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': {
- _number: 7,
- },
- 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': {
- _number: 5,
- },
- 'd6bcee67570859ccb684873a85cf50b1f0e96fda': {
- _number: 6,
- },
- 'cc960918a7f90388f4a9e05753d0f7b90ad44546': {
- _number: 3,
- },
- '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': {
- _number: 4,
- },
- },
- };
- let patchNum = 7;
- let relatedChanges = [
- {
- commit: {
- commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
- parents: [
- {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- },
- ],
- },
- },
- {
- commit: {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- parents: [
- {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- },
- ],
- },
- },
- {
- commit: {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- parents: [
- {
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- parents: [
- {
- commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- parents: [
- {
- commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
- },
- ],
- },
- },
- {
- commit: {
- commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
- parents: [
- {
- commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75',
- },
- ],
- },
- },
- ];
-
- let connectedChanges =
- element._computeConnectedRevisions(change, patchNum, relatedChanges);
- assert.deepEqual(connectedChanges, [
- '613bc4f81741a559c6667ac08d71dcc3348f73ce',
- 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
- 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
- ]);
-
- patchNum = 4;
- relatedChanges = [
- {
- commit: {
- commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
- parents: [
- {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- },
- ],
- },
- },
- {
- commit: {
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
- parents: [
- {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- },
- ],
- },
- },
- {
- commit: {
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
- parents: [
- {
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
- parents: [
- {
- commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- },
- ],
- },
- },
- {
- commit: {
- commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- parents: [
- {
- commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
- },
- ],
- },
- },
- {
- commit: {
- commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
- parents: [
- {
- commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c',
- },
- ],
- },
- },
- ];
-
- connectedChanges =
- element._computeConnectedRevisions(change, patchNum, relatedChanges);
- assert.deepEqual(connectedChanges, [
- 'af815dac54318826b7f1fa468acc76349ffc588e',
- '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
- 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
- ]);
- });
-
- test('_computeChangeContainerClass', () => {
- const change1 = {change_id: 123, _number: 0};
- const change2 = {change_id: 456, _change_number: 1};
- const change3 = {change_id: 123, _number: 2};
-
- assert.notEqual(element._computeChangeContainerClass(
- change1, change1).indexOf('thisChange'), -1);
- assert.equal(element._computeChangeContainerClass(
- change1, change2).indexOf('thisChange'), -1);
- assert.equal(element._computeChangeContainerClass(
- change1, change3).indexOf('thisChange'), -1);
- });
-
- test('_changesEqual', () => {
- const change1 = {change_id: 123, _number: 0};
- const change2 = {change_id: 456, _number: 1};
- const change3 = {change_id: 123, _number: 2};
- const change4 = {change_id: 123, _change_number: 1};
-
- assert.isTrue(element._changesEqual(change1, change1));
- assert.isFalse(element._changesEqual(change1, change2));
- assert.isFalse(element._changesEqual(change1, change3));
- assert.isTrue(element._changesEqual(change2, change4));
- });
-
- test('_getChangeNumber', () => {
- const change1 = {change_id: 123, _number: 0};
- const change2 = {change_id: 456, _change_number: 1};
- assert.equal(element._getChangeNumber(change1), 0);
- assert.equal(element._getChangeNumber(change2), 1);
- });
-
- test('event for section loaded fires for each section ', () => {
- const loadedStub = sandbox.stub();
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = true;
- element.addEventListener('new-section-loaded', loadedStub);
sandbox.stub(element, '_getRelatedChanges')
.returns(Promise.resolve({changes: []}));
sandbox.stub(element, '_getSubmittedTogether')
@@ -275,283 +302,263 @@
.returns(Promise.resolve());
sandbox.stub(element, '_getConflicts')
.returns(Promise.resolve());
-
- return element.reload().then(() => {
- assert.equal(loadedStub.callCount, 4);
- });
});
- suite('_getConflicts resolves undefined', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
-
- sandbox.stub(element, '_getRelatedChanges')
- .returns(Promise.resolve({changes: []}));
- sandbox.stub(element, '_getSubmittedTogether')
- .returns(Promise.resolve());
- sandbox.stub(element, '_getCherryPicks')
- .returns(Promise.resolve());
- sandbox.stub(element, '_getConflicts')
- .returns(Promise.resolve());
- });
-
- test('_conflicts are an empty array', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = true;
- element.reload();
- assert.equal(element._conflicts.length, 0);
- });
- });
-
- suite('get conflicts tests', () => {
- let element;
- let conflictsStub;
-
- setup(() => {
- element = fixture('basic');
-
- sandbox.stub(element, '_getRelatedChanges')
- .returns(Promise.resolve({changes: []}));
- sandbox.stub(element, '_getSubmittedTogether')
- .returns(Promise.resolve());
- sandbox.stub(element, '_getCherryPicks')
- .returns(Promise.resolve());
- conflictsStub = sandbox.stub(element, '_getConflicts')
- .returns(Promise.resolve());
- });
-
- test('request conflicts if open and mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = true;
- element.reload();
- assert.isTrue(conflictsStub.called);
- });
-
- test('does not request conflicts if closed and mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'MERGED',
- };
- element.reload();
- assert.isFalse(conflictsStub.called);
- });
-
- test('does not request conflicts if open and not mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'NEW',
- };
- element.mergeable = false;
- element.reload();
- assert.isFalse(conflictsStub.called);
- });
-
- test('doesnt request conflicts if closed and not mergeable', () => {
- element.patchNum = 7;
- element.change = {
- change_id: 123,
- status: 'MERGED',
- };
- element.mergeable = false;
- element.reload();
- assert.isFalse(conflictsStub.called);
- });
- });
-
- test('_calculateHasParent', () => {
- const changeId = 123;
- const relatedChanges = [];
-
- assert.equal(element._calculateHasParent(changeId, relatedChanges),
- false);
-
- relatedChanges.push({change_id: 123});
- assert.equal(element._calculateHasParent(changeId, relatedChanges),
- false);
-
- relatedChanges.push({change_id: 234});
- assert.equal(element._calculateHasParent(changeId, relatedChanges),
- true);
- });
-
- suite('hidden attribute and update event', () => {
- const changes = [{
- project: 'foo/bar',
- change_id: 'Ideadbeef',
- commit: {
- commit: 'deadbeef',
- parents: [{commit: 'abc123'}],
- author: {},
- subject: 'do that thing',
- },
- _change_number: 12345,
- _revision_number: 1,
- _current_revision_number: 1,
- status: 'NEW',
- }];
-
- test('clear and empties', () => {
- element._relatedResponse = {changes};
- element._submittedTogether = {changes};
- element._conflicts = changes;
- element._cherryPicks = changes;
- element._sameTopic = changes;
-
- element.hidden = false;
- element.clear();
- assert.isTrue(element.hidden);
- assert.equal(element._relatedResponse.changes.length, 0);
- assert.equal(element._submittedTogether.changes.length, 0);
- assert.equal(element._conflicts.length, 0);
- assert.equal(element._cherryPicks.length, 0);
- assert.equal(element._sameTopic.length, 0);
- });
-
- test('update fires', () => {
- const updateHandler = sandbox.stub();
- element.addEventListener('update', updateHandler);
-
- element._resultsChanged({}, {}, [], [], []);
- assert.isTrue(element.hidden);
- assert.isFalse(updateHandler.called);
-
- element._resultsChanged({}, {}, [], [], ['test']);
- assert.isFalse(element.hidden);
- assert.isTrue(updateHandler.called);
- });
-
- suite('hiding and unhiding', () => {
- test('related response', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({changes}, {}, [], [], []);
- assert.isFalse(element.hidden);
- });
-
- test('submitted together', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {changes}, [], [], []);
- assert.isFalse(element.hidden);
- });
-
- test('conflicts', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {}, changes, [], []);
- assert.isFalse(element.hidden);
- });
-
- test('cherrypicks', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {}, [], changes, []);
- assert.isFalse(element.hidden);
- });
-
- test('same topic', () => {
- assert.isTrue(element.hidden);
- element._resultsChanged({}, {}, [], [], changes);
- assert.isFalse(element.hidden);
- });
- });
- });
-
- test('_computeChangeURL uses Gerrit.Nav', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChangeById');
- element._computeChangeURL(123, 'abc/def', 12);
- assert.isTrue(getUrlStub.called);
- });
-
- suite('submitted together changes', () => {
- const change = {
- project: 'foo/bar',
- change_id: 'Ideadbeef',
- commit: {
- commit: 'deadbeef',
- parents: [{commit: 'abc123'}],
- author: {},
- subject: 'do that thing',
- },
- _change_number: 12345,
- _revision_number: 1,
- _current_revision_number: 1,
+ test('_conflicts are an empty array', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
status: 'NEW',
};
+ element.mergeable = true;
+ element.reload();
+ assert.equal(element._conflicts.length, 0);
+ });
+ });
- test('_computeSubmittedTogetherClass', () => {
- assert.strictEqual(
- element._computeSubmittedTogetherClass(undefined),
- 'hidden');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({changes: []}),
- 'hidden');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({changes: [{}]}),
- '');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 0,
- }),
- 'hidden');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [],
- non_visible_changes: 1,
- }),
- '');
- assert.strictEqual(
- element._computeSubmittedTogetherClass({
- changes: [{}],
- non_visible_changes: 1,
- }),
- '');
+ suite('get conflicts tests', () => {
+ let element;
+ let conflictsStub;
+
+ setup(() => {
+ element = fixture('basic');
+
+ sandbox.stub(element, '_getRelatedChanges')
+ .returns(Promise.resolve({changes: []}));
+ sandbox.stub(element, '_getSubmittedTogether')
+ .returns(Promise.resolve());
+ sandbox.stub(element, '_getCherryPicks')
+ .returns(Promise.resolve());
+ conflictsStub = sandbox.stub(element, '_getConflicts')
+ .returns(Promise.resolve());
+ });
+
+ test('request conflicts if open and mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'NEW',
+ };
+ element.mergeable = true;
+ element.reload();
+ assert.isTrue(conflictsStub.called);
+ });
+
+ test('does not request conflicts if closed and mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'MERGED',
+ };
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+
+ test('does not request conflicts if open and not mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'NEW',
+ };
+ element.mergeable = false;
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+
+ test('doesnt request conflicts if closed and not mergeable', () => {
+ element.patchNum = 7;
+ element.change = {
+ change_id: 123,
+ status: 'MERGED',
+ };
+ element.mergeable = false;
+ element.reload();
+ assert.isFalse(conflictsStub.called);
+ });
+ });
+
+ test('_calculateHasParent', () => {
+ const changeId = 123;
+ const relatedChanges = [];
+
+ assert.equal(element._calculateHasParent(changeId, relatedChanges),
+ false);
+
+ relatedChanges.push({change_id: 123});
+ assert.equal(element._calculateHasParent(changeId, relatedChanges),
+ false);
+
+ relatedChanges.push({change_id: 234});
+ assert.equal(element._calculateHasParent(changeId, relatedChanges),
+ true);
+ });
+
+ suite('hidden attribute and update event', () => {
+ const changes = [{
+ project: 'foo/bar',
+ change_id: 'Ideadbeef',
+ commit: {
+ commit: 'deadbeef',
+ parents: [{commit: 'abc123'}],
+ author: {},
+ subject: 'do that thing',
+ },
+ _change_number: 12345,
+ _revision_number: 1,
+ _current_revision_number: 1,
+ status: 'NEW',
+ }];
+
+ test('clear and empties', () => {
+ element._relatedResponse = {changes};
+ element._submittedTogether = {changes};
+ element._conflicts = changes;
+ element._cherryPicks = changes;
+ element._sameTopic = changes;
+
+ element.hidden = false;
+ element.clear();
+ assert.isTrue(element.hidden);
+ assert.equal(element._relatedResponse.changes.length, 0);
+ assert.equal(element._submittedTogether.changes.length, 0);
+ assert.equal(element._conflicts.length, 0);
+ assert.equal(element._cherryPicks.length, 0);
+ assert.equal(element._sameTopic.length, 0);
+ });
+
+ test('update fires', () => {
+ const updateHandler = sandbox.stub();
+ element.addEventListener('update', updateHandler);
+
+ element._resultsChanged({}, {}, [], [], []);
+ assert.isTrue(element.hidden);
+ assert.isFalse(updateHandler.called);
+
+ element._resultsChanged({}, {}, [], [], ['test']);
+ assert.isFalse(element.hidden);
+ assert.isTrue(updateHandler.called);
+ });
+
+ suite('hiding and unhiding', () => {
+ test('related response', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({changes}, {}, [], [], []);
+ assert.isFalse(element.hidden);
});
- test('no submitted together changes', () => {
- flushAsynchronousOperations();
- assert.include(element.$.submittedTogether.className, 'hidden');
+ test('submitted together', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {changes}, [], [], []);
+ assert.isFalse(element.hidden);
});
- test('no non-visible submitted together changes', () => {
- element._submittedTogether = {changes: [change]};
- flushAsynchronousOperations();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.isNull(element.shadowRoot
- .querySelector('.note'));
+ test('conflicts', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {}, changes, [], []);
+ assert.isFalse(element.hidden);
});
- test('no visible submitted together changes', () => {
- // Technically this should never happen, but worth asserting the logic.
- element._submittedTogether = {changes: [], non_visible_changes: 1};
- flushAsynchronousOperations();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.isNotNull(element.shadowRoot
- .querySelector('.note'));
- assert.strictEqual(
- element.shadowRoot
- .querySelector('.note').innerText, '(+ 1 non-visible change)');
+ test('cherrypicks', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {}, [], changes, []);
+ assert.isFalse(element.hidden);
});
- test('visible and non-visible submitted together changes', () => {
- element._submittedTogether = {changes: [change], non_visible_changes: 2};
- flushAsynchronousOperations();
- assert.notInclude(element.$.submittedTogether.className, 'hidden');
- assert.isNotNull(element.shadowRoot
- .querySelector('.note'));
- assert.strictEqual(
- element.shadowRoot
- .querySelector('.note').innerText, '(+ 2 non-visible changes)');
+ test('same topic', () => {
+ assert.isTrue(element.hidden);
+ element._resultsChanged({}, {}, [], [], changes);
+ assert.isFalse(element.hidden);
});
});
});
+
+ test('_computeChangeURL uses Gerrit.Nav', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChangeById');
+ element._computeChangeURL(123, 'abc/def', 12);
+ assert.isTrue(getUrlStub.called);
+ });
+
+ suite('submitted together changes', () => {
+ const change = {
+ project: 'foo/bar',
+ change_id: 'Ideadbeef',
+ commit: {
+ commit: 'deadbeef',
+ parents: [{commit: 'abc123'}],
+ author: {},
+ subject: 'do that thing',
+ },
+ _change_number: 12345,
+ _revision_number: 1,
+ _current_revision_number: 1,
+ status: 'NEW',
+ };
+
+ test('_computeSubmittedTogetherClass', () => {
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass(undefined),
+ 'hidden');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({changes: []}),
+ 'hidden');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({changes: [{}]}),
+ '');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({
+ changes: [],
+ non_visible_changes: 0,
+ }),
+ 'hidden');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({
+ changes: [],
+ non_visible_changes: 1,
+ }),
+ '');
+ assert.strictEqual(
+ element._computeSubmittedTogetherClass({
+ changes: [{}],
+ non_visible_changes: 1,
+ }),
+ '');
+ });
+
+ test('no submitted together changes', () => {
+ flushAsynchronousOperations();
+ assert.include(element.$.submittedTogether.className, 'hidden');
+ });
+
+ test('no non-visible submitted together changes', () => {
+ element._submittedTogether = {changes: [change]};
+ flushAsynchronousOperations();
+ assert.notInclude(element.$.submittedTogether.className, 'hidden');
+ assert.isNull(element.shadowRoot
+ .querySelector('.note'));
+ });
+
+ test('no visible submitted together changes', () => {
+ // Technically this should never happen, but worth asserting the logic.
+ element._submittedTogether = {changes: [], non_visible_changes: 1};
+ flushAsynchronousOperations();
+ assert.notInclude(element.$.submittedTogether.className, 'hidden');
+ assert.isNotNull(element.shadowRoot
+ .querySelector('.note'));
+ assert.strictEqual(
+ element.shadowRoot
+ .querySelector('.note').innerText, '(+ 1 non-visible change)');
+ });
+
+ test('visible and non-visible submitted together changes', () => {
+ element._submittedTogether = {changes: [change], non_visible_changes: 2};
+ flushAsynchronousOperations();
+ assert.notInclude(element.$.submittedTogether.className, 'hidden');
+ assert.isNotNull(element.shadowRoot
+ .querySelector('.note'));
+ assert.strictEqual(
+ element.shadowRoot
+ .querySelector('.note').innerText, '(+ 2 non-visible changes)');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index 5fd3795..7e651c5 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reply-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="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html">
-<link rel="import" href="gr-reply-dialog.html">
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="../../plugins/gr-plugin-host/gr-plugin-host.js"></script>
+<script type="module" src="./gr-reply-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../plugins/gr-plugin-host/gr-plugin-host.js';
+import './gr-reply-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -43,130 +49,134 @@
</template>
</test-fixture>
-<script>
- suite('gr-reply-dialog tests', async () => {
- await readyToTest();
- let element;
- let changeNum;
- let patchNum;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../plugins/gr-plugin-host/gr-plugin-host.js';
+import './gr-reply-dialog.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-reply-dialog tests', () => {
+ let element;
+ let changeNum;
+ let patchNum;
- let sandbox;
+ let sandbox;
- const setupElement = element => {
- element.change = {
- _number: changeNum,
- labels: {
- 'Verified': {
- values: {
- '-1': 'Fails',
- ' 0': 'No score',
- '+1': 'Verified',
- },
- default_value: 0,
+ const setupElement = element => {
+ element.change = {
+ _number: changeNum,
+ labels: {
+ 'Verified': {
+ values: {
+ '-1': 'Fails',
+ ' 0': 'No score',
+ '+1': 'Verified',
},
- 'Code-Review': {
- values: {
- '-2': 'Do not submit',
- '-1': 'I would prefer that you didn\'t submit this',
- ' 0': 'No score',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- all: [{_account_id: 42, value: 0}],
- default_value: 0,
- },
+ default_value: 0,
},
- };
- element.patchNum = patchNum;
- element.permittedLabels = {
- 'Code-Review': [
- '-1',
- ' 0',
- '+1',
- ],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
+ 'Code-Review': {
+ values: {
+ '-2': 'Do not submit',
+ '-1': 'I would prefer that you didn\'t submit this',
+ ' 0': 'No score',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ all: [{_account_id: 42, value: 0}],
+ default_value: 0,
+ },
+ },
};
+ element.patchNum = patchNum;
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- changeNum = 42;
- patchNum = 1;
+ changeNum = 42;
+ patchNum = 1;
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getAccount() { return Promise.resolve({_account_id: 42}); },
- });
-
- element = fixture('basic');
- setupElement(element);
-
- // Allow the elements created by dom-repeat to be stamped.
- flushAsynchronousOperations();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getAccount() { return Promise.resolve({_account_id: 42}); },
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
+ setupElement(element);
- test('_submit blocked when invalid email is supplied to ccs', () => {
- const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
- // Stub the below function to avoid side effects from the send promise
- // resolving.
- sandbox.stub(element, '_purgeReviewersPendingRemove');
+ // Allow the elements created by dom-repeat to be stamped.
+ flushAsynchronousOperations();
+ });
- element.$.ccs.$.entry.setText('test');
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isFalse(sendStub.called);
- flushAsynchronousOperations();
+ teardown(() => {
+ sandbox.restore();
+ });
- element.$.ccs.$.entry.setText('test@test.test');
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isTrue(sendStub.called);
- });
+ test('_submit blocked when invalid email is supplied to ccs', () => {
+ const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+ // Stub the below function to avoid side effects from the send promise
+ // resolving.
+ sandbox.stub(element, '_purgeReviewersPendingRemove');
- test('lgtm plugin', done => {
- Gerrit._testOnly_resetPlugins();
- const pluginHost = fixture('plugin-host');
- pluginHost.config = {
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html?' + Math.random(),
- window.location.href).toString(),
- ],
- },
- };
- element = fixture('basic');
- setupElement(element);
- const importSpy =
- sandbox.spy(element.shadowRoot
- .querySelector('gr-endpoint-decorator'), '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues).then(() => {
- flush(() => {
- const textarea = element.$.textarea.getNativeTextarea();
- textarea.value = 'LGTM';
- textarea.dispatchEvent(new CustomEvent(
- 'input', {bubbles: true, composed: true}));
- const labelScoreRows = Polymer.dom(element.$.labelScores.root)
- .querySelector('gr-label-score-row[name="Code-Review"]');
- const selectedBtn = Polymer.dom(labelScoreRows.root)
- .querySelector('gr-button[data-value="+1"].iron-selected');
- assert.isOk(selectedBtn);
- done();
- });
+ element.$.ccs.$.entry.setText('test');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isFalse(sendStub.called);
+ flushAsynchronousOperations();
+
+ element.$.ccs.$.entry.setText('test@test.test');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isTrue(sendStub.called);
+ });
+
+ test('lgtm plugin', done => {
+ Gerrit._testOnly_resetPlugins();
+ const pluginHost = fixture('plugin-host');
+ pluginHost.config = {
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString(),
+ ],
+ },
+ };
+ element = fixture('basic');
+ setupElement(element);
+ const importSpy =
+ sandbox.spy(element.shadowRoot
+ .querySelector('gr-endpoint-decorator'), '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues).then(() => {
+ flush(() => {
+ const textarea = element.$.textarea.getNativeTextarea();
+ textarea.value = 'LGTM';
+ textarea.dispatchEvent(new CustomEvent(
+ 'input', {bubbles: true, composed: true}));
+ const labelScoreRows = dom(element.$.labelScores.root)
+ .querySelector('gr-label-score-row[name="Code-Review"]');
+ const selectedBtn = dom(labelScoreRows.root)
+ .querySelector('gr-button[data-value="+1"].iron-selected');
+ assert.isOk(selectedBtn);
+ done();
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index b1a05f5..305505d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.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,887 +14,916 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+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/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-textarea/gr-textarea.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-formatted-text/gr-formatted-text.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-storage/gr-storage.js';
+import '../../shared/gr-account-list/gr-account-list.js';
+import '../gr-label-scores/gr-label-scores.js';
+import '../gr-thread-list/gr-thread-list.js';
+import '../../../styles/shared-styles.js';
+import '../gr-comment-list/gr-comment-list.js';
+import '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.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-reply-dialog_html.js';
- const FocusTarget = {
- ANY: 'any',
- BODY: 'body',
- CCS: 'cc',
- REVIEWERS: 'reviewers',
- };
+const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
- const ReviewerTypes = {
- REVIEWER: 'REVIEWER',
- CC: 'CC',
- };
+const FocusTarget = {
+ ANY: 'any',
+ BODY: 'body',
+ CCS: 'cc',
+ REVIEWERS: 'reviewers',
+};
- const LatestPatchState = {
- LATEST: 'latest',
- CHECKING: 'checking',
- NOT_LATEST: 'not-latest',
- };
+const ReviewerTypes = {
+ REVIEWER: 'REVIEWER',
+ CC: 'CC',
+};
- const ButtonLabels = {
- START_REVIEW: 'Start review',
- SEND: 'Send',
- };
+const LatestPatchState = {
+ LATEST: 'latest',
+ CHECKING: 'checking',
+ NOT_LATEST: 'not-latest',
+};
- const ButtonTooltips = {
- SAVE: 'Save but do not send notification or change review state',
- START_REVIEW: 'Mark as ready for review and send reply',
- SEND: 'Send reply',
- };
+const ButtonLabels = {
+ START_REVIEW: 'Start review',
+ SEND: 'Send',
+};
- const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
+const ButtonTooltips = {
+ SAVE: 'Save but do not send notification or change review state',
+ START_REVIEW: 'Mark as ready for review and send reply',
+ SEND: 'Send reply',
+};
- const SEND_REPLY_TIMING_LABEL = 'SendReply';
+const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
+
+const SEND_REPLY_TIMING_LABEL = 'SendReply';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrReplyDialog extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-reply-dialog'; }
+ /**
+ * Fired when a reply is successfully sent.
+ *
+ * @event send
+ */
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when the user presses the cancel button.
+ *
+ * @event cancel
*/
- class GrReplyDialog extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-reply-dialog'; }
+
+ /**
+ * Fired when the main textarea's value changes, which may have triggered
+ * a change in size for the dialog.
+ *
+ * @event autogrow
+ */
+
+ /**
+ * Fires to show an alert when a send is attempted on the non-latest patch.
+ *
+ * @event show-alert
+ */
+
+ /**
+ * Fires when the reply dialog believes that the server side diff drafts
+ * have been updated and need to be refreshed.
+ *
+ * @event comment-refresh
+ */
+
+ /**
+ * Fires when the state of the send button (enabled/disabled) changes.
+ *
+ * @event send-disabled-changed
+ */
+
+ constructor() {
+ super();
+ this.FocusTarget = FocusTarget;
+ }
+
+ static get properties() {
+ return {
/**
- * Fired when a reply is successfully sent.
- *
- * @event send
+ * @type {{ _number: number, removable_reviewers: Array }}
*/
-
- /**
- * Fired when the user presses the cancel button.
- *
- * @event cancel
- */
-
- /**
- * Fired when the main textarea's value changes, which may have triggered
- * a change in size for the dialog.
- *
- * @event autogrow
- */
-
- /**
- * Fires to show an alert when a send is attempted on the non-latest patch.
- *
- * @event show-alert
- */
-
- /**
- * Fires when the reply dialog believes that the server side diff drafts
- * have been updated and need to be refreshed.
- *
- * @event comment-refresh
- */
-
- /**
- * Fires when the state of the send button (enabled/disabled) changes.
- *
- * @event send-disabled-changed
- */
-
- constructor() {
- super();
- this.FocusTarget = FocusTarget;
- }
-
- static get properties() {
- return {
+ change: Object,
+ patchNum: String,
+ canBeStarted: {
+ type: Boolean,
+ value: false,
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ draft: {
+ type: String,
+ value: '',
+ observer: '_draftChanged',
+ },
+ quote: {
+ type: String,
+ value: '',
+ },
+ /** @type {!Function} */
+ filterReviewerSuggestion: {
+ type: Function,
+ value() {
+ return this._filterReviewerSuggestionGenerator(false);
+ },
+ },
+ /** @type {!Function} */
+ filterCCSuggestion: {
+ type: Function,
+ value() {
+ return this._filterReviewerSuggestionGenerator(true);
+ },
+ },
+ permittedLabels: Object,
/**
- * @type {{ _number: number, removable_reviewers: Array }}
+ * @type {{ commentlinks: Array }}
*/
- change: Object,
- patchNum: String,
- canBeStarted: {
- type: Boolean,
- value: false,
- },
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- draft: {
- type: String,
- value: '',
- observer: '_draftChanged',
- },
- quote: {
- type: String,
- value: '',
- },
- /** @type {!Function} */
- filterReviewerSuggestion: {
- type: Function,
- value() {
- return this._filterReviewerSuggestionGenerator(false);
- },
- },
- /** @type {!Function} */
- filterCCSuggestion: {
- type: Function,
- value() {
- return this._filterReviewerSuggestionGenerator(true);
- },
- },
- permittedLabels: Object,
- /**
- * @type {{ commentlinks: Array }}
- */
- projectConfig: Object,
- knownLatestState: String,
- underReview: {
- type: Boolean,
- value: true,
- },
+ projectConfig: Object,
+ knownLatestState: String,
+ underReview: {
+ type: Boolean,
+ value: true,
+ },
- _account: Object,
- _ccs: Array,
- /** @type {?Object} */
- _ccPendingConfirmation: {
- type: Object,
- observer: '_reviewerPendingConfirmationUpdated',
+ _account: Object,
+ _ccs: Array,
+ /** @type {?Object} */
+ _ccPendingConfirmation: {
+ type: Object,
+ observer: '_reviewerPendingConfirmationUpdated',
+ },
+ _messagePlaceholder: {
+ type: String,
+ computed: '_computeMessagePlaceholder(canBeStarted)',
+ },
+ _owner: Object,
+ /** @type {?} */
+ _pendingConfirmationDetails: Object,
+ _includeComments: {
+ type: Boolean,
+ value: true,
+ },
+ _reviewers: Array,
+ /** @type {?Object} */
+ _reviewerPendingConfirmation: {
+ type: Object,
+ observer: '_reviewerPendingConfirmationUpdated',
+ },
+ _previewFormatting: {
+ type: Boolean,
+ value: false,
+ observer: '_handleHeightChanged',
+ },
+ _reviewersPendingRemove: {
+ type: Object,
+ value: {
+ CC: [],
+ REVIEWER: [],
},
- _messagePlaceholder: {
- type: String,
- computed: '_computeMessagePlaceholder(canBeStarted)',
- },
- _owner: Object,
- /** @type {?} */
- _pendingConfirmationDetails: Object,
- _includeComments: {
- type: Boolean,
- value: true,
- },
- _reviewers: Array,
- /** @type {?Object} */
- _reviewerPendingConfirmation: {
- type: Object,
- observer: '_reviewerPendingConfirmationUpdated',
- },
- _previewFormatting: {
- type: Boolean,
- value: false,
- observer: '_handleHeightChanged',
- },
- _reviewersPendingRemove: {
- type: Object,
- value: {
- CC: [],
- REVIEWER: [],
- },
- },
- _sendButtonLabel: {
- type: String,
- computed: '_computeSendButtonLabel(canBeStarted)',
- },
- _savingComments: Boolean,
- _reviewersMutated: {
- type: Boolean,
- value: false,
- },
- _labelsChanged: {
- type: Boolean,
- value: false,
- },
- _saveTooltip: {
- type: String,
- value: ButtonTooltips.SAVE,
- readOnly: true,
- },
- _pluginMessage: {
- type: String,
- value: '',
- },
- _sendDisabled: {
- type: Boolean,
- computed: '_computeSendButtonDisabled(_sendButtonLabel, ' +
- 'draftCommentThreads, draft, _reviewersMutated, _labelsChanged, ' +
- '_includeComments, disabled)',
- observer: '_sendDisabledChanged',
- },
- draftCommentThreads: {
- type: Array,
- observer: '_handleHeightChanged',
- },
- };
- }
+ },
+ _sendButtonLabel: {
+ type: String,
+ computed: '_computeSendButtonLabel(canBeStarted)',
+ },
+ _savingComments: Boolean,
+ _reviewersMutated: {
+ type: Boolean,
+ value: false,
+ },
+ _labelsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _saveTooltip: {
+ type: String,
+ value: ButtonTooltips.SAVE,
+ readOnly: true,
+ },
+ _pluginMessage: {
+ type: String,
+ value: '',
+ },
+ _sendDisabled: {
+ type: Boolean,
+ computed: '_computeSendButtonDisabled(_sendButtonLabel, ' +
+ 'draftCommentThreads, draft, _reviewersMutated, _labelsChanged, ' +
+ '_includeComments, disabled)',
+ observer: '_sendDisabledChanged',
+ },
+ draftCommentThreads: {
+ type: Array,
+ observer: '_handleHeightChanged',
+ },
+ };
+ }
- get keyBindings() {
- return {
- 'esc': '_handleEscKey',
- 'ctrl+enter meta+enter': '_handleEnterKey',
- };
- }
+ get keyBindings() {
+ return {
+ 'esc': '_handleEscKey',
+ 'ctrl+enter meta+enter': '_handleEnterKey',
+ };
+ }
- static get observers() {
- return [
- '_changeUpdated(change.reviewers.*, change.owner)',
- '_ccsChanged(_ccs.splices)',
- '_reviewersChanged(_reviewers.splices)',
- ];
- }
+ static get observers() {
+ return [
+ '_changeUpdated(change.reviewers.*, change.owner)',
+ '_ccsChanged(_ccs.splices)',
+ '_reviewersChanged(_reviewers.splices)',
+ ];
+ }
- /** @override */
- attached() {
- super.attached();
- this._getAccount().then(account => {
- this._account = account || {};
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._getAccount().then(account => {
+ this._account = account || {};
+ });
+ }
- /** @override */
- ready() {
- super.ready();
- this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
+ }
- open(opt_focusTarget) {
- this.knownLatestState = LatestPatchState.CHECKING;
- this.fetchChangeUpdates(this.change, this.$.restAPI)
- .then(result => {
- this.knownLatestState = result.isLatest ?
- LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
- });
-
- this._focusOn(opt_focusTarget);
- if (this.quote && this.quote.length) {
- // If a reply quote has been provided, use it and clear the property.
- this.draft = this.quote;
- this.quote = '';
- } else {
- // Otherwise, check for an unsaved draft in localstorage.
- this.draft = this._loadStoredDraft();
- }
- if (this.$.restAPI.hasPendingDiffDrafts()) {
- this._savingComments = true;
- this.$.restAPI.awaitPendingDiffDrafts().then(() => {
- this.fire('comment-refresh');
- this._savingComments = false;
+ open(opt_focusTarget) {
+ this.knownLatestState = LatestPatchState.CHECKING;
+ this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ this.knownLatestState = result.isLatest ?
+ LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
});
- }
+
+ this._focusOn(opt_focusTarget);
+ if (this.quote && this.quote.length) {
+ // If a reply quote has been provided, use it and clear the property.
+ this.draft = this.quote;
+ this.quote = '';
+ } else {
+ // Otherwise, check for an unsaved draft in localstorage.
+ this.draft = this._loadStoredDraft();
}
-
- focus() {
- this._focusOn(FocusTarget.ANY);
- }
-
- getFocusStops() {
- const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
- return {
- start: this.$.reviewers.focusStart,
- end,
- };
- }
-
- setLabelValue(label, value) {
- const selectorEl =
- this.$.labelScores.shadowRoot
- .querySelector(`gr-label-score-row[name="${label}"]`);
- if (!selectorEl) { return; }
- selectorEl.setSelectedValue(value);
- }
-
- getLabelValue(label) {
- const selectorEl =
- this.$.labelScores.shadowRoot
- .querySelector(`gr-label-score-row[name="${label}"]`);
- if (!selectorEl) { return null; }
-
- return selectorEl.selectedValue;
- }
-
- _handleEscKey(e) {
- this.cancel();
- }
-
- _handleEnterKey(e) {
- this._submit();
- }
-
- _ccsChanged(splices) {
- this._reviewerTypeChanged(splices, ReviewerTypes.CC);
- }
-
- _reviewersChanged(splices) {
- this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER);
- }
-
- _reviewerTypeChanged(splices, reviewerType) {
- if (splices && splices.indexSplices) {
- this._reviewersMutated = true;
- this._processReviewerChange(splices.indexSplices,
- reviewerType);
- let key;
- let index;
- let account;
- // Remove any accounts that already exist as a CC for reviewer
- // or vice versa.
- const isReviewer = ReviewerTypes.REVIEWER === reviewerType;
- for (const splice of splices.indexSplices) {
- for (let i = 0; i < splice.addedCount; i++) {
- account = splice.object[splice.index + i];
- key = this._accountOrGroupKey(account);
- const array = isReviewer ? this._ccs : this._reviewers;
- index = array.findIndex(
- account => this._accountOrGroupKey(account) === key);
- if (index >= 0) {
- this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
- const moveFrom = isReviewer ? 'CC' : 'reviewer';
- const moveTo = isReviewer ? 'reviewer' : 'CC';
- const message = (account.name || account.email || key) +
- ` moved from ${moveFrom} to ${moveTo}.`;
- this.fire('show-alert', {message});
- }
- }
- }
- }
- }
-
- _processReviewerChange(indexSplices, type) {
- for (const splice of indexSplices) {
- for (const account of splice.removed) {
- if (!this._reviewersPendingRemove[type]) {
- console.err('Invalid type ' + type + ' for reviewer.');
- return;
- }
- this._reviewersPendingRemove[type].push(account);
- }
- }
- }
-
- /**
- * Resets the state of the _reviewersPendingRemove object, and removes
- * accounts if necessary.
- *
- * @param {boolean} isCancel true if the action is a cancel.
- * @param {Object=} opt_accountIdsTransferred map of account IDs that must
- * not be removed, because they have been readded in another state.
- */
- _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) {
- let reviewerArr;
- const keep = opt_accountIdsTransferred || {};
- for (const type in this._reviewersPendingRemove) {
- if (this._reviewersPendingRemove.hasOwnProperty(type)) {
- if (!isCancel) {
- reviewerArr = this._reviewersPendingRemove[type];
- for (let i = 0; i < reviewerArr.length; i++) {
- if (!keep[reviewerArr[i]._account_id]) {
- this._removeAccount(reviewerArr[i], type);
- }
- }
- }
- this._reviewersPendingRemove[type] = [];
- }
- }
- }
-
- /**
- * Removes an account from the change, both on the backend and the client.
- * Does nothing if the account is a pending addition.
- *
- * @param {!Object} account
- * @param {string} type
- */
- _removeAccount(account, type) {
- if (account._pendingAdd) { return; }
-
- return this.$.restAPI.removeChangeReviewer(this.change._number,
- account._account_id).then(response => {
- if (!response.ok) { return response; }
-
- const reviewers = this.change.reviewers[type] || [];
- for (let i = 0; i < reviewers.length; i++) {
- if (reviewers[i]._account_id == account._account_id) {
- this.splice(`change.reviewers.${type}`, i, 1);
- break;
- }
- }
+ if (this.$.restAPI.hasPendingDiffDrafts()) {
+ this._savingComments = true;
+ this.$.restAPI.awaitPendingDiffDrafts().then(() => {
+ this.fire('comment-refresh');
+ this._savingComments = false;
});
}
-
- _mapReviewer(reviewer) {
- let reviewerId;
- let confirmed;
- if (reviewer.account) {
- reviewerId = reviewer.account._account_id || reviewer.account.email;
- } else if (reviewer.group) {
- reviewerId = reviewer.group.id;
- confirmed = reviewer.group.confirmed;
- }
- return {reviewer: reviewerId, confirmed};
- }
-
- send(includeComments, startReview) {
- this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
- const labels = this.$.labelScores.getLabelValues();
-
- const obj = {
- drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
- labels,
- };
-
- if (startReview) {
- obj.ready = true;
- }
-
- if (this.draft != null) {
- obj.message = this.draft;
- }
-
- const accountAdditions = {};
- obj.reviewers = this.$.reviewers.additions().map(reviewer => {
- if (reviewer.account) {
- accountAdditions[reviewer.account._account_id] = true;
- }
- return this._mapReviewer(reviewer);
- });
- const ccsEl = this.$.ccs;
- if (ccsEl) {
- for (let reviewer of ccsEl.additions()) {
- if (reviewer.account) {
- accountAdditions[reviewer.account._account_id] = true;
- }
- reviewer = this._mapReviewer(reviewer);
- reviewer.state = 'CC';
- obj.reviewers.push(reviewer);
- }
- }
-
- this.disabled = true;
-
- const errFn = this._handle400Error.bind(this);
- return this._saveReview(obj, errFn)
- .then(response => {
- if (!response) {
- // Null or undefined response indicates that an error handler
- // took responsibility, so just return.
- return {};
- }
- if (!response.ok) {
- this.fire('server-error', {response});
- return {};
- }
-
- this.draft = '';
- this._includeComments = true;
- this.fire('send', null, {bubbles: false});
- return accountAdditions;
- })
- .then(result => {
- this.disabled = false;
- return result;
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
- }
-
- _focusOn(section) {
- // Safeguard- always want to focus on something.
- if (!section || section === FocusTarget.ANY) {
- section = this._chooseFocusTarget();
- }
- if (section === FocusTarget.BODY) {
- const textarea = this.$.textarea;
- textarea.async(textarea.getNativeTextarea()
- .focus.bind(textarea.getNativeTextarea()));
- } else if (section === FocusTarget.REVIEWERS) {
- const reviewerEntry = this.$.reviewers.focusStart;
- reviewerEntry.async(reviewerEntry.focus);
- } else if (section === FocusTarget.CCS) {
- const ccEntry = this.$.ccs.focusStart;
- ccEntry.async(ccEntry.focus);
- }
- }
-
- _chooseFocusTarget() {
- // If we are the owner and the reviewers field is empty, focus on that.
- if (this._account && this.change && this.change.owner &&
- this._account._account_id === this.change.owner._account_id &&
- (!this._reviewers || this._reviewers.length === 0)) {
- return FocusTarget.REVIEWERS;
- }
-
- // Default to BODY.
- return FocusTarget.BODY;
- }
-
- _handle400Error(response) {
- // A call to _saveReview could fail with a server error if erroneous
- // reviewers were requested. This is signalled with a 400 Bad Request
- // status. The default gr-rest-api-interface error handling would
- // result in a large JSON response body being displayed to the user in
- // the gr-error-manager toast.
- //
- // We can modify the error handling behavior by passing this function
- // through to restAPI as a custom error handling function. Since we're
- // short-circuiting restAPI we can do our own response parsing and fire
- // the server-error ourselves.
- //
- this.disabled = false;
-
- // Using response.clone() here, because getResponseObject() and
- // potentially the generic error handler will want to call text() on the
- // response object, which can only be done once per object.
- const jsonPromise = this.$.restAPI.getResponseObject(response.clone());
- return jsonPromise.then(result => {
- // Only perform custom error handling for 400s and a parseable
- // ReviewResult response.
- if (response.status === 400 && result) {
- const errors = [];
- for (const state of ['reviewers', 'ccs']) {
- if (!result.hasOwnProperty(state)) { continue; }
- for (const reviewer of Object.values(result[state])) {
- if (reviewer.error) {
- errors.push(reviewer.error);
- }
- }
- }
- response = {
- ok: false,
- status: response.status,
- text() { return Promise.resolve(errors.join(', ')); },
- };
- }
- this.fire('server-error', {response});
- return null; // Means that the error has been handled.
- });
- }
-
- _computeHideDraftList(draftCommentThreads) {
- return draftCommentThreads.length === 0;
- }
-
- _computeDraftsTitle(draftCommentThreads) {
- const total = draftCommentThreads.length;
- if (total == 0) { return ''; }
- if (total == 1) { return '1 Draft'; }
- if (total > 1) { return total + ' Drafts'; }
- }
-
- _computeMessagePlaceholder(canBeStarted) {
- return canBeStarted ?
- 'Add a note for your reviewers...' :
- 'Say something nice...';
- }
-
- _changeUpdated(changeRecord, owner) {
- // Polymer 2: check for undefined
- if ([changeRecord, owner].some(arg => arg === undefined)) {
- return;
- }
-
- this._rebuildReviewerArrays(changeRecord.base, owner);
- }
-
- _rebuildReviewerArrays(change, owner) {
- this._owner = owner;
-
- const reviewers = [];
- const ccs = [];
-
- for (const key in change) {
- if (change.hasOwnProperty(key)) {
- if (key !== 'REVIEWER' && key !== 'CC') {
- console.warn('unexpected reviewer state:', key);
- continue;
- }
- for (const entry of change[key]) {
- if (entry._account_id === owner._account_id) {
- continue;
- }
- switch (key) {
- case 'REVIEWER':
- reviewers.push(entry);
- break;
- case 'CC':
- ccs.push(entry);
- break;
- }
- }
- }
- }
-
- this._ccs = ccs;
- this._reviewers = reviewers;
- }
-
- _accountOrGroupKey(entry) {
- return entry.id || entry._account_id;
- }
-
- /**
- * Generates a function to filter out reviewer/CC entries. When isCCs is
- * truthy, the function filters out entries that already exist in this._ccs.
- * When falsy, the function filters entries that exist in this._reviewers.
- *
- * @param {boolean} isCCs
- * @return {!Function}
- */
- _filterReviewerSuggestionGenerator(isCCs) {
- return suggestion => {
- let entry;
- if (suggestion.account) {
- entry = suggestion.account;
- } else if (suggestion.group) {
- entry = suggestion.group;
- } else {
- console.warn(
- 'received suggestion that was neither account nor group:',
- suggestion);
- }
- if (entry._account_id === this._owner._account_id) {
- return false;
- }
-
- const key = this._accountOrGroupKey(entry);
- const finder = entry => this._accountOrGroupKey(entry) === key;
- if (isCCs) {
- return this._ccs.find(finder) === undefined;
- }
- return this._reviewers.find(finder) === undefined;
- };
- }
-
- _getAccount() {
- return this.$.restAPI.getAccount();
- }
-
- _cancelTapHandler(e) {
- e.preventDefault();
- this.cancel();
- }
-
- cancel() {
- this.fire('cancel', null, {bubbles: false});
- this.$.textarea.closeDropdown();
- this._purgeReviewersPendingRemove(true);
- this._rebuildReviewerArrays(this.change.reviewers, this._owner);
- }
-
- _saveClickHandler(e) {
- e.preventDefault();
- if (!this.$.ccs.submitEntryText()) {
- // Do not proceed with the save if there is an invalid email entry in
- // the text field of the CC entry.
- return;
- }
- this.send(this._includeComments, false).then(keepReviewers => {
- this._purgeReviewersPendingRemove(false, keepReviewers);
- });
- }
-
- _sendTapHandler(e) {
- e.preventDefault();
- this._submit();
- }
-
- _submit() {
- if (!this.$.ccs.submitEntryText()) {
- // Do not proceed with the send if there is an invalid email entry in
- // the text field of the CC entry.
- return;
- }
- if (this._sendDisabled) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- bubbles: true,
- composed: true,
- detail: {message: EMPTY_REPLY_MESSAGE},
- }));
- return;
- }
- return this.send(this._includeComments, this.canBeStarted)
- .then(keepReviewers => {
- this._purgeReviewersPendingRemove(false, keepReviewers);
- })
- .catch(err => {
- this.dispatchEvent(new CustomEvent('show-error', {
- bubbles: true,
- composed: true,
- detail: {message: `Error submitting review ${err}`},
- }));
- });
- }
-
- _saveReview(review, opt_errFn) {
- return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
- review, opt_errFn);
- }
-
- _reviewerPendingConfirmationUpdated(reviewer) {
- if (reviewer === null) {
- this.$.reviewerConfirmationOverlay.close();
- } else {
- this._pendingConfirmationDetails =
- this._ccPendingConfirmation || this._reviewerPendingConfirmation;
- this.$.reviewerConfirmationOverlay.open();
- }
- }
-
- _confirmPendingReviewer() {
- if (this._ccPendingConfirmation) {
- this.$.ccs.confirmGroup(this._ccPendingConfirmation.group);
- this._focusOn(FocusTarget.CCS);
- } else {
- this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
- this._focusOn(FocusTarget.REVIEWERS);
- }
- }
-
- _cancelPendingReviewer() {
- this._ccPendingConfirmation = null;
- this._reviewerPendingConfirmation = null;
-
- const target =
- this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
- this._focusOn(target);
- }
-
- _getStorageLocation() {
- // Tests trigger this method without setting change.
- if (!this.change) { return {}; }
- return {
- changeNum: this.change._number,
- patchNum: '@change',
- path: '@change',
- };
- }
-
- _loadStoredDraft() {
- const draft = this.$.storage.getDraftComment(this._getStorageLocation());
- return draft ? draft.message : '';
- }
-
- _handleAccountTextEntry() {
- // When either of the account entries has input added to the autocomplete,
- // it should trigger the save button to enable/
- //
- // Note: if the text is removed, the save button will not get disabled.
- this._reviewersMutated = true;
- }
-
- _draftChanged(newDraft, oldDraft) {
- this.debounce('store', () => {
- if (!newDraft.length && oldDraft) {
- // If the draft has been modified to be empty, then erase the storage
- // entry.
- this.$.storage.eraseDraftComment(this._getStorageLocation());
- } else if (newDraft.length) {
- this.$.storage.setDraftComment(this._getStorageLocation(),
- this.draft);
- }
- }, STORAGE_DEBOUNCE_INTERVAL_MS);
- }
-
- _handleHeightChanged(e) {
- this.fire('autogrow');
- }
-
- _handleLabelsChanged() {
- this._labelsChanged = Object.keys(
- this.$.labelScores.getLabelValues()).length !== 0;
- }
-
- _isState(knownLatestState, value) {
- return knownLatestState === value;
- }
-
- _reload() {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this.change);
- this.cancel();
- }
-
- _computeSendButtonLabel(canBeStarted) {
- return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
- }
-
- _computeSendButtonTooltip(canBeStarted) {
- return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND;
- }
-
- _computeSavingLabelClass(savingComments) {
- return savingComments ? 'saving' : '';
- }
-
- _computeSendButtonDisabled(
- buttonLabel, draftCommentThreads, text, reviewersMutated,
- labelsChanged, includeComments, disabled) {
- // Polymer 2: check for undefined
- if ([
- buttonLabel,
- draftCommentThreads,
- text,
- reviewersMutated,
- labelsChanged,
- includeComments,
- disabled,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (disabled) { return true; }
- if (buttonLabel === ButtonLabels.START_REVIEW) { return false; }
- const hasDrafts = includeComments && draftCommentThreads.length;
- return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
- }
-
- _computePatchSetWarning(patchNum, labelsChanged) {
- let str = `Patch ${patchNum} is not latest.`;
- if (labelsChanged) {
- str += ' Voting on a non-latest patch will have no effect.';
- }
- return str;
- }
-
- setPluginMessage(message) {
- this._pluginMessage = message;
- }
-
- _sendDisabledChanged(sendDisabled) {
- this.dispatchEvent(new CustomEvent('send-disabled-changed'));
- }
-
- _getReviewerSuggestionsProvider(change) {
- const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
- change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
- provider.init();
- return provider;
- }
-
- _getCcSuggestionsProvider(change) {
- const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
- change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC);
- provider.init();
- return provider;
- }
-
- _onThreadListModified() {
- // TODO(taoalpha): this won't propogate the changes to the files
- // should consider replacing this with either top level events
- // or gerrit level events
-
- // emit the event so change-view can also get updated with latest changes
- this.fire('comment-refresh');
- }
}
- customElements.define(GrReplyDialog.is, GrReplyDialog);
-})();
+ focus() {
+ this._focusOn(FocusTarget.ANY);
+ }
+
+ getFocusStops() {
+ const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
+ return {
+ start: this.$.reviewers.focusStart,
+ end,
+ };
+ }
+
+ setLabelValue(label, value) {
+ const selectorEl =
+ this.$.labelScores.shadowRoot
+ .querySelector(`gr-label-score-row[name="${label}"]`);
+ if (!selectorEl) { return; }
+ selectorEl.setSelectedValue(value);
+ }
+
+ getLabelValue(label) {
+ const selectorEl =
+ this.$.labelScores.shadowRoot
+ .querySelector(`gr-label-score-row[name="${label}"]`);
+ if (!selectorEl) { return null; }
+
+ return selectorEl.selectedValue;
+ }
+
+ _handleEscKey(e) {
+ this.cancel();
+ }
+
+ _handleEnterKey(e) {
+ this._submit();
+ }
+
+ _ccsChanged(splices) {
+ this._reviewerTypeChanged(splices, ReviewerTypes.CC);
+ }
+
+ _reviewersChanged(splices) {
+ this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER);
+ }
+
+ _reviewerTypeChanged(splices, reviewerType) {
+ if (splices && splices.indexSplices) {
+ this._reviewersMutated = true;
+ this._processReviewerChange(splices.indexSplices,
+ reviewerType);
+ let key;
+ let index;
+ let account;
+ // Remove any accounts that already exist as a CC for reviewer
+ // or vice versa.
+ const isReviewer = ReviewerTypes.REVIEWER === reviewerType;
+ for (const splice of splices.indexSplices) {
+ for (let i = 0; i < splice.addedCount; i++) {
+ account = splice.object[splice.index + i];
+ key = this._accountOrGroupKey(account);
+ const array = isReviewer ? this._ccs : this._reviewers;
+ index = array.findIndex(
+ account => this._accountOrGroupKey(account) === key);
+ if (index >= 0) {
+ this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
+ const moveFrom = isReviewer ? 'CC' : 'reviewer';
+ const moveTo = isReviewer ? 'reviewer' : 'CC';
+ const message = (account.name || account.email || key) +
+ ` moved from ${moveFrom} to ${moveTo}.`;
+ this.fire('show-alert', {message});
+ }
+ }
+ }
+ }
+ }
+
+ _processReviewerChange(indexSplices, type) {
+ for (const splice of indexSplices) {
+ for (const account of splice.removed) {
+ if (!this._reviewersPendingRemove[type]) {
+ console.err('Invalid type ' + type + ' for reviewer.');
+ return;
+ }
+ this._reviewersPendingRemove[type].push(account);
+ }
+ }
+ }
+
+ /**
+ * Resets the state of the _reviewersPendingRemove object, and removes
+ * accounts if necessary.
+ *
+ * @param {boolean} isCancel true if the action is a cancel.
+ * @param {Object=} opt_accountIdsTransferred map of account IDs that must
+ * not be removed, because they have been readded in another state.
+ */
+ _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) {
+ let reviewerArr;
+ const keep = opt_accountIdsTransferred || {};
+ for (const type in this._reviewersPendingRemove) {
+ if (this._reviewersPendingRemove.hasOwnProperty(type)) {
+ if (!isCancel) {
+ reviewerArr = this._reviewersPendingRemove[type];
+ for (let i = 0; i < reviewerArr.length; i++) {
+ if (!keep[reviewerArr[i]._account_id]) {
+ this._removeAccount(reviewerArr[i], type);
+ }
+ }
+ }
+ this._reviewersPendingRemove[type] = [];
+ }
+ }
+ }
+
+ /**
+ * Removes an account from the change, both on the backend and the client.
+ * Does nothing if the account is a pending addition.
+ *
+ * @param {!Object} account
+ * @param {string} type
+ */
+ _removeAccount(account, type) {
+ if (account._pendingAdd) { return; }
+
+ return this.$.restAPI.removeChangeReviewer(this.change._number,
+ account._account_id).then(response => {
+ if (!response.ok) { return response; }
+
+ const reviewers = this.change.reviewers[type] || [];
+ for (let i = 0; i < reviewers.length; i++) {
+ if (reviewers[i]._account_id == account._account_id) {
+ this.splice(`change.reviewers.${type}`, i, 1);
+ break;
+ }
+ }
+ });
+ }
+
+ _mapReviewer(reviewer) {
+ let reviewerId;
+ let confirmed;
+ if (reviewer.account) {
+ reviewerId = reviewer.account._account_id || reviewer.account.email;
+ } else if (reviewer.group) {
+ reviewerId = reviewer.group.id;
+ confirmed = reviewer.group.confirmed;
+ }
+ return {reviewer: reviewerId, confirmed};
+ }
+
+ send(includeComments, startReview) {
+ this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
+ const labels = this.$.labelScores.getLabelValues();
+
+ const obj = {
+ drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
+ labels,
+ };
+
+ if (startReview) {
+ obj.ready = true;
+ }
+
+ if (this.draft != null) {
+ obj.message = this.draft;
+ }
+
+ const accountAdditions = {};
+ obj.reviewers = this.$.reviewers.additions().map(reviewer => {
+ if (reviewer.account) {
+ accountAdditions[reviewer.account._account_id] = true;
+ }
+ return this._mapReviewer(reviewer);
+ });
+ const ccsEl = this.$.ccs;
+ if (ccsEl) {
+ for (let reviewer of ccsEl.additions()) {
+ if (reviewer.account) {
+ accountAdditions[reviewer.account._account_id] = true;
+ }
+ reviewer = this._mapReviewer(reviewer);
+ reviewer.state = 'CC';
+ obj.reviewers.push(reviewer);
+ }
+ }
+
+ this.disabled = true;
+
+ const errFn = this._handle400Error.bind(this);
+ return this._saveReview(obj, errFn)
+ .then(response => {
+ if (!response) {
+ // Null or undefined response indicates that an error handler
+ // took responsibility, so just return.
+ return {};
+ }
+ if (!response.ok) {
+ this.fire('server-error', {response});
+ return {};
+ }
+
+ this.draft = '';
+ this._includeComments = true;
+ this.fire('send', null, {bubbles: false});
+ return accountAdditions;
+ })
+ .then(result => {
+ this.disabled = false;
+ return result;
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
+ }
+
+ _focusOn(section) {
+ // Safeguard- always want to focus on something.
+ if (!section || section === FocusTarget.ANY) {
+ section = this._chooseFocusTarget();
+ }
+ if (section === FocusTarget.BODY) {
+ const textarea = this.$.textarea;
+ textarea.async(textarea.getNativeTextarea()
+ .focus.bind(textarea.getNativeTextarea()));
+ } else if (section === FocusTarget.REVIEWERS) {
+ const reviewerEntry = this.$.reviewers.focusStart;
+ reviewerEntry.async(reviewerEntry.focus);
+ } else if (section === FocusTarget.CCS) {
+ const ccEntry = this.$.ccs.focusStart;
+ ccEntry.async(ccEntry.focus);
+ }
+ }
+
+ _chooseFocusTarget() {
+ // If we are the owner and the reviewers field is empty, focus on that.
+ if (this._account && this.change && this.change.owner &&
+ this._account._account_id === this.change.owner._account_id &&
+ (!this._reviewers || this._reviewers.length === 0)) {
+ return FocusTarget.REVIEWERS;
+ }
+
+ // Default to BODY.
+ return FocusTarget.BODY;
+ }
+
+ _handle400Error(response) {
+ // A call to _saveReview could fail with a server error if erroneous
+ // reviewers were requested. This is signalled with a 400 Bad Request
+ // status. The default gr-rest-api-interface error handling would
+ // result in a large JSON response body being displayed to the user in
+ // the gr-error-manager toast.
+ //
+ // We can modify the error handling behavior by passing this function
+ // through to restAPI as a custom error handling function. Since we're
+ // short-circuiting restAPI we can do our own response parsing and fire
+ // the server-error ourselves.
+ //
+ this.disabled = false;
+
+ // Using response.clone() here, because getResponseObject() and
+ // potentially the generic error handler will want to call text() on the
+ // response object, which can only be done once per object.
+ const jsonPromise = this.$.restAPI.getResponseObject(response.clone());
+ return jsonPromise.then(result => {
+ // Only perform custom error handling for 400s and a parseable
+ // ReviewResult response.
+ if (response.status === 400 && result) {
+ const errors = [];
+ for (const state of ['reviewers', 'ccs']) {
+ if (!result.hasOwnProperty(state)) { continue; }
+ for (const reviewer of Object.values(result[state])) {
+ if (reviewer.error) {
+ errors.push(reviewer.error);
+ }
+ }
+ }
+ response = {
+ ok: false,
+ status: response.status,
+ text() { return Promise.resolve(errors.join(', ')); },
+ };
+ }
+ this.fire('server-error', {response});
+ return null; // Means that the error has been handled.
+ });
+ }
+
+ _computeHideDraftList(draftCommentThreads) {
+ return draftCommentThreads.length === 0;
+ }
+
+ _computeDraftsTitle(draftCommentThreads) {
+ const total = draftCommentThreads.length;
+ if (total == 0) { return ''; }
+ if (total == 1) { return '1 Draft'; }
+ if (total > 1) { return total + ' Drafts'; }
+ }
+
+ _computeMessagePlaceholder(canBeStarted) {
+ return canBeStarted ?
+ 'Add a note for your reviewers...' :
+ 'Say something nice...';
+ }
+
+ _changeUpdated(changeRecord, owner) {
+ // Polymer 2: check for undefined
+ if ([changeRecord, owner].some(arg => arg === undefined)) {
+ return;
+ }
+
+ this._rebuildReviewerArrays(changeRecord.base, owner);
+ }
+
+ _rebuildReviewerArrays(change, owner) {
+ this._owner = owner;
+
+ const reviewers = [];
+ const ccs = [];
+
+ for (const key in change) {
+ if (change.hasOwnProperty(key)) {
+ if (key !== 'REVIEWER' && key !== 'CC') {
+ console.warn('unexpected reviewer state:', key);
+ continue;
+ }
+ for (const entry of change[key]) {
+ if (entry._account_id === owner._account_id) {
+ continue;
+ }
+ switch (key) {
+ case 'REVIEWER':
+ reviewers.push(entry);
+ break;
+ case 'CC':
+ ccs.push(entry);
+ break;
+ }
+ }
+ }
+ }
+
+ this._ccs = ccs;
+ this._reviewers = reviewers;
+ }
+
+ _accountOrGroupKey(entry) {
+ return entry.id || entry._account_id;
+ }
+
+ /**
+ * Generates a function to filter out reviewer/CC entries. When isCCs is
+ * truthy, the function filters out entries that already exist in this._ccs.
+ * When falsy, the function filters entries that exist in this._reviewers.
+ *
+ * @param {boolean} isCCs
+ * @return {!Function}
+ */
+ _filterReviewerSuggestionGenerator(isCCs) {
+ return suggestion => {
+ let entry;
+ if (suggestion.account) {
+ entry = suggestion.account;
+ } else if (suggestion.group) {
+ entry = suggestion.group;
+ } else {
+ console.warn(
+ 'received suggestion that was neither account nor group:',
+ suggestion);
+ }
+ if (entry._account_id === this._owner._account_id) {
+ return false;
+ }
+
+ const key = this._accountOrGroupKey(entry);
+ const finder = entry => this._accountOrGroupKey(entry) === key;
+ if (isCCs) {
+ return this._ccs.find(finder) === undefined;
+ }
+ return this._reviewers.find(finder) === undefined;
+ };
+ }
+
+ _getAccount() {
+ return this.$.restAPI.getAccount();
+ }
+
+ _cancelTapHandler(e) {
+ e.preventDefault();
+ this.cancel();
+ }
+
+ cancel() {
+ this.fire('cancel', null, {bubbles: false});
+ this.$.textarea.closeDropdown();
+ this._purgeReviewersPendingRemove(true);
+ this._rebuildReviewerArrays(this.change.reviewers, this._owner);
+ }
+
+ _saveClickHandler(e) {
+ e.preventDefault();
+ if (!this.$.ccs.submitEntryText()) {
+ // Do not proceed with the save if there is an invalid email entry in
+ // the text field of the CC entry.
+ return;
+ }
+ this.send(this._includeComments, false).then(keepReviewers => {
+ this._purgeReviewersPendingRemove(false, keepReviewers);
+ });
+ }
+
+ _sendTapHandler(e) {
+ e.preventDefault();
+ this._submit();
+ }
+
+ _submit() {
+ if (!this.$.ccs.submitEntryText()) {
+ // Do not proceed with the send if there is an invalid email entry in
+ // the text field of the CC entry.
+ return;
+ }
+ if (this._sendDisabled) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ bubbles: true,
+ composed: true,
+ detail: {message: EMPTY_REPLY_MESSAGE},
+ }));
+ return;
+ }
+ return this.send(this._includeComments, this.canBeStarted)
+ .then(keepReviewers => {
+ this._purgeReviewersPendingRemove(false, keepReviewers);
+ })
+ .catch(err => {
+ this.dispatchEvent(new CustomEvent('show-error', {
+ bubbles: true,
+ composed: true,
+ detail: {message: `Error submitting review ${err}`},
+ }));
+ });
+ }
+
+ _saveReview(review, opt_errFn) {
+ return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
+ review, opt_errFn);
+ }
+
+ _reviewerPendingConfirmationUpdated(reviewer) {
+ if (reviewer === null) {
+ this.$.reviewerConfirmationOverlay.close();
+ } else {
+ this._pendingConfirmationDetails =
+ this._ccPendingConfirmation || this._reviewerPendingConfirmation;
+ this.$.reviewerConfirmationOverlay.open();
+ }
+ }
+
+ _confirmPendingReviewer() {
+ if (this._ccPendingConfirmation) {
+ this.$.ccs.confirmGroup(this._ccPendingConfirmation.group);
+ this._focusOn(FocusTarget.CCS);
+ } else {
+ this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
+ this._focusOn(FocusTarget.REVIEWERS);
+ }
+ }
+
+ _cancelPendingReviewer() {
+ this._ccPendingConfirmation = null;
+ this._reviewerPendingConfirmation = null;
+
+ const target =
+ this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
+ this._focusOn(target);
+ }
+
+ _getStorageLocation() {
+ // Tests trigger this method without setting change.
+ if (!this.change) { return {}; }
+ return {
+ changeNum: this.change._number,
+ patchNum: '@change',
+ path: '@change',
+ };
+ }
+
+ _loadStoredDraft() {
+ const draft = this.$.storage.getDraftComment(this._getStorageLocation());
+ return draft ? draft.message : '';
+ }
+
+ _handleAccountTextEntry() {
+ // When either of the account entries has input added to the autocomplete,
+ // it should trigger the save button to enable/
+ //
+ // Note: if the text is removed, the save button will not get disabled.
+ this._reviewersMutated = true;
+ }
+
+ _draftChanged(newDraft, oldDraft) {
+ this.debounce('store', () => {
+ if (!newDraft.length && oldDraft) {
+ // If the draft has been modified to be empty, then erase the storage
+ // entry.
+ this.$.storage.eraseDraftComment(this._getStorageLocation());
+ } else if (newDraft.length) {
+ this.$.storage.setDraftComment(this._getStorageLocation(),
+ this.draft);
+ }
+ }, STORAGE_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _handleHeightChanged(e) {
+ this.fire('autogrow');
+ }
+
+ _handleLabelsChanged() {
+ this._labelsChanged = Object.keys(
+ this.$.labelScores.getLabelValues()).length !== 0;
+ }
+
+ _isState(knownLatestState, value) {
+ return knownLatestState === value;
+ }
+
+ _reload() {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this.change);
+ this.cancel();
+ }
+
+ _computeSendButtonLabel(canBeStarted) {
+ return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
+ }
+
+ _computeSendButtonTooltip(canBeStarted) {
+ return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND;
+ }
+
+ _computeSavingLabelClass(savingComments) {
+ return savingComments ? 'saving' : '';
+ }
+
+ _computeSendButtonDisabled(
+ buttonLabel, draftCommentThreads, text, reviewersMutated,
+ labelsChanged, includeComments, disabled) {
+ // Polymer 2: check for undefined
+ if ([
+ buttonLabel,
+ draftCommentThreads,
+ text,
+ reviewersMutated,
+ labelsChanged,
+ includeComments,
+ disabled,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (disabled) { return true; }
+ if (buttonLabel === ButtonLabels.START_REVIEW) { return false; }
+ const hasDrafts = includeComments && draftCommentThreads.length;
+ return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
+ }
+
+ _computePatchSetWarning(patchNum, labelsChanged) {
+ let str = `Patch ${patchNum} is not latest.`;
+ if (labelsChanged) {
+ str += ' Voting on a non-latest patch will have no effect.';
+ }
+ return str;
+ }
+
+ setPluginMessage(message) {
+ this._pluginMessage = message;
+ }
+
+ _sendDisabledChanged(sendDisabled) {
+ this.dispatchEvent(new CustomEvent('send-disabled-changed'));
+ }
+
+ _getReviewerSuggestionsProvider(change) {
+ const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+ change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
+ provider.init();
+ return provider;
+ }
+
+ _getCcSuggestionsProvider(change) {
+ const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
+ change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC);
+ provider.init();
+ return provider;
+ }
+
+ _onThreadListModified() {
+ // TODO(taoalpha): this won't propogate the changes to the files
+ // should consider replacing this with either top level events
+ // or gerrit level events
+
+ // emit the event so change-view can also get updated with latest changes
+ this.fire('comment-refresh');
+ }
+}
+
+customElements.define(GrReplyDialog.is, GrReplyDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
index 8424b5d..0c98834 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
@@ -1,47 +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="/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/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../../shared/gr-account-list/gr-account-list.html">
-<link rel="import" href="../gr-label-scores/gr-label-scores.html">
-<link rel="import" href="../gr-thread-list/gr-thread-list.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../change/gr-comment-list/gr-comment-list.html">
-<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script>
-
-<dom-module id="gr-reply-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
background-color: var(--dialog-background-color);
@@ -167,33 +142,15 @@
<section class="peopleContainer">
<div class="peopleList">
<div class="peopleListLabel">Reviewers</div>
- <gr-account-list
- id="reviewers"
- accounts="{{_reviewers}}"
- removable-values="[[change.removable_reviewers]]"
- filter="[[filterReviewerSuggestion]]"
- pending-confirmation="{{_reviewerPendingConfirmation}}"
- placeholder="Add reviewer..."
- on-account-text-changed="_handleAccountTextEntry"
- suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
+ <gr-account-list id="reviewers" accounts="{{_reviewers}}" removable-values="[[change.removable_reviewers]]" filter="[[filterReviewerSuggestion]]" pending-confirmation="{{_reviewerPendingConfirmation}}" placeholder="Add reviewer..." on-account-text-changed="_handleAccountTextEntry" suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]">
</gr-account-list>
</div>
<div class="peopleList">
<div class="peopleListLabel">CC</div>
- <gr-account-list
- id="ccs"
- accounts="{{_ccs}}"
- filter="[[filterCCSuggestion]]"
- pending-confirmation="{{_ccPendingConfirmation}}"
- allow-any-input
- placeholder="Add CC..."
- on-account-text-changed="_handleAccountTextEntry"
- suggestions-provider="[[_getCcSuggestionsProvider(change)]]">
+ <gr-account-list id="ccs" accounts="{{_ccs}}" filter="[[filterCCSuggestion]]" pending-confirmation="{{_ccPendingConfirmation}}" allow-any-input="" placeholder="Add CC..." on-account-text-changed="_handleAccountTextEntry" suggestions-provider="[[_getCcSuggestionsProvider(change)]]">
</gr-account-list>
</div>
- <gr-overlay
- id="reviewerConfirmationOverlay"
- on-iron-overlay-canceled="_cancelPendingReviewer">
+ <gr-overlay id="reviewerConfirmationOverlay" on-iron-overlay-canceled="_cancelPendingReviewer">
<div class="reviewerConfirmation">
Group
<span class="groupName">
@@ -215,18 +172,7 @@
</section>
<section class="textareaContainer">
<gr-endpoint-decorator name="reply-text">
- <gr-textarea
- id="textarea"
- class="message"
- autocomplete="on"
- placeholder=[[_messagePlaceholder]]
- fixed-position-dropdown
- hide-border="true"
- monospace="true"
- disabled="{{disabled}}"
- rows="4"
- text="{{draft}}"
- on-bind-value-changed="_handleHeightChanged">
+ <gr-textarea id="textarea" class="message" autocomplete="on" placeholder="[[_messagePlaceholder]]" fixed-position-dropdown="" hide-border="true" monospace="true" disabled="{{disabled}}" rows="4" text="{{draft}}" on-bind-value-changed="_handleHeightChanged">
</gr-textarea>
</gr-endpoint-decorator>
</section>
@@ -235,84 +181,44 @@
<input type="checkbox" checked="{{_previewFormatting::change}}">
Preview formatting
</label>
- <gr-formatted-text
- content="[[draft]]"
- hidden$="[[!_previewFormatting]]"
- config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+ <gr-formatted-text content="[[draft]]" hidden\$="[[!_previewFormatting]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text>
</section>
<section class="labelsContainer">
<gr-endpoint-decorator name="reply-label-scores">
- <gr-label-scores
- id="labelScores"
- account="[[_account]]"
- change="[[change]]"
- on-labels-changed="_handleLabelsChanged"
- permitted-labels=[[permittedLabels]]></gr-label-scores>
+ <gr-label-scores id="labelScores" account="[[_account]]" change="[[change]]" on-labels-changed="_handleLabelsChanged" permitted-labels="[[permittedLabels]]"></gr-label-scores>
</gr-endpoint-decorator>
<div id="pluginMessage">[[_pluginMessage]]</div>
</section>
- <section class="draftsContainer" hidden$="[[_computeHideDraftList(draftCommentThreads)]]">
+ <section class="draftsContainer" hidden\$="[[_computeHideDraftList(draftCommentThreads)]]">
<div class="includeComments">
- <input type="checkbox" id="includeComments"
- checked="{{_includeComments::change}}">
+ <input type="checkbox" id="includeComments" checked="{{_includeComments::change}}">
<label for="includeComments">Publish [[_computeDraftsTitle(draftCommentThreads)]]</label>
</div>
- <gr-thread-list
- id="commentList"
- hidden$="[[!_includeComments]]"
- threads="[[draftCommentThreads]]"
- change="[[change]]"
- change-num="[[change._number]]"
- logged-in="true"
- hide-toggle-buttons
- on-thread-list-modified="_onThreadListModified">
+ <gr-thread-list id="commentList" hidden\$="[[!_includeComments]]" threads="[[draftCommentThreads]]" change="[[change]]" change-num="[[change._number]]" logged-in="true" hide-toggle-buttons="" on-thread-list-modified="_onThreadListModified">
</gr-thread-list>
- <span
- id="savingLabel"
- class$="[[_computeSavingLabelClass(_savingComments)]]">
+ <span id="savingLabel" class\$="[[_computeSavingLabelClass(_savingComments)]]">
Saving comments...
</span>
</section>
<section class="actions">
<div class="left">
- <span
- id="checkingStatusLabel"
- hidden$="[[!_isState(knownLatestState, 'checking')]]">
+ <span id="checkingStatusLabel" hidden\$="[[!_isState(knownLatestState, 'checking')]]">
Checking whether patch [[patchNum]] is latest...
</span>
- <span
- id="notLatestLabel"
- hidden$="[[!_isState(knownLatestState, 'not-latest')]]">
+ <span id="notLatestLabel" hidden\$="[[!_isState(knownLatestState, 'not-latest')]]">
[[_computePatchSetWarning(patchNum, _labelsChanged)]]
- <gr-button link on-click="_reload">Reload</gr-button>
+ <gr-button link="" on-click="_reload">Reload</gr-button>
</span>
</div>
<div class="right">
- <gr-button
- link
- id="cancelButton"
- class="action cancel"
- on-click="_cancelTapHandler">Cancel</gr-button>
+ <gr-button link="" id="cancelButton" class="action cancel" on-click="_cancelTapHandler">Cancel</gr-button>
<template is="dom-if" if="[[canBeStarted]]">
<!-- Use 'Send' here as the change may only about reviewers / ccs
and when this button is visible, the next button will always
be 'Start review' -->
- <gr-button
- link
- disabled="[[_isState(knownLatestState, 'not-latest')]]"
- class="action save"
- has-tooltip
- title="[[_saveTooltip]]"
- on-click="_saveClickHandler">Save</gr-button>
+ <gr-button link="" disabled="[[_isState(knownLatestState, 'not-latest')]]" class="action save" has-tooltip="" title="[[_saveTooltip]]" on-click="_saveClickHandler">Save</gr-button>
</template>
- <gr-button
- id="sendButton"
- primary
- disabled="[[_sendDisabled]]"
- class="action send"
- has-tooltip
- title$="[[_computeSendButtonTooltip(canBeStarted)]]"
- on-click="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
+ <gr-button id="sendButton" primary="" disabled="[[_sendDisabled]]" class="action send" has-tooltip="" title\$="[[_computeSendButtonTooltip(canBeStarted)]]" on-click="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
</div>
</section>
</div>
@@ -320,6 +226,4 @@
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-storage id="storage"></gr-storage>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-reply-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index b7c2330..5257ef46 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -19,16 +19,20 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reply-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="/bower_components/iron-overlay-behavior/iron-overlay-manager.html">
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-reply-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-reply-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-reply-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,1196 +40,1199 @@
</template>
</test-fixture>
-<script>
- function cloneableResponse(status, text) {
- return {
- ok: false,
- status,
- text() {
- return Promise.resolve(text);
- },
- clone() {
- return {
- ok: false,
- status,
- text() {
- return Promise.resolve(text);
- },
- };
- },
- };
- }
-
- suite('gr-reply-dialog tests', async () => {
- await readyToTest();
- let element;
- let changeNum;
- let patchNum;
-
- let sandbox;
- let getDraftCommentStub;
- let setDraftCommentStub;
- let eraseDraftCommentStub;
-
- let lastId = 0;
- const makeAccount = function() { return {_account_id: lastId++}; };
- const makeGroup = function() { return {id: lastId++}; };
-
- setup(() => {
- sandbox = sinon.sandbox.create();
-
- changeNum = 42;
- patchNum = 1;
-
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getAccount() { return Promise.resolve({}); },
- getChange() { return Promise.resolve({}); },
- getChangeSuggestedReviewers() { return Promise.resolve([]); },
- });
-
- element = fixture('basic');
- element.change = {
- _number: changeNum,
- labels: {
- 'Verified': {
- values: {
- '-1': 'Fails',
- ' 0': 'No score',
- '+1': 'Verified',
- },
- default_value: 0,
- },
- 'Code-Review': {
- values: {
- '-2': 'Do not submit',
- '-1': 'I would prefer that you didn\'t submit this',
- ' 0': 'No score',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- default_value: 0,
- },
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import {IronOverlayManager} from '@polymer/iron-overlay-behavior/iron-overlay-manager.js';
+import '../../../test/common-test-setup.js';
+import './gr-reply-dialog.js';
+function cloneableResponse(status, text) {
+ return {
+ ok: false,
+ status,
+ text() {
+ return Promise.resolve(text);
+ },
+ clone() {
+ return {
+ ok: false,
+ status,
+ text() {
+ return Promise.resolve(text);
},
};
- element.patchNum = patchNum;
- element.permittedLabels = {
- 'Code-Review': [
- '-1',
- ' 0',
- '+1',
- ],
- 'Verified': [
- '-1',
- ' 0',
- '+1',
- ],
- };
+ },
+ };
+}
- getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
- setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
- eraseDraftCommentStub = sandbox.stub(element.$.storage,
- 'eraseDraftComment');
+suite('gr-reply-dialog tests', () => {
+ let element;
+ let changeNum;
+ let patchNum;
- sandbox.stub(element, 'fetchChangeUpdates')
- .returns(Promise.resolve({isLatest: true}));
+ let sandbox;
+ let getDraftCommentStub;
+ let setDraftCommentStub;
+ let eraseDraftCommentStub;
- // Allow the elements created by dom-repeat to be stamped.
- flushAsynchronousOperations();
+ let lastId = 0;
+ const makeAccount = function() { return {_account_id: lastId++}; };
+ const makeGroup = function() { return {id: lastId++}; };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+
+ changeNum = 42;
+ patchNum = 1;
+
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getAccount() { return Promise.resolve({}); },
+ getChange() { return Promise.resolve({}); },
+ getChangeSuggestedReviewers() { return Promise.resolve([]); },
});
- teardown(() => {
- sandbox.restore();
- });
-
- function stubSaveReview(jsonResponseProducer) {
- return sandbox.stub(
- element,
- '_saveReview',
- review => new Promise((resolve, reject) => {
- try {
- const result = jsonResponseProducer(review) || {};
- const resultStr =
- element.$.restAPI.JSON_PREFIX + JSON.stringify(result);
- resolve({
- ok: true,
- text() {
- return Promise.resolve(resultStr);
- },
- });
- } catch (err) {
- reject(err);
- }
- }));
- }
-
- test('default to publishing draft comments with reply', done => {
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
- flush(() => {
- flush(() => {
- element.draft = 'I wholeheartedly disapprove';
-
- stubSaveReview(review => {
- assert.deepEqual(review, {
- drafts: 'PUBLISH_ALL_REVISIONS',
- labels: {
- 'Code-Review': 0,
- 'Verified': 0,
- },
- message: 'I wholeheartedly disapprove',
- reviewers: [],
- });
- assert.isFalse(element.$.commentList.hidden);
- done();
- });
-
- // This is needed on non-Blink engines most likely due to the ways in
- // which the dom-repeat elements are stamped.
- flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- });
- });
- });
- });
-
- test('keep draft comments with reply', done => {
- MockInteractions.tap(element.shadowRoot.querySelector('#includeComments'));
- assert.equal(element._includeComments, false);
-
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
- flush(() => {
- flush(() => {
- element.draft = 'I wholeheartedly disapprove';
-
- stubSaveReview(review => {
- assert.deepEqual(review, {
- drafts: 'KEEP',
- labels: {
- 'Code-Review': 0,
- 'Verified': 0,
- },
- message: 'I wholeheartedly disapprove',
- reviewers: [],
- });
- assert.isTrue(element.$.commentList.hidden);
- done();
- });
-
- // This is needed on non-Blink engines most likely due to the ways in
- // which the dom-repeat elements are stamped.
- flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- });
- });
- });
- });
-
- test('label picker', done => {
- element.draft = 'I wholeheartedly disapprove';
- stubSaveReview(review => {
- assert.deepEqual(review, {
- drafts: 'PUBLISH_ALL_REVISIONS',
- labels: {
- 'Code-Review': -1,
- 'Verified': -1,
+ element = fixture('basic');
+ element.change = {
+ _number: changeNum,
+ labels: {
+ 'Verified': {
+ values: {
+ '-1': 'Fails',
+ ' 0': 'No score',
+ '+1': 'Verified',
},
- message: 'I wholeheartedly disapprove',
- reviewers: [],
- });
- });
+ default_value: 0,
+ },
+ 'Code-Review': {
+ values: {
+ '-2': 'Do not submit',
+ '-1': 'I would prefer that you didn\'t submit this',
+ ' 0': 'No score',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ },
+ };
+ element.patchNum = patchNum;
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ 'Verified': [
+ '-1',
+ ' 0',
+ '+1',
+ ],
+ };
- sandbox.stub(element.$.labelScores, 'getLabelValues', () => {
- return {
- 'Code-Review': -1,
- 'Verified': -1,
- };
- });
+ getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment');
+ setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment');
+ eraseDraftCommentStub = sandbox.stub(element.$.storage,
+ 'eraseDraftComment');
- element.addEventListener('send', () => {
- // Flush to ensure properties are updated.
- flush(() => {
- assert.isFalse(element.disabled,
- 'Element should be enabled when done sending reply.');
- assert.equal(element.draft.length, 0);
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
+
+ // Allow the elements created by dom-repeat to be stamped.
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ function stubSaveReview(jsonResponseProducer) {
+ return sandbox.stub(
+ element,
+ '_saveReview',
+ review => new Promise((resolve, reject) => {
+ try {
+ const result = jsonResponseProducer(review) || {};
+ const resultStr =
+ element.$.restAPI.JSON_PREFIX + JSON.stringify(result);
+ resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(resultStr);
+ },
+ });
+ } catch (err) {
+ reject(err);
+ }
+ }));
+ }
+
+ test('default to publishing draft comments with reply', done => {
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+ flush(() => {
+ flush(() => {
+ element.draft = 'I wholeheartedly disapprove';
+
+ stubSaveReview(review => {
+ assert.deepEqual(review, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {
+ 'Code-Review': 0,
+ 'Verified': 0,
+ },
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ assert.isFalse(element.$.commentList.hidden);
done();
});
- });
- // This is needed on non-Blink engines most likely due to the ways in
- // which the dom-repeat elements are stamped.
- flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- assert.isTrue(element.disabled);
- });
- });
-
- test('getlabelValue returns value', done => {
- flush(() => {
- element.shadowRoot
- .querySelector('gr-label-scores')
- .shadowRoot
- .querySelector(`gr-label-score-row[name="Verified"]`)
- .setSelectedValue(-1);
- assert.equal('-1', element.getLabelValue('Verified'));
- done();
- });
- });
-
- test('getlabelValue when no score is selected', done => {
- flush(() => {
- element.shadowRoot
- .querySelector('gr-label-scores')
- .shadowRoot
- .querySelector(`gr-label-score-row[name="Code-Review"]`)
- .setSelectedValue(-1);
- assert.strictEqual(element.getLabelValue('Verified'), ' 0');
- done();
- });
- });
-
- test('setlabelValue', done => {
- element._account = {_account_id: 1};
- flush(() => {
- const label = 'Verified';
- const value = '+1';
- element.setLabelValue(label, value);
-
- const labels = element.$.labelScores.getLabelValues();
- assert.deepEqual(labels, {
- 'Code-Review': 0,
- 'Verified': 1,
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
});
+ });
+ });
+ });
+
+ test('keep draft comments with reply', done => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#includeComments'));
+ assert.equal(element._includeComments, false);
+
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+ flush(() => {
+ flush(() => {
+ element.draft = 'I wholeheartedly disapprove';
+
+ stubSaveReview(review => {
+ assert.deepEqual(review, {
+ drafts: 'KEEP',
+ labels: {
+ 'Code-Review': 0,
+ 'Verified': 0,
+ },
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ assert.isTrue(element.$.commentList.hidden);
+ done();
+ });
+
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ });
+ });
+ });
+ });
+
+ test('label picker', done => {
+ element.draft = 'I wholeheartedly disapprove';
+ stubSaveReview(review => {
+ assert.deepEqual(review, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {
+ 'Code-Review': -1,
+ 'Verified': -1,
+ },
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ });
+
+ sandbox.stub(element.$.labelScores, 'getLabelValues', () => {
+ return {
+ 'Code-Review': -1,
+ 'Verified': -1,
+ };
+ });
+
+ element.addEventListener('send', () => {
+ // Flush to ensure properties are updated.
+ flush(() => {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done sending reply.');
+ assert.equal(element.draft.length, 0);
done();
});
});
- function getActiveElement() {
- return Polymer.IronOverlayManager.deepActiveElement;
- }
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ assert.isTrue(element.disabled);
+ });
+ });
- function isVisible(el) {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') != 'none';
- }
+ test('getlabelValue returns value', done => {
+ flush(() => {
+ element.shadowRoot
+ .querySelector('gr-label-scores')
+ .shadowRoot
+ .querySelector(`gr-label-score-row[name="Verified"]`)
+ .setSelectedValue(-1);
+ assert.equal('-1', element.getLabelValue('Verified'));
+ done();
+ });
+ });
- function overlayObserver(mode) {
- return new Promise(resolve => {
- function listener() {
- element.removeEventListener('iron-overlay-' + mode, listener);
- resolve();
- }
- element.addEventListener('iron-overlay-' + mode, listener);
+ test('getlabelValue when no score is selected', done => {
+ flush(() => {
+ element.shadowRoot
+ .querySelector('gr-label-scores')
+ .shadowRoot
+ .querySelector(`gr-label-score-row[name="Code-Review"]`)
+ .setSelectedValue(-1);
+ assert.strictEqual(element.getLabelValue('Verified'), ' 0');
+ done();
+ });
+ });
+
+ test('setlabelValue', done => {
+ element._account = {_account_id: 1};
+ flush(() => {
+ const label = 'Verified';
+ const value = '+1';
+ element.setLabelValue(label, value);
+
+ const labels = element.$.labelScores.getLabelValues();
+ assert.deepEqual(labels, {
+ 'Code-Review': 0,
+ 'Verified': 1,
});
- }
+ done();
+ });
+ });
- function isFocusInsideElement(element) {
- // In Polymer 2 focused element either <paper-input> or nested
- // native input <input> element depending on the current focus
- // in browser window.
- // For example, the focus is changed if the developer console
- // get a focus.
- let activeElement = getActiveElement();
- while (activeElement) {
- if (activeElement === element) {
- return true;
- }
- if (activeElement.parentElement) {
- activeElement = activeElement.parentElement;
- } else {
- activeElement = activeElement.getRootNode().host;
- }
+ function getActiveElement() {
+ return IronOverlayManager.deepActiveElement;
+ }
+
+ function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') != 'none';
+ }
+
+ function overlayObserver(mode) {
+ return new Promise(resolve => {
+ function listener() {
+ element.removeEventListener('iron-overlay-' + mode, listener);
+ resolve();
}
- return false;
+ element.addEventListener('iron-overlay-' + mode, listener);
+ });
+ }
+
+ function isFocusInsideElement(element) {
+ // In Polymer 2 focused element either <paper-input> or nested
+ // native input <input> element depending on the current focus
+ // in browser window.
+ // For example, the focus is changed if the developer console
+ // get a focus.
+ let activeElement = getActiveElement();
+ while (activeElement) {
+ if (activeElement === element) {
+ return true;
+ }
+ if (activeElement.parentElement) {
+ activeElement = activeElement.parentElement;
+ } else {
+ activeElement = activeElement.getRootNode().host;
+ }
}
+ return false;
+ }
- function testConfirmationDialog(done, cc) {
- const yesButton = element
- .shadowRoot
- .querySelector('.reviewerConfirmationButtons gr-button:first-child');
- const noButton = element
- .shadowRoot
- .querySelector('.reviewerConfirmationButtons gr-button:last-child');
+ function testConfirmationDialog(done, cc) {
+ const yesButton = element
+ .shadowRoot
+ .querySelector('.reviewerConfirmationButtons gr-button:first-child');
+ const noButton = element
+ .shadowRoot
+ .querySelector('.reviewerConfirmationButtons gr-button:last-child');
- element._ccPendingConfirmation = null;
- element._reviewerPendingConfirmation = null;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+ element._ccPendingConfirmation = null;
+ element._reviewerPendingConfirmation = null;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
- // Cause the confirmation dialog to display.
- let observer = overlayObserver('opened');
- const group = {
- id: 'id',
- name: 'name',
+ // Cause the confirmation dialog to display.
+ let observer = overlayObserver('opened');
+ const group = {
+ id: 'id',
+ name: 'name',
+ };
+ if (cc) {
+ element._ccPendingConfirmation = {
+ group,
+ count: 10,
};
- if (cc) {
- element._ccPendingConfirmation = {
- group,
- count: 10,
- };
- } else {
- element._reviewerPendingConfirmation = {
- group,
- count: 10,
- };
- }
- flushAsynchronousOperations();
+ } else {
+ element._reviewerPendingConfirmation = {
+ group,
+ count: 10,
+ };
+ }
+ flushAsynchronousOperations();
- if (cc) {
- assert.deepEqual(
- element._ccPendingConfirmation,
- element._pendingConfirmationDetails);
- } else {
- assert.deepEqual(
- element._reviewerPendingConfirmation,
- element._pendingConfirmationDetails);
- }
+ if (cc) {
+ assert.deepEqual(
+ element._ccPendingConfirmation,
+ element._pendingConfirmationDetails);
+ } else {
+ assert.deepEqual(
+ element._reviewerPendingConfirmation,
+ element._pendingConfirmationDetails);
+ }
- observer
- .then(() => {
- assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
- observer = overlayObserver('closed');
- const expected = 'Group name has 10 members';
- assert.notEqual(
- element.$.reviewerConfirmationOverlay.innerText
- .indexOf(expected),
- -1);
- MockInteractions.tap(noButton); // close the overlay
- return observer;
- }).then(() => {
- assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+ observer
+ .then(() => {
+ assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
+ observer = overlayObserver('closed');
+ const expected = 'Group name has 10 members';
+ assert.notEqual(
+ element.$.reviewerConfirmationOverlay.innerText
+ .indexOf(expected),
+ -1);
+ MockInteractions.tap(noButton); // close the overlay
+ return observer;
+ }).then(() => {
+ assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
- // We should be focused on account entry input.
+ // We should be focused on account entry input.
+ assert.isTrue(
+ isFocusInsideElement(
+ element.$.reviewers.$.entry.$.input.$.input
+ )
+ );
+
+ // No reviewer/CC should have been added.
+ assert.equal(element.$.ccs.additions().length, 0);
+ assert.equal(element.$.reviewers.additions().length, 0);
+
+ // Reopen confirmation dialog.
+ observer = overlayObserver('opened');
+ if (cc) {
+ element._ccPendingConfirmation = {
+ group,
+ count: 10,
+ };
+ } else {
+ element._reviewerPendingConfirmation = {
+ group,
+ count: 10,
+ };
+ }
+ return observer;
+ })
+ .then(() => {
+ assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
+ observer = overlayObserver('closed');
+ MockInteractions.tap(yesButton); // Confirm the group.
+ return observer;
+ })
+ .then(() => {
+ assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
+ const additions = cc ?
+ element.$.ccs.additions() :
+ element.$.reviewers.additions();
+ assert.deepEqual(
+ additions,
+ [
+ {
+ group: {
+ id: 'id',
+ name: 'name',
+ confirmed: true,
+ _group: true,
+ _pendingAdd: true,
+ },
+ },
+ ]);
+
+ // We should be focused on account entry input.
+ if (cc) {
+ assert.isTrue(
+ isFocusInsideElement(
+ element.$.ccs.$.entry.$.input.$.input
+ )
+ );
+ } else {
assert.isTrue(
isFocusInsideElement(
element.$.reviewers.$.entry.$.input.$.input
)
);
+ }
+ })
+ .then(done);
+ }
- // No reviewer/CC should have been added.
- assert.equal(element.$.ccs.additions().length, 0);
- assert.equal(element.$.reviewers.additions().length, 0);
+ test('cc confirmation', done => {
+ testConfirmationDialog(done, true);
+ });
- // Reopen confirmation dialog.
- observer = overlayObserver('opened');
- if (cc) {
- element._ccPendingConfirmation = {
- group,
- count: 10,
- };
- } else {
- element._reviewerPendingConfirmation = {
- group,
- count: 10,
- };
- }
- return observer;
- })
- .then(() => {
- assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
- observer = overlayObserver('closed');
- MockInteractions.tap(yesButton); // Confirm the group.
- return observer;
- })
- .then(() => {
- assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
- const additions = cc ?
- element.$.ccs.additions() :
- element.$.reviewers.additions();
- assert.deepEqual(
- additions,
- [
- {
- group: {
- id: 'id',
- name: 'name',
- confirmed: true,
- _group: true,
- _pendingAdd: true,
- },
- },
- ]);
+ test('reviewer confirmation', done => {
+ testConfirmationDialog(done, false);
+ });
- // We should be focused on account entry input.
- if (cc) {
- assert.isTrue(
- isFocusInsideElement(
- element.$.ccs.$.entry.$.input.$.input
- )
- );
- } else {
- assert.isTrue(
- isFocusInsideElement(
- element.$.reviewers.$.entry.$.input.$.input
- )
- );
- }
- })
- .then(done);
- }
+ test('_getStorageLocation', () => {
+ const actual = element._getStorageLocation();
+ assert.equal(actual.changeNum, changeNum);
+ assert.equal(actual.patchNum, '@change');
+ assert.equal(actual.path, '@change');
+ });
- test('cc confirmation', done => {
- testConfirmationDialog(done, true);
+ test('_reviewersMutated when account-text-change is fired from ccs', () => {
+ flushAsynchronousOperations();
+ assert.isFalse(element._reviewersMutated);
+ assert.isTrue(element.$.ccs.allowAnyInput);
+ assert.isFalse(element.shadowRoot
+ .querySelector('#reviewers').allowAnyInput);
+ element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed',
+ {bubbles: true, composed: true}));
+ assert.isTrue(element._reviewersMutated);
+ });
+
+ test('gets draft from storage on open', () => {
+ const storedDraft = 'hello world';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, storedDraft);
+ });
+
+ test('gets draft from storage even when text is already present', () => {
+ const storedDraft = 'hello world';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.draft = 'foo bar';
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, storedDraft);
+ });
+
+ test('blank if no stored draft', () => {
+ getDraftCommentStub.returns(null);
+ element.draft = 'foo bar';
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, '');
+ });
+
+ test('does not check stored draft when quote is present', () => {
+ const storedDraft = 'hello world';
+ const quote = '> foo bar';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.quote = quote;
+ element.open();
+ assert.isFalse(getDraftCommentStub.called);
+ assert.equal(element.draft, quote);
+ assert.isNotOk(element.quote);
+ });
+
+ test('updates stored draft on edits', () => {
+ const firstEdit = 'hello';
+ const location = element._getStorageLocation();
+
+ element.draft = firstEdit;
+ element.flushDebouncer('store');
+
+ assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit));
+
+ element.draft = '';
+ element.flushDebouncer('store');
+
+ assert.isTrue(eraseDraftCommentStub.calledWith(location));
+ });
+
+ test('400 converts to human-readable server-error', done => {
+ sandbox.stub(window, 'fetch', () => {
+ const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
+ '"ccs":{"id2":{"error":"second error"}}}';
+ return Promise.resolve(cloneableResponse(400, text));
});
- test('reviewer confirmation', done => {
- testConfirmationDialog(done, false);
- });
-
- test('_getStorageLocation', () => {
- const actual = element._getStorageLocation();
- assert.equal(actual.changeNum, changeNum);
- assert.equal(actual.patchNum, '@change');
- assert.equal(actual.path, '@change');
- });
-
- test('_reviewersMutated when account-text-change is fired from ccs', () => {
- flushAsynchronousOperations();
- assert.isFalse(element._reviewersMutated);
- assert.isTrue(element.$.ccs.allowAnyInput);
- assert.isFalse(element.shadowRoot
- .querySelector('#reviewers').allowAnyInput);
- element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed',
- {bubbles: true, composed: true}));
- assert.isTrue(element._reviewersMutated);
- });
-
- test('gets draft from storage on open', () => {
- const storedDraft = 'hello world';
- getDraftCommentStub.returns({message: storedDraft});
- element.open();
- assert.isTrue(getDraftCommentStub.called);
- assert.equal(element.draft, storedDraft);
- });
-
- test('gets draft from storage even when text is already present', () => {
- const storedDraft = 'hello world';
- getDraftCommentStub.returns({message: storedDraft});
- element.draft = 'foo bar';
- element.open();
- assert.isTrue(getDraftCommentStub.called);
- assert.equal(element.draft, storedDraft);
- });
-
- test('blank if no stored draft', () => {
- getDraftCommentStub.returns(null);
- element.draft = 'foo bar';
- element.open();
- assert.isTrue(getDraftCommentStub.called);
- assert.equal(element.draft, '');
- });
-
- test('does not check stored draft when quote is present', () => {
- const storedDraft = 'hello world';
- const quote = '> foo bar';
- getDraftCommentStub.returns({message: storedDraft});
- element.quote = quote;
- element.open();
- assert.isFalse(getDraftCommentStub.called);
- assert.equal(element.draft, quote);
- assert.isNotOk(element.quote);
- });
-
- test('updates stored draft on edits', () => {
- const firstEdit = 'hello';
- const location = element._getStorageLocation();
-
- element.draft = firstEdit;
- element.flushDebouncer('store');
-
- assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit));
-
- element.draft = '';
- element.flushDebouncer('store');
-
- assert.isTrue(eraseDraftCommentStub.calledWith(location));
- });
-
- test('400 converts to human-readable server-error', done => {
- sandbox.stub(window, 'fetch', () => {
- const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
- '"ccs":{"id2":{"error":"second error"}}}';
- return Promise.resolve(cloneableResponse(400, text));
- });
-
- element.addEventListener('server-error', event => {
- if (event.target !== element) {
- return;
- }
- event.detail.response.text().then(body => {
- assert.equal(body, 'first error, second error');
- done();
- });
- });
-
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- flush(() => { element.send(); });
- });
-
- test('non-json 400 is treated as a normal server-error', done => {
- sandbox.stub(window, 'fetch', () => {
- const text = 'Comment validation error!';
- return Promise.resolve(cloneableResponse(400, text));
- });
-
- element.addEventListener('server-error', event => {
- if (event.target !== element) {
- return;
- }
- event.detail.response.text().then(body => {
- assert.equal(body, 'Comment validation error!');
- done();
- });
- });
-
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- flush(() => { element.send(); });
- });
-
- test('filterReviewerSuggestion', () => {
- const owner = makeAccount();
- const reviewer1 = makeAccount();
- const reviewer2 = makeGroup();
- const cc1 = makeAccount();
- const cc2 = makeGroup();
- let filter = element._filterReviewerSuggestionGenerator(false);
-
- element._owner = owner;
- element._reviewers = [reviewer1, reviewer2];
- element._ccs = [cc1, cc2];
-
- assert.isTrue(filter({account: makeAccount()}));
- assert.isTrue(filter({group: makeGroup()}));
-
- // Owner should be excluded.
- assert.isFalse(filter({account: owner}));
-
- // Existing and pending reviewers should be excluded when isCC = false.
- assert.isFalse(filter({account: reviewer1}));
- assert.isFalse(filter({group: reviewer2}));
-
- filter = element._filterReviewerSuggestionGenerator(true);
-
- // Existing and pending CCs should be excluded when isCC = true;.
- assert.isFalse(filter({account: cc1}));
- assert.isFalse(filter({group: cc2}));
- });
-
- test('_focusOn', () => {
- sandbox.spy(element, '_chooseFocusTarget');
- flushAsynchronousOperations();
- const textareaStub = sandbox.stub(element.$.textarea, 'async');
- const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
- 'async');
- const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async');
- element._focusOn();
- assert.equal(element._chooseFocusTarget.callCount, 1);
- assert.deepEqual(textareaStub.callCount, 1);
- assert.deepEqual(reviewerEntryStub.callCount, 0);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.ANY);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 2);
- assert.deepEqual(reviewerEntryStub.callCount, 0);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.BODY);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 3);
- assert.deepEqual(reviewerEntryStub.callCount, 0);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.REVIEWERS);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 3);
- assert.deepEqual(reviewerEntryStub.callCount, 1);
- assert.deepEqual(ccStub.callCount, 0);
-
- element._focusOn(element.FocusTarget.CCS);
- assert.equal(element._chooseFocusTarget.callCount, 2);
- assert.deepEqual(textareaStub.callCount, 3);
- assert.deepEqual(reviewerEntryStub.callCount, 1);
- assert.deepEqual(ccStub.callCount, 1);
- });
-
- test('_chooseFocusTarget', () => {
- element._account = null;
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
-
- element._account = {_account_id: 1};
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
-
- element.change.owner = {_account_id: 2};
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
-
- element.change.owner._account_id = 1;
- element.change._reviewers = null;
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
-
- element._reviewers = [];
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
-
- element._reviewers.push({});
- assert.strictEqual(
- element._chooseFocusTarget(), element.FocusTarget.BODY);
- });
-
- test('only send labels that have changed', done => {
- flush(() => {
- stubSaveReview(review => {
- assert.deepEqual(review.labels, {
- 'Code-Review': 0,
- 'Verified': -1,
- });
- });
-
- element.addEventListener('send', () => {
- done();
- });
- // Without wrapping this test in flush(), the below two calls to
- // MockInteractions.tap() cause a race in some situations in shadow DOM.
- // The send button can be tapped before the others, causing the test to
- // fail.
-
- element.shadowRoot
- .querySelector('gr-label-scores').shadowRoot
- .querySelector(
- 'gr-label-score-row[name="Verified"]')
- .setSelectedValue(-1);
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- });
- });
-
- test('_processReviewerChange', () => {
- const mockIndexSplices = function(toRemove) {
- return [{
- removed: [toRemove],
- }];
- };
-
- element._processReviewerChange(
- mockIndexSplices(makeAccount()), 'REVIEWER');
- assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1);
- });
-
- test('_purgeReviewersPendingRemove', () => {
- const removeStub = sandbox.stub(element, '_removeAccount');
- const mock = function() {
- element._reviewersPendingRemove = {
- test: [makeAccount()],
- test2: [makeAccount(), makeAccount()],
- };
- };
- const checkObjEmpty = function(obj) {
- for (const prop in obj) {
- if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; }
- }
- return true;
- };
- mock();
- element._purgeReviewersPendingRemove(true); // Cancel
- assert.isFalse(removeStub.called);
- assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
-
- mock();
- element._purgeReviewersPendingRemove(false); // Submit
- assert.isTrue(removeStub.called);
- assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
- });
-
- test('_removeAccount', done => {
- sandbox.stub(element.$.restAPI, 'removeChangeReviewer')
- .returns(Promise.resolve({ok: true}));
- const arr = [makeAccount(), makeAccount()];
- element.change.reviewers = {
- REVIEWER: arr.slice(),
- };
-
- element._removeAccount(arr[1], 'REVIEWER').then(() => {
- assert.equal(element.change.reviewers.REVIEWER.length, 1);
- assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1));
+ element.addEventListener('server-error', event => {
+ if (event.target !== element) {
+ return;
+ }
+ event.detail.response.text().then(body => {
+ assert.equal(body, 'first error, second error');
done();
});
});
- test('moving from cc to reviewer', () => {
- element._reviewersPendingRemove = {
- CC: [],
- REVIEWER: [],
- };
- flushAsynchronousOperations();
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ flush(() => { element.send(); });
+ });
- const reviewer1 = makeAccount();
- const reviewer2 = makeAccount();
- const reviewer3 = makeAccount();
- const cc1 = makeAccount();
- const cc2 = makeAccount();
- const cc3 = makeAccount();
- const cc4 = makeAccount();
- element._reviewers = [reviewer1, reviewer2, reviewer3];
- element._ccs = [cc1, cc2, cc3, cc4];
- element.push('_reviewers', cc1);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers,
- [reviewer1, reviewer2, reviewer3, cc1]);
- assert.deepEqual(element._ccs, [cc2, cc3, cc4]);
- assert.deepEqual(element._reviewersPendingRemove.CC, [cc1]);
-
- element.push('_reviewers', cc4, cc3);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers,
- [reviewer1, reviewer2, reviewer3, cc1, cc4, cc3]);
- assert.deepEqual(element._ccs, [cc2]);
- assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]);
+ test('non-json 400 is treated as a normal server-error', done => {
+ sandbox.stub(window, 'fetch', () => {
+ const text = 'Comment validation error!';
+ return Promise.resolve(cloneableResponse(400, text));
});
- test('moving from reviewer to cc', () => {
- element._reviewersPendingRemove = {
- CC: [],
- REVIEWER: [],
- };
- flushAsynchronousOperations();
-
- const reviewer1 = makeAccount();
- const reviewer2 = makeAccount();
- const reviewer3 = makeAccount();
- const cc1 = makeAccount();
- const cc2 = makeAccount();
- const cc3 = makeAccount();
- const cc4 = makeAccount();
- element._reviewers = [reviewer1, reviewer2, reviewer3];
- element._ccs = [cc1, cc2, cc3, cc4];
- element.push('_ccs', reviewer1);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers,
- [reviewer2, reviewer3]);
- assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]);
- assert.deepEqual(element._reviewersPendingRemove.REVIEWER, [reviewer1]);
-
- element.push('_ccs', reviewer3, reviewer2);
- flushAsynchronousOperations();
-
- assert.deepEqual(element._reviewers, []);
- assert.deepEqual(element._ccs,
- [cc1, cc2, cc3, cc4, reviewer1, reviewer3, reviewer2]);
- assert.deepEqual(element._reviewersPendingRemove.REVIEWER,
- [reviewer1, reviewer3, reviewer2]);
- });
-
- test('migrate reviewers between states', done => {
- element._reviewersPendingRemove = {
- CC: [],
- REVIEWER: [],
- };
- flushAsynchronousOperations();
- const reviewers = element.$.reviewers;
- const ccs = element.$.ccs;
- const reviewer1 = makeAccount();
- const reviewer2 = makeAccount();
- const cc1 = makeAccount();
- const cc2 = makeAccount();
- const cc3 = makeAccount();
- element._reviewers = [reviewer1, reviewer2];
- element._ccs = [cc1, cc2, cc3];
-
- const mutations = [];
-
- stubSaveReview(review => mutations.push(...review.reviewers));
-
- sandbox.stub(element, '_removeAccount', (account, type) => {
- mutations.push({state: 'REMOVED', account});
- return Promise.resolve();
- });
-
- // Remove and add to other field.
- reviewers.fire('remove', {account: reviewer1});
- ccs.$.entry.fire('add', {value: {account: reviewer1}});
- ccs.fire('remove', {account: cc1});
- ccs.fire('remove', {account: cc3});
- reviewers.$.entry.fire('add', {value: {account: cc1}});
-
- // Add to other field without removing from former field.
- // (Currently not possible in UI, but this is a good consistency check).
- reviewers.$.entry.fire('add', {value: {account: cc2}});
- ccs.$.entry.fire('add', {value: {account: reviewer2}});
- const mapReviewer = function(reviewer, opt_state) {
- const result = {reviewer: reviewer._account_id, confirmed: undefined};
- if (opt_state) {
- result.state = opt_state;
- }
- return result;
- };
-
- // Send and purge and verify moves, delete cc3.
- element.send()
- .then(keepReviewers =>
- element._purgeReviewersPendingRemove(false, keepReviewers))
- .then(() => {
- assert.deepEqual(
- mutations, [
- mapReviewer(cc1),
- mapReviewer(cc2),
- mapReviewer(reviewer1, 'CC'),
- mapReviewer(reviewer2, 'CC'),
- {account: cc3, state: 'REMOVED'},
- ]);
- done();
- });
- });
-
- test('emits cancel on esc key', () => {
- const cancelHandler = sandbox.spy();
- element.addEventListener('cancel', cancelHandler);
- MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
- flushAsynchronousOperations();
-
- assert.isTrue(cancelHandler.called);
- });
-
- test('should not send on enter key', () => {
- stubSaveReview(() => undefined);
- element.addEventListener('send', () => assert.fail('wrongly called'));
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
- flushAsynchronousOperations();
- });
-
- test('emit send on ctrl+enter key', done => {
- stubSaveReview(() => undefined);
- element.addEventListener('send', () => done());
- MockInteractions.pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter');
- flushAsynchronousOperations();
- });
-
- test('_computeMessagePlaceholder', () => {
- assert.equal(
- element._computeMessagePlaceholder(false),
- 'Say something nice...');
- assert.equal(
- element._computeMessagePlaceholder(true),
- 'Add a note for your reviewers...');
- });
-
- test('_computeSendButtonLabel', () => {
- assert.equal(
- element._computeSendButtonLabel(false),
- 'Send');
- assert.equal(
- element._computeSendButtonLabel(true),
- 'Start review');
- });
-
- test('_handle400Error reviewrs and CCs', done => {
- const error1 = 'error 1';
- const error2 = 'error 2';
- const error3 = 'error 3';
- const text = ')]}\'' + JSON.stringify({
- reviewers: {
- username1: {
- input: 'user 1',
- error: error1,
- },
- username2: {
- input: 'user 2',
- error: error2,
- },
- },
- ccs: {
- username3: {
- input: 'user 3',
- error: error3,
- },
- },
- });
- element.addEventListener('server-error', e => {
- e.detail.response.text().then(text => {
- assert.equal(text, [error1, error2, error3].join(', '));
- done();
- });
- });
- element._handle400Error(cloneableResponse(400, text));
- });
-
- test('_handle400Error CCs only', done => {
- const error1 = 'error 1';
- const text = ')]}\'' + JSON.stringify({
- ccs: {
- username1: {
- input: 'user 1',
- error: error1,
- },
- },
- });
- element.addEventListener('server-error', e => {
- e.detail.response.text().then(text => {
- assert.equal(text, error1);
- done();
- });
- });
- element._handle400Error(cloneableResponse(400, text));
- });
-
- test('fires height change when the drafts comments load', done => {
- // Flush DOM operations before binding to the autogrow event so we don't
- // catch the events fired from the initial layout.
- flush(() => {
- const autoGrowHandler = sinon.stub();
- element.addEventListener('autogrow', autoGrowHandler);
- element.draftCommentThreads = [];
- flush(() => {
- assert.isTrue(autoGrowHandler.called);
- done();
- });
+ element.addEventListener('server-error', event => {
+ if (event.target !== element) {
+ return;
+ }
+ event.detail.response.text().then(body => {
+ assert.equal(body, 'Comment validation error!');
+ done();
});
});
- suite('post review API', () => {
- let startReviewStub;
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ flush(() => { element.send(); });
+ });
- setup(() => {
- startReviewStub = sandbox.stub(
- element.$.restAPI,
- 'startReview',
- () => Promise.resolve());
- });
+ test('filterReviewerSuggestion', () => {
+ const owner = makeAccount();
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeGroup();
+ const cc1 = makeAccount();
+ const cc2 = makeGroup();
+ let filter = element._filterReviewerSuggestionGenerator(false);
- test('ready property in review input on start review', () => {
- stubSaveReview(review => {
- assert.isTrue(review.ready);
- return {ready: true};
- });
- return element.send(true, true).then(() => {
- assert.isFalse(startReviewStub.called);
- });
- });
+ element._owner = owner;
+ element._reviewers = [reviewer1, reviewer2];
+ element._ccs = [cc1, cc2];
- test('no ready property in review input on save review', () => {
- stubSaveReview(review => {
- assert.isUndefined(review.ready);
- });
- return element.send(true, false).then(() => {
- assert.isFalse(startReviewStub.called);
- });
- });
- });
+ assert.isTrue(filter({account: makeAccount()}));
+ assert.isTrue(filter({group: makeGroup()}));
- suite('start review and save buttons', () => {
- let sendStub;
+ // Owner should be excluded.
+ assert.isFalse(filter({account: owner}));
- setup(() => {
- sendStub = sandbox.stub(element, 'send', () => Promise.resolve());
- element.canBeStarted = true;
- // Flush to make both Start/Save buttons appear in DOM.
- flushAsynchronousOperations();
- });
+ // Existing and pending reviewers should be excluded when isCC = false.
+ assert.isFalse(filter({account: reviewer1}));
+ assert.isFalse(filter({group: reviewer2}));
- test('start review sets ready', () => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.send'));
- flushAsynchronousOperations();
- assert.isTrue(sendStub.calledWith(true, true));
- });
+ filter = element._filterReviewerSuggestionGenerator(true);
- test('save review doesn\'t set ready', () => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- flushAsynchronousOperations();
- assert.isTrue(sendStub.calledWith(true, false));
- });
- });
+ // Existing and pending CCs should be excluded when isCC = true;.
+ assert.isFalse(filter({account: cc1}));
+ assert.isFalse(filter({group: cc2}));
+ });
- test('buttons disabled until all API calls are resolved', () => {
+ test('_focusOn', () => {
+ sandbox.spy(element, '_chooseFocusTarget');
+ flushAsynchronousOperations();
+ const textareaStub = sandbox.stub(element.$.textarea, 'async');
+ const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
+ 'async');
+ const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async');
+ element._focusOn();
+ assert.equal(element._chooseFocusTarget.callCount, 1);
+ assert.deepEqual(textareaStub.callCount, 1);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.ANY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 2);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.BODY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.REVIEWERS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.CCS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 3);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 1);
+ });
+
+ test('_chooseFocusTarget', () => {
+ element._account = null;
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+ element._account = {_account_id: 1};
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+ element.change.owner = {_account_id: 2};
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+
+ element.change.owner._account_id = 1;
+ element.change._reviewers = null;
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
+
+ element._reviewers = [];
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.REVIEWERS);
+
+ element._reviewers.push({});
+ assert.strictEqual(
+ element._chooseFocusTarget(), element.FocusTarget.BODY);
+ });
+
+ test('only send labels that have changed', done => {
+ flush(() => {
stubSaveReview(review => {
+ assert.deepEqual(review.labels, {
+ 'Code-Review': 0,
+ 'Verified': -1,
+ });
+ });
+
+ element.addEventListener('send', () => {
+ done();
+ });
+ // Without wrapping this test in flush(), the below two calls to
+ // MockInteractions.tap() cause a race in some situations in shadow DOM.
+ // The send button can be tapped before the others, causing the test to
+ // fail.
+
+ element.shadowRoot
+ .querySelector('gr-label-scores').shadowRoot
+ .querySelector(
+ 'gr-label-score-row[name="Verified"]')
+ .setSelectedValue(-1);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ });
+ });
+
+ test('_processReviewerChange', () => {
+ const mockIndexSplices = function(toRemove) {
+ return [{
+ removed: [toRemove],
+ }];
+ };
+
+ element._processReviewerChange(
+ mockIndexSplices(makeAccount()), 'REVIEWER');
+ assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1);
+ });
+
+ test('_purgeReviewersPendingRemove', () => {
+ const removeStub = sandbox.stub(element, '_removeAccount');
+ const mock = function() {
+ element._reviewersPendingRemove = {
+ test: [makeAccount()],
+ test2: [makeAccount(), makeAccount()],
+ };
+ };
+ const checkObjEmpty = function(obj) {
+ for (const prop in obj) {
+ if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; }
+ }
+ return true;
+ };
+ mock();
+ element._purgeReviewersPendingRemove(true); // Cancel
+ assert.isFalse(removeStub.called);
+ assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
+
+ mock();
+ element._purgeReviewersPendingRemove(false); // Submit
+ assert.isTrue(removeStub.called);
+ assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
+ });
+
+ test('_removeAccount', done => {
+ sandbox.stub(element.$.restAPI, 'removeChangeReviewer')
+ .returns(Promise.resolve({ok: true}));
+ const arr = [makeAccount(), makeAccount()];
+ element.change.reviewers = {
+ REVIEWER: arr.slice(),
+ };
+
+ element._removeAccount(arr[1], 'REVIEWER').then(() => {
+ assert.equal(element.change.reviewers.REVIEWER.length, 1);
+ assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1));
+ done();
+ });
+ });
+
+ test('moving from cc to reviewer', () => {
+ element._reviewersPendingRemove = {
+ CC: [],
+ REVIEWER: [],
+ };
+ flushAsynchronousOperations();
+
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeAccount();
+ const reviewer3 = makeAccount();
+ const cc1 = makeAccount();
+ const cc2 = makeAccount();
+ const cc3 = makeAccount();
+ const cc4 = makeAccount();
+ element._reviewers = [reviewer1, reviewer2, reviewer3];
+ element._ccs = [cc1, cc2, cc3, cc4];
+ element.push('_reviewers', cc1);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers,
+ [reviewer1, reviewer2, reviewer3, cc1]);
+ assert.deepEqual(element._ccs, [cc2, cc3, cc4]);
+ assert.deepEqual(element._reviewersPendingRemove.CC, [cc1]);
+
+ element.push('_reviewers', cc4, cc3);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers,
+ [reviewer1, reviewer2, reviewer3, cc1, cc4, cc3]);
+ assert.deepEqual(element._ccs, [cc2]);
+ assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]);
+ });
+
+ test('moving from reviewer to cc', () => {
+ element._reviewersPendingRemove = {
+ CC: [],
+ REVIEWER: [],
+ };
+ flushAsynchronousOperations();
+
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeAccount();
+ const reviewer3 = makeAccount();
+ const cc1 = makeAccount();
+ const cc2 = makeAccount();
+ const cc3 = makeAccount();
+ const cc4 = makeAccount();
+ element._reviewers = [reviewer1, reviewer2, reviewer3];
+ element._ccs = [cc1, cc2, cc3, cc4];
+ element.push('_ccs', reviewer1);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers,
+ [reviewer2, reviewer3]);
+ assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]);
+ assert.deepEqual(element._reviewersPendingRemove.REVIEWER, [reviewer1]);
+
+ element.push('_ccs', reviewer3, reviewer2);
+ flushAsynchronousOperations();
+
+ assert.deepEqual(element._reviewers, []);
+ assert.deepEqual(element._ccs,
+ [cc1, cc2, cc3, cc4, reviewer1, reviewer3, reviewer2]);
+ assert.deepEqual(element._reviewersPendingRemove.REVIEWER,
+ [reviewer1, reviewer3, reviewer2]);
+ });
+
+ test('migrate reviewers between states', done => {
+ element._reviewersPendingRemove = {
+ CC: [],
+ REVIEWER: [],
+ };
+ flushAsynchronousOperations();
+ const reviewers = element.$.reviewers;
+ const ccs = element.$.ccs;
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeAccount();
+ const cc1 = makeAccount();
+ const cc2 = makeAccount();
+ const cc3 = makeAccount();
+ element._reviewers = [reviewer1, reviewer2];
+ element._ccs = [cc1, cc2, cc3];
+
+ const mutations = [];
+
+ stubSaveReview(review => mutations.push(...review.reviewers));
+
+ sandbox.stub(element, '_removeAccount', (account, type) => {
+ mutations.push({state: 'REMOVED', account});
+ return Promise.resolve();
+ });
+
+ // Remove and add to other field.
+ reviewers.fire('remove', {account: reviewer1});
+ ccs.$.entry.fire('add', {value: {account: reviewer1}});
+ ccs.fire('remove', {account: cc1});
+ ccs.fire('remove', {account: cc3});
+ reviewers.$.entry.fire('add', {value: {account: cc1}});
+
+ // Add to other field without removing from former field.
+ // (Currently not possible in UI, but this is a good consistency check).
+ reviewers.$.entry.fire('add', {value: {account: cc2}});
+ ccs.$.entry.fire('add', {value: {account: reviewer2}});
+ const mapReviewer = function(reviewer, opt_state) {
+ const result = {reviewer: reviewer._account_id, confirmed: undefined};
+ if (opt_state) {
+ result.state = opt_state;
+ }
+ return result;
+ };
+
+ // Send and purge and verify moves, delete cc3.
+ element.send()
+ .then(keepReviewers =>
+ element._purgeReviewersPendingRemove(false, keepReviewers))
+ .then(() => {
+ assert.deepEqual(
+ mutations, [
+ mapReviewer(cc1),
+ mapReviewer(cc2),
+ mapReviewer(reviewer1, 'CC'),
+ mapReviewer(reviewer2, 'CC'),
+ {account: cc3, state: 'REMOVED'},
+ ]);
+ done();
+ });
+ });
+
+ test('emits cancel on esc key', () => {
+ const cancelHandler = sandbox.spy();
+ element.addEventListener('cancel', cancelHandler);
+ MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
+ flushAsynchronousOperations();
+
+ assert.isTrue(cancelHandler.called);
+ });
+
+ test('should not send on enter key', () => {
+ stubSaveReview(() => undefined);
+ element.addEventListener('send', () => assert.fail('wrongly called'));
+ MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
+ flushAsynchronousOperations();
+ });
+
+ test('emit send on ctrl+enter key', done => {
+ stubSaveReview(() => undefined);
+ element.addEventListener('send', () => done());
+ MockInteractions.pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter');
+ flushAsynchronousOperations();
+ });
+
+ test('_computeMessagePlaceholder', () => {
+ assert.equal(
+ element._computeMessagePlaceholder(false),
+ 'Say something nice...');
+ assert.equal(
+ element._computeMessagePlaceholder(true),
+ 'Add a note for your reviewers...');
+ });
+
+ test('_computeSendButtonLabel', () => {
+ assert.equal(
+ element._computeSendButtonLabel(false),
+ 'Send');
+ assert.equal(
+ element._computeSendButtonLabel(true),
+ 'Start review');
+ });
+
+ test('_handle400Error reviewrs and CCs', done => {
+ const error1 = 'error 1';
+ const error2 = 'error 2';
+ const error3 = 'error 3';
+ const text = ')]}\'' + JSON.stringify({
+ reviewers: {
+ username1: {
+ input: 'user 1',
+ error: error1,
+ },
+ username2: {
+ input: 'user 2',
+ error: error2,
+ },
+ },
+ ccs: {
+ username3: {
+ input: 'user 3',
+ error: error3,
+ },
+ },
+ });
+ element.addEventListener('server-error', e => {
+ e.detail.response.text().then(text => {
+ assert.equal(text, [error1, error2, error3].join(', '));
+ done();
+ });
+ });
+ element._handle400Error(cloneableResponse(400, text));
+ });
+
+ test('_handle400Error CCs only', done => {
+ const error1 = 'error 1';
+ const text = ')]}\'' + JSON.stringify({
+ ccs: {
+ username1: {
+ input: 'user 1',
+ error: error1,
+ },
+ },
+ });
+ element.addEventListener('server-error', e => {
+ e.detail.response.text().then(text => {
+ assert.equal(text, error1);
+ done();
+ });
+ });
+ element._handle400Error(cloneableResponse(400, text));
+ });
+
+ test('fires height change when the drafts comments load', done => {
+ // Flush DOM operations before binding to the autogrow event so we don't
+ // catch the events fired from the initial layout.
+ flush(() => {
+ const autoGrowHandler = sinon.stub();
+ element.addEventListener('autogrow', autoGrowHandler);
+ element.draftCommentThreads = [];
+ flush(() => {
+ assert.isTrue(autoGrowHandler.called);
+ done();
+ });
+ });
+ });
+
+ suite('post review API', () => {
+ let startReviewStub;
+
+ setup(() => {
+ startReviewStub = sandbox.stub(
+ element.$.restAPI,
+ 'startReview',
+ () => Promise.resolve());
+ });
+
+ test('ready property in review input on start review', () => {
+ stubSaveReview(review => {
+ assert.isTrue(review.ready);
return {ready: true};
});
return element.send(true, true).then(() => {
- assert.isFalse(element.disabled);
+ assert.isFalse(startReviewStub.called);
});
});
- suite('error handling', () => {
- const expectedDraft = 'draft';
- const expectedError = new Error('test');
-
- setup(() => {
- element.draft = expectedDraft;
+ test('no ready property in review input on save review', () => {
+ stubSaveReview(review => {
+ assert.isUndefined(review.ready);
});
-
- function assertDialogOpenAndEnabled() {
- assert.strictEqual(expectedDraft, element.draft);
- assert.isFalse(element.disabled);
- }
-
- test('error occurs in _saveReview', () => {
- stubSaveReview(review => {
- throw expectedError;
- });
- return element.send(true, true).catch(err => {
- assert.strictEqual(expectedError, err);
- assertDialogOpenAndEnabled();
- });
+ return element.send(true, false).then(() => {
+ assert.isFalse(startReviewStub.called);
});
-
- suite('pending diff drafts?', () => {
- test('yes', () => {
- const promise = mockPromise();
- const refreshHandler = sandbox.stub();
-
- element.addEventListener('comment-refresh', refreshHandler);
- sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true);
- element.$.restAPI._pendingRequests.sendDiffDraft = [promise];
- element.open();
-
- assert.isFalse(refreshHandler.called);
- assert.isTrue(element._savingComments);
-
- promise.resolve();
-
- return element.$.restAPI.awaitPendingDiffDrafts().then(() => {
- assert.isTrue(refreshHandler.called);
- assert.isFalse(element._savingComments);
- });
- });
-
- test('no', () => {
- sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false);
- element.open();
- assert.notOk(element._savingComments);
- });
- });
- });
-
- test('_computeSendButtonDisabled', () => {
- const fn = element._computeSendButtonDisabled.bind(element);
- assert.isFalse(fn(
- /* buttonLabel= */ 'Start review',
- /* draftCommentThreads= */ [],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- assert.isTrue(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ [],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock nonempty comment draft array, with seding comments.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ true,
- /* disabled= */ false
- ));
- // Mock nonempty comment draft array, without seding comments.
- assert.isTrue(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock nonempty change message.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ 'test',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock reviewers mutated.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ '',
- /* reviewersMutated= */ true,
- /* labelsChanged= */ false,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Mock labels changed.
- assert.isFalse(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ true,
- /* includeComments= */ false,
- /* disabled= */ false
- ));
- // Whole dialog is disabled.
- assert.isTrue(fn(
- /* buttonLabel= */ 'Send',
- /* draftCommentThreads= */ {},
- /* text= */ '',
- /* reviewersMutated= */ false,
- /* labelsChanged= */ true,
- /* includeComments= */ false,
- /* disabled= */ true
- ));
- });
-
- test('_submit blocked when no mutations exist', () => {
- const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
- // Stub the below function to avoid side effects from the send promise
- // resolving.
- sandbox.stub(element, '_purgeReviewersPendingRemove');
- element.draftCommentThreads = [];
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isFalse(sendStub.called);
-
- element.draftCommentThreads = [{comments: [{__draft: true}]}];
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button.send'));
- assert.isTrue(sendStub.called);
- });
-
- test('getFocusStops', () => {
- // Setting draftCommentThreads to an empty object causes _sendDisabled to be
- // computed to false.
- element.draftCommentThreads = [];
- assert.equal(element.getFocusStops().end, element.$.cancelButton);
- element.draftCommentThreads = [{comments: [{__draft: true}]}];
- assert.equal(element.getFocusStops().end, element.$.sendButton);
- });
-
- test('setPluginMessage', () => {
- element.setPluginMessage('foo');
- assert.equal(element.$.pluginMessage.textContent, 'foo');
});
});
+
+ suite('start review and save buttons', () => {
+ let sendStub;
+
+ setup(() => {
+ sendStub = sandbox.stub(element, 'send', () => Promise.resolve());
+ element.canBeStarted = true;
+ // Flush to make both Start/Save buttons appear in DOM.
+ flushAsynchronousOperations();
+ });
+
+ test('start review sets ready', () => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.send'));
+ flushAsynchronousOperations();
+ assert.isTrue(sendStub.calledWith(true, true));
+ });
+
+ test('save review doesn\'t set ready', () => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+ flushAsynchronousOperations();
+ assert.isTrue(sendStub.calledWith(true, false));
+ });
+ });
+
+ test('buttons disabled until all API calls are resolved', () => {
+ stubSaveReview(review => {
+ return {ready: true};
+ });
+ return element.send(true, true).then(() => {
+ assert.isFalse(element.disabled);
+ });
+ });
+
+ suite('error handling', () => {
+ const expectedDraft = 'draft';
+ const expectedError = new Error('test');
+
+ setup(() => {
+ element.draft = expectedDraft;
+ });
+
+ function assertDialogOpenAndEnabled() {
+ assert.strictEqual(expectedDraft, element.draft);
+ assert.isFalse(element.disabled);
+ }
+
+ test('error occurs in _saveReview', () => {
+ stubSaveReview(review => {
+ throw expectedError;
+ });
+ return element.send(true, true).catch(err => {
+ assert.strictEqual(expectedError, err);
+ assertDialogOpenAndEnabled();
+ });
+ });
+
+ suite('pending diff drafts?', () => {
+ test('yes', () => {
+ const promise = mockPromise();
+ const refreshHandler = sandbox.stub();
+
+ element.addEventListener('comment-refresh', refreshHandler);
+ sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true);
+ element.$.restAPI._pendingRequests.sendDiffDraft = [promise];
+ element.open();
+
+ assert.isFalse(refreshHandler.called);
+ assert.isTrue(element._savingComments);
+
+ promise.resolve();
+
+ return element.$.restAPI.awaitPendingDiffDrafts().then(() => {
+ assert.isTrue(refreshHandler.called);
+ assert.isFalse(element._savingComments);
+ });
+ });
+
+ test('no', () => {
+ sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false);
+ element.open();
+ assert.notOk(element._savingComments);
+ });
+ });
+ });
+
+ test('_computeSendButtonDisabled', () => {
+ const fn = element._computeSendButtonDisabled.bind(element);
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Start review',
+ /* draftCommentThreads= */ [],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ assert.isTrue(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ [],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock nonempty comment draft array, with seding comments.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ true,
+ /* disabled= */ false
+ ));
+ // Mock nonempty comment draft array, without seding comments.
+ assert.isTrue(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ [{comments: [{__draft: true}]}],
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock nonempty change message.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ 'test',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock reviewers mutated.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ '',
+ /* reviewersMutated= */ true,
+ /* labelsChanged= */ false,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Mock labels changed.
+ assert.isFalse(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ true,
+ /* includeComments= */ false,
+ /* disabled= */ false
+ ));
+ // Whole dialog is disabled.
+ assert.isTrue(fn(
+ /* buttonLabel= */ 'Send',
+ /* draftCommentThreads= */ {},
+ /* text= */ '',
+ /* reviewersMutated= */ false,
+ /* labelsChanged= */ true,
+ /* includeComments= */ false,
+ /* disabled= */ true
+ ));
+ });
+
+ test('_submit blocked when no mutations exist', () => {
+ const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+ // Stub the below function to avoid side effects from the send promise
+ // resolving.
+ sandbox.stub(element, '_purgeReviewersPendingRemove');
+ element.draftCommentThreads = [];
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isFalse(sendStub.called);
+
+ element.draftCommentThreads = [{comments: [{__draft: true}]}];
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button.send'));
+ assert.isTrue(sendStub.called);
+ });
+
+ test('getFocusStops', () => {
+ // Setting draftCommentThreads to an empty object causes _sendDisabled to be
+ // computed to false.
+ element.draftCommentThreads = [];
+ assert.equal(element.getFocusStops().end, element.$.cancelButton);
+ element.draftCommentThreads = [{comments: [{__draft: true}]}];
+ assert.equal(element.getFocusStops().end, element.$.sendButton);
+ });
+
+ test('setPluginMessage', () => {
+ element.setPluginMessage('foo');
+ assert.equal(element.$.pluginMessage.textContent, 'foo');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index ddc6275..a74ec5f 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.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,276 +14,288 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-account-chip/gr-account-chip.js';
+import '../../shared/gr-button/gr-button.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-reviewer-list_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrReviewerList extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-reviewer-list'; }
+ /**
+ * Fired when the "Add reviewer..." button is tapped.
+ *
+ * @event show-reply-dialog
+ */
+
+ static get properties() {
+ return {
+ change: Object,
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ mutable: {
+ type: Boolean,
+ value: false,
+ },
+ reviewersOnly: {
+ type: Boolean,
+ value: false,
+ },
+ ccsOnly: {
+ type: Boolean,
+ value: false,
+ },
+ maxReviewersDisplayed: Number,
+
+ _displayedReviewers: {
+ type: Array,
+ value() { return []; },
+ },
+ _reviewers: {
+ type: Array,
+ value() { return []; },
+ },
+ _showInput: {
+ type: Boolean,
+ value: false,
+ },
+ _addLabel: {
+ type: String,
+ computed: '_computeAddLabel(ccsOnly)',
+ },
+ _hiddenReviewerCount: {
+ type: Number,
+ computed: '_computeHiddenCount(_reviewers, _displayedReviewers)',
+ },
+
+ // Used for testing.
+ _lastAutocompleteRequest: Object,
+ _xhrPromise: Object,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_reviewersChanged(change.reviewers.*, change.owner)',
+ ];
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Converts change.permitted_labels to an array of hashes of label keys to
+ * numeric scores.
+ * Example:
+ * [{
+ * 'Code-Review': ['-1', ' 0', '+1']
+ * }]
+ * will be converted to
+ * [{
+ * label: 'Code-Review',
+ * scores: [-1, 0, 1]
+ * }]
*/
- class GrReviewerList extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-reviewer-list'; }
- /**
- * Fired when the "Add reviewer..." button is tapped.
- *
- * @event show-reply-dialog
- */
-
- static get properties() {
+ _permittedLabelsToNumericScores(labels) {
+ if (!labels) return [];
+ return Object.keys(labels).map(label => {
return {
- change: Object,
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- mutable: {
- type: Boolean,
- value: false,
- },
- reviewersOnly: {
- type: Boolean,
- value: false,
- },
- ccsOnly: {
- type: Boolean,
- value: false,
- },
- maxReviewersDisplayed: Number,
-
- _displayedReviewers: {
- type: Array,
- value() { return []; },
- },
- _reviewers: {
- type: Array,
- value() { return []; },
- },
- _showInput: {
- type: Boolean,
- value: false,
- },
- _addLabel: {
- type: String,
- computed: '_computeAddLabel(ccsOnly)',
- },
- _hiddenReviewerCount: {
- type: Number,
- computed: '_computeHiddenCount(_reviewers, _displayedReviewers)',
- },
-
- // Used for testing.
- _lastAutocompleteRequest: Object,
- _xhrPromise: Object,
+ label,
+ scores: labels[label].map(v => parseInt(v, 10)),
};
- }
+ });
+ }
- static get observers() {
- return [
- '_reviewersChanged(change.reviewers.*, change.owner)',
- ];
- }
+ /**
+ * Returns hash of labels to max permitted score.
+ *
+ * @param {!Object} change
+ * @returns {!Object} labels to max permitted scores hash
+ */
+ _getMaxPermittedScores(change) {
+ return this._permittedLabelsToNumericScores(change.permitted_labels)
+ .map(({label, scores}) => {
+ return {
+ [label]: scores
+ .map(v => parseInt(v, 10))
+ .reduce((a, b) => Math.max(a, b))};
+ })
+ .reduce((acc, i) => Object.assign(acc, i), {});
+ }
- /**
- * Converts change.permitted_labels to an array of hashes of label keys to
- * numeric scores.
- * Example:
- * [{
- * 'Code-Review': ['-1', ' 0', '+1']
- * }]
- * will be converted to
- * [{
- * label: 'Code-Review',
- * scores: [-1, 0, 1]
- * }]
- */
- _permittedLabelsToNumericScores(labels) {
- if (!labels) return [];
- return Object.keys(labels).map(label => {
- return {
- label,
- scores: labels[label].map(v => parseInt(v, 10)),
- };
- });
- }
-
- /**
- * Returns hash of labels to max permitted score.
- *
- * @param {!Object} change
- * @returns {!Object} labels to max permitted scores hash
- */
- _getMaxPermittedScores(change) {
- return this._permittedLabelsToNumericScores(change.permitted_labels)
- .map(({label, scores}) => {
- return {
- [label]: scores
- .map(v => parseInt(v, 10))
- .reduce((a, b) => Math.max(a, b))};
- })
- .reduce((acc, i) => Object.assign(acc, i), {});
- }
-
- /**
- * Returns max permitted score for reviewer.
- *
- * @param {!Object} reviewer
- * @param {!Object} change
- * @param {string} label
- * @return {number}
- */
- _getReviewerPermittedScore(reviewer, change, label) {
- // Note (issue 7874): sometimes the "all" list is not included in change
- // detail responses, even when DETAILED_LABELS is included in options.
- if (!change.labels[label].all) { return NaN; }
- const detailed = change.labels[label].all.filter(
- ({_account_id}) => reviewer._account_id === _account_id).pop();
- if (!detailed) {
- return NaN;
- }
- if (detailed.hasOwnProperty('permitted_voting_range')) {
- return detailed.permitted_voting_range.max;
- } else if (detailed.hasOwnProperty('value')) {
- // If preset, user can vote on the label.
- return 0;
- }
+ /**
+ * Returns max permitted score for reviewer.
+ *
+ * @param {!Object} reviewer
+ * @param {!Object} change
+ * @param {string} label
+ * @return {number}
+ */
+ _getReviewerPermittedScore(reviewer, change, label) {
+ // Note (issue 7874): sometimes the "all" list is not included in change
+ // detail responses, even when DETAILED_LABELS is included in options.
+ if (!change.labels[label].all) { return NaN; }
+ const detailed = change.labels[label].all.filter(
+ ({_account_id}) => reviewer._account_id === _account_id).pop();
+ if (!detailed) {
return NaN;
}
+ if (detailed.hasOwnProperty('permitted_voting_range')) {
+ return detailed.permitted_voting_range.max;
+ } else if (detailed.hasOwnProperty('value')) {
+ // If preset, user can vote on the label.
+ return 0;
+ }
+ return NaN;
+ }
- _computeReviewerTooltip(reviewer, change) {
- if (!change || !change.labels) { return ''; }
- const maxScores = [];
- const maxPermitted = this._getMaxPermittedScores(change);
- for (const label of Object.keys(change.labels)) {
- const maxScore =
- this._getReviewerPermittedScore(reviewer, change, label);
- if (isNaN(maxScore) || maxScore < 0) { continue; }
- if (maxScore > 0 && maxScore === maxPermitted[label]) {
- maxScores.push(`${label}: +${maxScore}`);
- } else {
- maxScores.push(`${label}`);
- }
- }
- if (maxScores.length) {
- return 'Votable: ' + maxScores.join(', ');
+ _computeReviewerTooltip(reviewer, change) {
+ if (!change || !change.labels) { return ''; }
+ const maxScores = [];
+ const maxPermitted = this._getMaxPermittedScores(change);
+ for (const label of Object.keys(change.labels)) {
+ const maxScore =
+ this._getReviewerPermittedScore(reviewer, change, label);
+ if (isNaN(maxScore) || maxScore < 0) { continue; }
+ if (maxScore > 0 && maxScore === maxPermitted[label]) {
+ maxScores.push(`${label}: +${maxScore}`);
} else {
- return '';
+ maxScores.push(`${label}`);
}
}
-
- _reviewersChanged(changeRecord, owner) {
- // Polymer 2: check for undefined
- if ([changeRecord, owner].some(arg => arg === undefined)) {
- return;
- }
-
- let result = [];
- const reviewers = changeRecord.base;
- for (const key in reviewers) {
- if (this.reviewersOnly && key !== 'REVIEWER') {
- continue;
- }
- if (this.ccsOnly && key !== 'CC') {
- continue;
- }
- if (key === 'REVIEWER' || key === 'CC') {
- result = result.concat(reviewers[key]);
- }
- }
- this._reviewers = result
- .filter(reviewer => reviewer._account_id != owner._account_id);
-
- // If there is one or two more than the max reviewers, don't show the
- // 'show more' button, because it takes up just as much space.
- if (this.maxReviewersDisplayed &&
- this._reviewers.length > this.maxReviewersDisplayed + 2) {
- this._displayedReviewers =
- this._reviewers.slice(0, this.maxReviewersDisplayed);
- } else {
- this._displayedReviewers = this._reviewers;
- }
- }
-
- _computeHiddenCount(reviewers, displayedReviewers) {
- // Polymer 2: check for undefined
- if ([reviewers, displayedReviewers].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return reviewers.length - displayedReviewers.length;
- }
-
- _computeCanRemoveReviewer(reviewer, mutable) {
- if (!mutable) { return false; }
-
- let current;
- for (let i = 0; i < this.change.removable_reviewers.length; i++) {
- current = this.change.removable_reviewers[i];
- if (current._account_id === reviewer._account_id ||
- (!reviewer._account_id && current.email === reviewer.email)) {
- return true;
- }
- }
- return false;
- }
-
- _handleRemove(e) {
- e.preventDefault();
- const target = Polymer.dom(e).rootTarget;
- if (!target.account) { return; }
- const accountID = target.account._account_id || target.account.email;
- this.disabled = true;
- this._xhrPromise = this._removeReviewer(accountID).then(response => {
- this.disabled = false;
- if (!response.ok) { return response; }
-
- const reviewers = this.change.reviewers;
-
- for (const type of ['REVIEWER', 'CC']) {
- reviewers[type] = reviewers[type] || [];
- for (let i = 0; i < reviewers[type].length; i++) {
- if (reviewers[type][i]._account_id == accountID ||
- reviewers[type][i].email == accountID) {
- this.splice('change.reviewers.' + type, i, 1);
- break;
- }
- }
- }
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
- }
-
- _handleAddTap(e) {
- e.preventDefault();
- const value = {};
- if (this.reviewersOnly) {
- value.reviewersOnly = true;
- }
- if (this.ccsOnly) {
- value.ccsOnly = true;
- }
- this.fire('show-reply-dialog', {value});
- }
-
- _handleViewAll(e) {
- this._displayedReviewers = this._reviewers;
- }
-
- _removeReviewer(id) {
- return this.$.restAPI.removeChangeReviewer(this.change._number, id);
- }
-
- _computeAddLabel(ccsOnly) {
- return ccsOnly ? 'Add CC' : 'Add reviewer';
+ if (maxScores.length) {
+ return 'Votable: ' + maxScores.join(', ');
+ } else {
+ return '';
}
}
- customElements.define(GrReviewerList.is, GrReviewerList);
-})();
+ _reviewersChanged(changeRecord, owner) {
+ // Polymer 2: check for undefined
+ if ([changeRecord, owner].some(arg => arg === undefined)) {
+ return;
+ }
+
+ let result = [];
+ const reviewers = changeRecord.base;
+ for (const key in reviewers) {
+ if (this.reviewersOnly && key !== 'REVIEWER') {
+ continue;
+ }
+ if (this.ccsOnly && key !== 'CC') {
+ continue;
+ }
+ if (key === 'REVIEWER' || key === 'CC') {
+ result = result.concat(reviewers[key]);
+ }
+ }
+ this._reviewers = result
+ .filter(reviewer => reviewer._account_id != owner._account_id);
+
+ // If there is one or two more than the max reviewers, don't show the
+ // 'show more' button, because it takes up just as much space.
+ if (this.maxReviewersDisplayed &&
+ this._reviewers.length > this.maxReviewersDisplayed + 2) {
+ this._displayedReviewers =
+ this._reviewers.slice(0, this.maxReviewersDisplayed);
+ } else {
+ this._displayedReviewers = this._reviewers;
+ }
+ }
+
+ _computeHiddenCount(reviewers, displayedReviewers) {
+ // Polymer 2: check for undefined
+ if ([reviewers, displayedReviewers].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return reviewers.length - displayedReviewers.length;
+ }
+
+ _computeCanRemoveReviewer(reviewer, mutable) {
+ if (!mutable) { return false; }
+
+ let current;
+ for (let i = 0; i < this.change.removable_reviewers.length; i++) {
+ current = this.change.removable_reviewers[i];
+ if (current._account_id === reviewer._account_id ||
+ (!reviewer._account_id && current.email === reviewer.email)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ _handleRemove(e) {
+ e.preventDefault();
+ const target = dom(e).rootTarget;
+ if (!target.account) { return; }
+ const accountID = target.account._account_id || target.account.email;
+ this.disabled = true;
+ this._xhrPromise = this._removeReviewer(accountID).then(response => {
+ this.disabled = false;
+ if (!response.ok) { return response; }
+
+ const reviewers = this.change.reviewers;
+
+ for (const type of ['REVIEWER', 'CC']) {
+ reviewers[type] = reviewers[type] || [];
+ for (let i = 0; i < reviewers[type].length; i++) {
+ if (reviewers[type][i]._account_id == accountID ||
+ reviewers[type][i].email == accountID) {
+ this.splice('change.reviewers.' + type, i, 1);
+ break;
+ }
+ }
+ }
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
+ }
+
+ _handleAddTap(e) {
+ e.preventDefault();
+ const value = {};
+ if (this.reviewersOnly) {
+ value.reviewersOnly = true;
+ }
+ if (this.ccsOnly) {
+ value.ccsOnly = true;
+ }
+ this.fire('show-reply-dialog', {value});
+ }
+
+ _handleViewAll(e) {
+ this._displayedReviewers = this._reviewers;
+ }
+
+ _removeReviewer(id) {
+ return this.$.restAPI.removeChangeReviewer(this.change._number, id);
+ }
+
+ _computeAddLabel(ccsOnly) {
+ return ccsOnly ? 'Add CC' : 'Add reviewer';
+ }
+}
+
+customElements.define(GrReviewerList.is, GrReviewerList);
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
index 132ce11..bf7db12 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-button/gr-button.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-reviewer-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -51,26 +44,13 @@
</style>
<div class="container">
<template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer">
- <gr-account-chip class="reviewer" account="[[reviewer]]"
- on-remove="_handleRemove"
- additional-text="[[_computeReviewerTooltip(reviewer, change)]]"
- removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
+ <gr-account-chip class="reviewer" account="[[reviewer]]" on-remove="_handleRemove" additional-text="[[_computeReviewerTooltip(reviewer, change)]]" removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
</gr-account-chip>
</template>
- <gr-button
- class="hiddenReviewers"
- link
- hidden$="[[!_hiddenReviewerCount]]"
- on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button>
- <div class="controlsContainer" hidden$="[[!mutable]]">
- <gr-button
- link
- id="addReviewer"
- class="addReviewer"
- on-click="_handleAddTap">[[_addLabel]]</gr-button>
+ <gr-button class="hiddenReviewers" link="" hidden\$="[[!_hiddenReviewerCount]]" on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button>
+ <div class="controlsContainer" hidden\$="[[!mutable]]">
+ <gr-button link="" id="addReviewer" class="addReviewer" on-click="_handleAddTap">[[_addLabel]]</gr-button>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-reviewer-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index be65a86..627fa10 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_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-reviewer-list</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-reviewer-list.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-reviewer-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-reviewer-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,78 +40,66 @@
</template>
</test-fixture>
-<script>
- suite('gr-reviewer-list 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-reviewer-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-reviewer-list tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- removeChangeReviewer() {
- return Promise.resolve({ok: true});
- },
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ removeChangeReviewer() {
+ return Promise.resolve({ok: true});
+ },
});
+ });
- teardown(() => {
- sandbox.restore();
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('controls hidden on immutable element', () => {
+ element.mutable = false;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.controlsContainer').hasAttribute('hidden'));
+ element.mutable = true;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.controlsContainer').hasAttribute('hidden'));
+ });
+
+ test('add reviewer button opens reply dialog', done => {
+ element.addEventListener('show-reply-dialog', () => {
+ done();
});
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.addReviewer'));
+ });
- test('controls hidden on immutable element', () => {
- element.mutable = false;
- assert.isTrue(element.shadowRoot
- .querySelector('.controlsContainer').hasAttribute('hidden'));
- element.mutable = true;
- assert.isFalse(element.shadowRoot
- .querySelector('.controlsContainer').hasAttribute('hidden'));
- });
-
- test('add reviewer button opens reply dialog', done => {
- element.addEventListener('show-reply-dialog', () => {
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('.addReviewer'));
- });
-
- test('only show remove for removable reviewers', () => {
- element.mutable = true;
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- REVIEWER: [
- {
- _account_id: 2,
- name: 'Bojack Horseman',
- email: 'SecretariatRulez96@hotmail.com',
- },
- {
- _account_id: 3,
- name: 'Pinky Penguin',
- },
- ],
- CC: [
- {
- _account_id: 4,
- name: 'Diane Nguyen',
- email: 'macarthurfellow2B@juno.com',
- },
- {
- email: 'test@e.mail',
- },
- ],
- },
- removable_reviewers: [
+ test('only show remove for removable reviewers', () => {
+ element.mutable = true;
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ REVIEWER: [
+ {
+ _account_id: 2,
+ name: 'Bojack Horseman',
+ email: 'SecretariatRulez96@hotmail.com',
+ },
{
_account_id: 3,
name: 'Pinky Penguin',
},
+ ],
+ CC: [
{
_account_id: 4,
name: 'Diane Nguyen',
@@ -116,230 +109,245 @@
email: 'test@e.mail',
},
],
- };
- flushAsynchronousOperations();
- const chips =
- Polymer.dom(element.root).querySelectorAll('gr-account-chip');
- assert.equal(chips.length, 4);
+ },
+ removable_reviewers: [
+ {
+ _account_id: 3,
+ name: 'Pinky Penguin',
+ },
+ {
+ _account_id: 4,
+ name: 'Diane Nguyen',
+ email: 'macarthurfellow2B@juno.com',
+ },
+ {
+ email: 'test@e.mail',
+ },
+ ],
+ };
+ flushAsynchronousOperations();
+ const chips =
+ dom(element.root).querySelectorAll('gr-account-chip');
+ assert.equal(chips.length, 4);
- for (const el of Array.from(chips)) {
- const accountID = el.account._account_id || el.account.email;
- assert.ok(accountID);
+ for (const el of Array.from(chips)) {
+ const accountID = el.account._account_id || el.account.email;
+ assert.ok(accountID);
- const buttonEl = el.shadowRoot
- .querySelector('gr-button');
- assert.isNotNull(buttonEl);
- if (accountID == 2) {
- assert.isTrue(buttonEl.hasAttribute('hidden'));
- } else {
- assert.isFalse(buttonEl.hasAttribute('hidden'));
- }
+ const buttonEl = el.shadowRoot
+ .querySelector('gr-button');
+ assert.isNotNull(buttonEl);
+ if (accountID == 2) {
+ assert.isTrue(buttonEl.hasAttribute('hidden'));
+ } else {
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
}
- });
-
- test('tracking reviewers and ccs', () => {
- let counter = 0;
- function makeAccount() {
- return {_account_id: counter++};
- }
-
- const owner = makeAccount();
- const reviewer = makeAccount();
- const cc = makeAccount();
- const reviewers = {
- REMOVED: [makeAccount()],
- REVIEWER: [owner, reviewer],
- CC: [owner, cc],
- };
-
- element.ccsOnly = false;
- element.reviewersOnly = false;
- element.change = {
- owner,
- reviewers,
- };
- assert.deepEqual(element._reviewers, [reviewer, cc]);
-
- element.reviewersOnly = true;
- element.change = {
- owner,
- reviewers,
- };
- assert.deepEqual(element._reviewers, [reviewer]);
-
- element.ccsOnly = true;
- element.reviewersOnly = false;
- element.change = {
- owner,
- reviewers,
- };
- assert.deepEqual(element._reviewers, [cc]);
- });
-
- test('_handleAddTap passes mode with event', () => {
- const fireStub = sandbox.stub(element, 'fire');
- const e = {preventDefault() {}};
-
- element.ccsOnly = false;
- element.reviewersOnly = false;
- element._handleAddTap(e);
- assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}}));
-
- element.reviewersOnly = true;
- element._handleAddTap(e);
- assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
- {value: {reviewersOnly: true}}));
-
- element.ccsOnly = true;
- element.reviewersOnly = false;
- element._handleAddTap(e);
- assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
- {value: {ccsOnly: true}}));
- });
-
- test('no show all reviewers button with 6 reviewers', () => {
- const reviewers = [];
- element.maxReviewersDisplayed = 5;
- for (let i = 0; i < 6; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 6);
- assert.equal(element._reviewers.length, 6);
- assert.isTrue(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('show all reviewers button with 8 reviewers', () => {
- const reviewers = [];
- element.maxReviewersDisplayed = 5;
- for (let i = 0; i < 8; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 3);
- assert.equal(element._displayedReviewers.length, 5);
- assert.equal(element._reviewers.length, 8);
- assert.isFalse(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('no maxReviewersDisplayed', () => {
- const reviewers = [];
- for (let i = 0; i < 7; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 7);
- assert.equal(element._reviewers.length, 7);
- assert.isTrue(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('show all reviewers button', () => {
- const reviewers = [];
- element.maxReviewersDisplayed = 5;
- for (let i = 0; i < 100; i++) {
- reviewers.push(
- {email: i+'reviewer@google.com', name: 'reviewer-' + i});
- }
- element.ccsOnly = true;
-
- element.change = {
- owner: {
- _account_id: 1,
- },
- reviewers: {
- CC: reviewers,
- },
- };
- assert.equal(element._hiddenReviewerCount, 95);
- assert.equal(element._displayedReviewers.length, 5);
- assert.equal(element._reviewers.length, 100);
- assert.isFalse(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.hiddenReviewers'));
-
- assert.equal(element._hiddenReviewerCount, 0);
- assert.equal(element._displayedReviewers.length, 100);
- assert.equal(element._reviewers.length, 100);
- assert.isTrue(element.shadowRoot
- .querySelector('.hiddenReviewers').hidden);
- });
-
- test('votable labels', () => {
- const change = {
- labels: {
- Foo: {
- all: [{_account_id: 7, permitted_voting_range: {max: 2}}],
- },
- Bar: {
- all: [{_account_id: 1, permitted_voting_range: {max: 1}},
- {_account_id: 7, permitted_voting_range: {max: 1}}],
- },
- FooBar: {
- all: [{_account_id: 7, value: 0}],
- },
- },
- permitted_labels: {
- Foo: ['-1', ' 0', '+1', '+2'],
- FooBar: ['-1', ' 0'],
- },
- };
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 1}, change),
- 'Votable: Bar');
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 7}, change),
- 'Votable: Foo: +2, Bar, FooBar');
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 2}, change),
- '');
- });
-
- test('fails gracefully when all is not included', () => {
- const change = {
- labels: {Foo: {}},
- permitted_labels: {
- Foo: ['-1', ' 0', '+1', '+2'],
- },
- };
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 1}, change), '');
- });
+ }
});
+
+ test('tracking reviewers and ccs', () => {
+ let counter = 0;
+ function makeAccount() {
+ return {_account_id: counter++};
+ }
+
+ const owner = makeAccount();
+ const reviewer = makeAccount();
+ const cc = makeAccount();
+ const reviewers = {
+ REMOVED: [makeAccount()],
+ REVIEWER: [owner, reviewer],
+ CC: [owner, cc],
+ };
+
+ element.ccsOnly = false;
+ element.reviewersOnly = false;
+ element.change = {
+ owner,
+ reviewers,
+ };
+ assert.deepEqual(element._reviewers, [reviewer, cc]);
+
+ element.reviewersOnly = true;
+ element.change = {
+ owner,
+ reviewers,
+ };
+ assert.deepEqual(element._reviewers, [reviewer]);
+
+ element.ccsOnly = true;
+ element.reviewersOnly = false;
+ element.change = {
+ owner,
+ reviewers,
+ };
+ assert.deepEqual(element._reviewers, [cc]);
+ });
+
+ test('_handleAddTap passes mode with event', () => {
+ const fireStub = sandbox.stub(element, 'fire');
+ const e = {preventDefault() {}};
+
+ element.ccsOnly = false;
+ element.reviewersOnly = false;
+ element._handleAddTap(e);
+ assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}}));
+
+ element.reviewersOnly = true;
+ element._handleAddTap(e);
+ assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+ {value: {reviewersOnly: true}}));
+
+ element.ccsOnly = true;
+ element.reviewersOnly = false;
+ element._handleAddTap(e);
+ assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog',
+ {value: {ccsOnly: true}}));
+ });
+
+ test('no show all reviewers button with 6 reviewers', () => {
+ const reviewers = [];
+ element.maxReviewersDisplayed = 5;
+ for (let i = 0; i < 6; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 0);
+ assert.equal(element._displayedReviewers.length, 6);
+ assert.equal(element._reviewers.length, 6);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('show all reviewers button with 8 reviewers', () => {
+ const reviewers = [];
+ element.maxReviewersDisplayed = 5;
+ for (let i = 0; i < 8; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 3);
+ assert.equal(element._displayedReviewers.length, 5);
+ assert.equal(element._reviewers.length, 8);
+ assert.isFalse(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('no maxReviewersDisplayed', () => {
+ const reviewers = [];
+ for (let i = 0; i < 7; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 0);
+ assert.equal(element._displayedReviewers.length, 7);
+ assert.equal(element._reviewers.length, 7);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('show all reviewers button', () => {
+ const reviewers = [];
+ element.maxReviewersDisplayed = 5;
+ for (let i = 0; i < 100; i++) {
+ reviewers.push(
+ {email: i+'reviewer@google.com', name: 'reviewer-' + i});
+ }
+ element.ccsOnly = true;
+
+ element.change = {
+ owner: {
+ _account_id: 1,
+ },
+ reviewers: {
+ CC: reviewers,
+ },
+ };
+ assert.equal(element._hiddenReviewerCount, 95);
+ assert.equal(element._displayedReviewers.length, 5);
+ assert.equal(element._reviewers.length, 100);
+ assert.isFalse(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.hiddenReviewers'));
+
+ assert.equal(element._hiddenReviewerCount, 0);
+ assert.equal(element._displayedReviewers.length, 100);
+ assert.equal(element._reviewers.length, 100);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.hiddenReviewers').hidden);
+ });
+
+ test('votable labels', () => {
+ const change = {
+ labels: {
+ Foo: {
+ all: [{_account_id: 7, permitted_voting_range: {max: 2}}],
+ },
+ Bar: {
+ all: [{_account_id: 1, permitted_voting_range: {max: 1}},
+ {_account_id: 7, permitted_voting_range: {max: 1}}],
+ },
+ FooBar: {
+ all: [{_account_id: 7, value: 0}],
+ },
+ },
+ permitted_labels: {
+ Foo: ['-1', ' 0', '+1', '+2'],
+ FooBar: ['-1', ' 0'],
+ },
+ };
+ assert.strictEqual(
+ element._computeReviewerTooltip({_account_id: 1}, change),
+ 'Votable: Bar');
+ assert.strictEqual(
+ element._computeReviewerTooltip({_account_id: 7}, change),
+ 'Votable: Foo: +2, Bar, FooBar');
+ assert.strictEqual(
+ element._computeReviewerTooltip({_account_id: 2}, change),
+ '');
+ });
+
+ test('fails gracefully when all is not included', () => {
+ const change = {
+ labels: {Foo: {}},
+ permitted_labels: {
+ Foo: ['-1', ' 0', '+1', '+2'],
+ },
+ };
+ assert.strictEqual(
+ element._computeReviewerTooltip({_account_id: 1}, change), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index e99367e..850bfb4 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -14,200 +14,209 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-comment-thread/gr-comment-thread.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.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-thread-list_html.js';
+
+/**
+ * Fired when a comment is saved or deleted
+ *
+ * @event thread-list-modified
+ * @extends Polymer.Element
+ */
+const NO_THREADS_MESSAGE = 'There are no inline comment threads on any diff '
+ + 'for this change.';
+const NO_ROBOT_COMMENTS_THREADS_MESSAGE = 'There are no findings for this ' +
+ 'patchset.';
+const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
+
+class GrThreadList extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-thread-list'; }
+
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: Object,
+ threads: Array,
+ changeNum: String,
+ loggedIn: Boolean,
+ _sortedThreads: {
+ type: Array,
+ },
+ _filteredThreads: {
+ type: Array,
+ computed: '_computeFilteredThreads(_sortedThreads, ' +
+ '_unresolvedOnly, _draftsOnly,' +
+ 'onlyShowRobotCommentsWithHumanReply)',
+ },
+ _unresolvedOnly: {
+ type: Boolean,
+ value: false,
+ },
+ _draftsOnly: {
+ type: Boolean,
+ value: false,
+ },
+ /* Boolean properties used must default to false if passed as attribute
+ by the parent */
+ onlyShowRobotCommentsWithHumanReply: {
+ type: Boolean,
+ value: false,
+ },
+ hideToggleButtons: {
+ type: Boolean,
+ value: false,
+ },
+ tab: {
+ type: String,
+ value: '',
+ },
+ };
+ }
+
+ static get observers() { return ['_computeSortedThreads(threads.*)']; }
+
+ _computeShowDraftToggle(loggedIn) {
+ return loggedIn ? 'show' : '';
+ }
+
+ _computeNoThreadsMessage(tab) {
+ if (tab === FINDINGS_TAB_NAME) {
+ return NO_ROBOT_COMMENTS_THREADS_MESSAGE;
+ }
+ return NO_THREADS_MESSAGE;
+ }
/**
- * Fired when a comment is saved or deleted
+ * Order as follows:
+ * - Unresolved threads with drafts (reverse chronological)
+ * - Unresolved threads without drafts (reverse chronological)
+ * - Resolved threads with drafts (reverse chronological)
+ * - Resolved threads without drafts (reverse chronological)
*
- * @event thread-list-modified
- * @extends Polymer.Element
+ * @param {!Object} changeRecord
*/
- const NO_THREADS_MESSAGE = 'There are no inline comment threads on any diff '
- + 'for this change.';
- const NO_ROBOT_COMMENTS_THREADS_MESSAGE = 'There are no findings for this ' +
- 'patchset.';
- const FINDINGS_TAB_NAME = '__gerrit_internal_findings';
+ _computeSortedThreads(changeRecord) {
+ const threads = changeRecord.base;
+ if (!threads) { return []; }
+ this._updateSortedThreads(threads);
+ }
- class GrThreadList extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-thread-list'; }
-
- static get properties() {
- return {
- /** @type {?} */
- change: Object,
- threads: Array,
- changeNum: String,
- loggedIn: Boolean,
- _sortedThreads: {
- type: Array,
- },
- _filteredThreads: {
- type: Array,
- computed: '_computeFilteredThreads(_sortedThreads, ' +
- '_unresolvedOnly, _draftsOnly,' +
- 'onlyShowRobotCommentsWithHumanReply)',
- },
- _unresolvedOnly: {
- type: Boolean,
- value: false,
- },
- _draftsOnly: {
- type: Boolean,
- value: false,
- },
- /* Boolean properties used must default to false if passed as attribute
- by the parent */
- onlyShowRobotCommentsWithHumanReply: {
- type: Boolean,
- value: false,
- },
- hideToggleButtons: {
- type: Boolean,
- value: false,
- },
- tab: {
- type: String,
- value: '',
- },
- };
- }
-
- static get observers() { return ['_computeSortedThreads(threads.*)']; }
-
- _computeShowDraftToggle(loggedIn) {
- return loggedIn ? 'show' : '';
- }
-
- _computeNoThreadsMessage(tab) {
- if (tab === FINDINGS_TAB_NAME) {
- return NO_ROBOT_COMMENTS_THREADS_MESSAGE;
- }
- return NO_THREADS_MESSAGE;
- }
-
- /**
- * Order as follows:
- * - Unresolved threads with drafts (reverse chronological)
- * - Unresolved threads without drafts (reverse chronological)
- * - Resolved threads with drafts (reverse chronological)
- * - Resolved threads without drafts (reverse chronological)
- *
- * @param {!Object} changeRecord
- */
- _computeSortedThreads(changeRecord) {
- const threads = changeRecord.base;
- if (!threads) { return []; }
- this._updateSortedThreads(threads);
- }
-
- _updateSortedThreads(threads) {
- this._sortedThreads =
- threads.map(this._getThreadWithSortInfo).sort((c1, c2) => {
- const c1Date = c1.__date || util.parseDate(c1.updated);
- const c2Date = c2.__date || util.parseDate(c2.updated);
- const dateCompare = c2Date - c1Date;
- if (c2.unresolved || c1.unresolved) {
- if (!c1.unresolved) { return 1; }
- if (!c2.unresolved) { return -1; }
- }
- if (c2.hasDraft || c1.hasDraft) {
- if (!c1.hasDraft) { return 1; }
- if (!c2.hasDraft) { return -1; }
- }
-
- if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
- return 0;
- }
- return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
- });
- }
-
- _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly,
- onlyShowRobotCommentsWithHumanReply) {
- // Polymer 2: check for undefined
- if ([
- sortedThreads,
- unresolvedOnly,
- draftsOnly,
- onlyShowRobotCommentsWithHumanReply,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return sortedThreads.filter(c => {
- if (draftsOnly) {
- return c.hasDraft;
- } else if (unresolvedOnly) {
- return c.unresolved;
- } else {
- const comments = c && c.thread && c.thread.comments;
- let robotComment = false;
- let humanReplyToRobotComment = false;
- comments.forEach(comment => {
- if (comment.robot_id) {
- robotComment = true;
- } else if (robotComment) {
- // Robot comment exists and human comment exists after it
- humanReplyToRobotComment = true;
- }
- });
- if (robotComment && onlyShowRobotCommentsWithHumanReply) {
- return humanReplyToRobotComment;
+ _updateSortedThreads(threads) {
+ this._sortedThreads =
+ threads.map(this._getThreadWithSortInfo).sort((c1, c2) => {
+ const c1Date = c1.__date || util.parseDate(c1.updated);
+ const c2Date = c2.__date || util.parseDate(c2.updated);
+ const dateCompare = c2Date - c1Date;
+ if (c2.unresolved || c1.unresolved) {
+ if (!c1.unresolved) { return 1; }
+ if (!c2.unresolved) { return -1; }
}
- return c;
- }
- }).map(threadInfo => threadInfo.thread);
+ if (c2.hasDraft || c1.hasDraft) {
+ if (!c1.hasDraft) { return 1; }
+ if (!c2.hasDraft) { return -1; }
+ }
+
+ if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
+ return 0;
+ }
+ return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
+ });
+ }
+
+ _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply) {
+ // Polymer 2: check for undefined
+ if ([
+ sortedThreads,
+ unresolvedOnly,
+ draftsOnly,
+ onlyShowRobotCommentsWithHumanReply,
+ ].some(arg => arg === undefined)) {
+ return undefined;
}
- _getThreadWithSortInfo(thread) {
- const lastComment = thread.comments[thread.comments.length - 1] || {};
-
- const lastNonDraftComment =
- (lastComment.__draft && thread.comments.length > 1) ?
- thread.comments[thread.comments.length - 2] :
- lastComment;
-
- return {
- thread,
- // Use the unresolved bit for the last non draft comment. This is what
- // anybody other than the current user would see.
- unresolved: !!lastNonDraftComment.unresolved,
- hasDraft: !!lastComment.__draft,
- updated: lastComment.updated,
- };
- }
-
- removeThread(rootId) {
- for (let i = 0; i < this.threads.length; i++) {
- if (this.threads[i].rootId === rootId) {
- this.splice('threads', i, 1);
- // Needed to ensure threads get re-rendered in the correct order.
- Polymer.dom.flush();
- return;
+ return sortedThreads.filter(c => {
+ if (draftsOnly) {
+ return c.hasDraft;
+ } else if (unresolvedOnly) {
+ return c.unresolved;
+ } else {
+ const comments = c && c.thread && c.thread.comments;
+ let robotComment = false;
+ let humanReplyToRobotComment = false;
+ comments.forEach(comment => {
+ if (comment.robot_id) {
+ robotComment = true;
+ } else if (robotComment) {
+ // Robot comment exists and human comment exists after it
+ humanReplyToRobotComment = true;
+ }
+ });
+ if (robotComment && onlyShowRobotCommentsWithHumanReply) {
+ return humanReplyToRobotComment;
}
+ return c;
}
- }
+ }).map(threadInfo => threadInfo.thread);
+ }
- _handleThreadDiscard(e) {
- this.removeThread(e.detail.rootId);
- }
+ _getThreadWithSortInfo(thread) {
+ const lastComment = thread.comments[thread.comments.length - 1] || {};
- _handleCommentsChanged(e) {
- // Reset threads so thread computations occur on deep array changes to
- // threads comments that are not observed naturally.
- this._updateSortedThreads(this.threads);
+ const lastNonDraftComment =
+ (lastComment.__draft && thread.comments.length > 1) ?
+ thread.comments[thread.comments.length - 2] :
+ lastComment;
- this.dispatchEvent(new CustomEvent('thread-list-modified',
- {detail: {rootId: e.detail.rootId, path: e.detail.path}}));
- }
+ return {
+ thread,
+ // Use the unresolved bit for the last non draft comment. This is what
+ // anybody other than the current user would see.
+ unresolved: !!lastNonDraftComment.unresolved,
+ hasDraft: !!lastComment.__draft,
+ updated: lastComment.updated,
+ };
+ }
- _isOnParent(side) {
- return !!side;
+ removeThread(rootId) {
+ for (let i = 0; i < this.threads.length; i++) {
+ if (this.threads[i].rootId === rootId) {
+ this.splice('threads', i, 1);
+ // Needed to ensure threads get re-rendered in the correct order.
+ flush();
+ return;
+ }
}
}
- customElements.define(GrThreadList.is, GrThreadList);
-})();
+ _handleThreadDiscard(e) {
+ this.removeThread(e.detail.rootId);
+ }
+
+ _handleCommentsChanged(e) {
+ // Reset threads so thread computations occur on deep array changes to
+ // threads comments that are not observed naturally.
+ this._updateSortedThreads(this.threads);
+
+ this.dispatchEvent(new CustomEvent('thread-list-modified',
+ {detail: {rootId: e.detail.rootId, path: e.detail.path}}));
+ }
+
+ _isOnParent(side) {
+ return !!side;
+ }
+}
+
+customElements.define(GrThreadList.is, GrThreadList);
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
index f04a39a..b9f0b01 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
@@ -1,27 +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="/bower_components/paper-toggle-button/paper-toggle-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
-
-<dom-module id="gr-thread-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
#threads {
display: block;
@@ -62,14 +57,10 @@
<template is="dom-if" if="[[!hideToggleButtons]]">
<div class="header">
<div class="toggleItem">
- <paper-toggle-button
- id="unresolvedToggle"
- checked="{{_unresolvedOnly}}"></paper-toggle-button>
+ <paper-toggle-button id="unresolvedToggle" checked="{{_unresolvedOnly}}"></paper-toggle-button>
Only unresolved threads</div>
- <div class$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]">
- <paper-toggle-button
- id="draftToggle"
- checked="{{_draftsOnly}}"></paper-toggle-button>
+ <div class\$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]">
+ <paper-toggle-button id="draftToggle" checked="{{_draftsOnly}}"></paper-toggle-button>
Only threads with drafts</div>
</div>
</template>
@@ -77,27 +68,8 @@
<template is="dom-if" if="[[!threads.length]]">
[[_computeNoThreadsMessage(tab)]]
</template>
- <template
- is="dom-repeat"
- items="[[_filteredThreads]]"
- as="thread"
- initial-count="5"
- target-framerate="60">
- <gr-comment-thread
- show-file-path
- change-num="[[changeNum]]"
- comments="[[thread.comments]]"
- comment-side="[[thread.commentSide]]"
- project-name="[[change.project]]"
- is-on-parent="[[_isOnParent(thread.commentSide)]]"
- line-num="[[thread.line]]"
- patch-num="[[thread.patchNum]]"
- path="[[thread.path]]"
- root-id="{{thread.rootId}}"
- on-thread-changed="_handleCommentsChanged"
- on-thread-discard="_handleThreadDiscard"></gr-comment-thread>
+ <template is="dom-repeat" items="[[_filteredThreads]]" as="thread" initial-count="5" target-framerate="60">
+ <gr-comment-thread show-file-path="" change-num="[[changeNum]]" comments="[[thread.comments]]" comment-side="[[thread.commentSide]]" project-name="[[change.project]]" is-on-parent="[[_isOnParent(thread.commentSide)]]" line-num="[[thread.line]]" patch-num="[[thread.patchNum]]" path="[[thread.path]]" root-id="{{thread.rootId}}" on-thread-changed="_handleCommentsChanged" on-thread-discard="_handleThreadDiscard"></gr-comment-thread>
</template>
</div>
- </template>
- <script src="gr-thread-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index 30c598a..a2c3620 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_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-thread-list</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-thread-list.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-thread-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-thread-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,338 +40,341 @@
</template>
</test-fixture>
-<script>
- suite('gr-thread-list tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let threadElements;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-thread-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-thread-list tests', () => {
+ let element;
+ let sandbox;
+ let threadElements;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.onlyShowRobotCommentsWithHumanReply = true;
- element.threads = [
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'ecf0b9fa_fe1a5f62',
- line: 5,
- updated: '2018-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.onlyShowRobotCommentsWithHumanReply = true;
+ element.threads = [
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- {
- id: '503008e2_0ab203ee',
- path: '/COMMIT_MSG',
- line: 5,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '2018-02-13 22:48:48.018000000',
- message: 'draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
+ patch_set: 4,
+ id: 'ecf0b9fa_fe1a5f62',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ id: '503008e2_0ab203ee',
+ path: '/COMMIT_MSG',
+ line: 5,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
+ updated: '2018-02-13 22:48:48.018000000',
+ message: 'draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'ecf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: 'test.txt',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'ecf0b9fa_fe1a5f62',
- start_datetime: '2018-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: 'test.txt',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 3,
- id: '09a9fb0a_1484e6cf',
- side: 'PARENT',
- updated: '2018-02-13 22:47:19.000000000',
- message: 'Some comment on another patchset.',
- unresolved: false,
+ patch_set: 3,
+ id: '09a9fb0a_1484e6cf',
+ side: 'PARENT',
+ updated: '2018-02-13 22:47:19.000000000',
+ message: 'Some comment on another patchset.',
+ unresolved: false,
+ },
+ ],
+ patchNum: 3,
+ path: 'test.txt',
+ rootId: '09a9fb0a_1484e6cf',
+ start_datetime: '2018-02-13 22:47:19.000000000',
+ commentSide: 'PARENT',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 3,
- path: 'test.txt',
- rootId: '09a9fb0a_1484e6cf',
- start_datetime: '2018-02-13 22:47:19.000000000',
- commentSide: 'PARENT',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: '8caddf38_44770ec1',
- line: 4,
- updated: '2018-02-13 22:48:40.000000000',
- message: 'Another unresolved comment',
- unresolved: true,
+ patch_set: 2,
+ id: '8caddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-13 22:48:40.000000000',
+ message: 'Another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: '8caddf38_44770ec1',
+ start_datetime: '2018-02-13 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: '8caddf38_44770ec1',
- start_datetime: '2018-02-13 22:48:40.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: 'scaddf38_44770ec1',
- line: 4,
- updated: '2018-02-14 22:48:40.000000000',
- message: 'Yet another unresolved comment',
- unresolved: true,
+ patch_set: 2,
+ id: 'scaddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-14 22:48:40.000000000',
+ message: 'Yet another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: 'scaddf38_44770ec1',
+ start_datetime: '2018-02-14 22:48:40.000000000',
+ },
+ {
+ comments: [
+ {
+ id: 'zcf0b9fa_fe1a5f62',
+ path: '/COMMIT_MSG',
+ line: 6,
+ updated: '2018-02-15 22:48:48.018000000',
+ message: 'resolved draft',
+ unresolved: false,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 6,
+ rootId: 'zcf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-09 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: 'scaddf38_44770ec1',
- start_datetime: '2018-02-14 22:48:40.000000000',
- },
- {
- comments: [
- {
- id: 'zcf0b9fa_fe1a5f62',
- path: '/COMMIT_MSG',
- line: 6,
- updated: '2018-02-15 22:48:48.018000000',
- message: 'resolved draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
+ patch_set: 4,
+ id: 'rc1',
+ line: 5,
+ updated: '2019-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc1',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc1',
+ start_datetime: '2019-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 6,
- rootId: 'zcf0b9fa_fe1a5f62',
- start_datetime: '2018-02-09 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc1',
- line: 5,
- updated: '2019-02-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc1',
+ patch_set: 4,
+ id: 'rc2',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc2',
+ },
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
},
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc1',
- start_datetime: '2019-02-08 18:49:18.000000000',
- },
- {
- comments: [
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc2',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- robot_id: 'rc2',
- },
- {
- __path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'c2_1',
- line: 5,
- updated: '2019-03-08 18:49:18.000000000',
- message: 'test',
- unresolved: true,
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc2',
- start_datetime: '2019-03-08 18:49:18.000000000',
- },
- ];
- flushAsynchronousOperations();
- threadElements = Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread');
- });
+ patch_set: 4,
+ id: 'c2_1',
+ line: 5,
+ updated: '2019-03-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc2',
+ start_datetime: '2019-03-08 18:49:18.000000000',
+ },
+ ];
+ flushAsynchronousOperations();
+ threadElements = dom(element.root)
+ .querySelectorAll('gr-comment-thread');
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('draft toggle only appears when logged in', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.draftToggle')).display,
- 'none');
- element.loggedIn = true;
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.draftToggle')).display,
- 'none');
- });
+ test('draft toggle only appears when logged in', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.draftToggle')).display,
+ 'none');
+ element.loggedIn = true;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.draftToggle')).display,
+ 'none');
+ });
- test('there are five threads by default', () => {
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 5);
- });
+ test('there are five threads by default', () => {
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 5);
+ });
- test('_computeSortedThreads', () => {
- assert.equal(element._sortedThreads.length, 7);
- // Draft and unresolved
- assert.equal(element._sortedThreads[0].thread.rootId,
- 'ecf0b9fa_fe1a5f62');
- // Unresolved robot comment
- assert.equal(element._sortedThreads[1].thread.rootId,
- 'rc2');
- // Unresolved robot comment
- assert.equal(element._sortedThreads[2].thread.rootId,
- 'rc1');
- // unresolved
- assert.equal(element._sortedThreads[3].thread.rootId,
- 'scaddf38_44770ec1');
- // unresolved
- assert.equal(element._sortedThreads[4].thread.rootId,
- '8caddf38_44770ec1');
- // resolved and draft
- assert.equal(element._sortedThreads[5].thread.rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved
- assert.equal(element._sortedThreads[6].thread.rootId,
- '09a9fb0a_1484e6cf');
- });
+ test('_computeSortedThreads', () => {
+ assert.equal(element._sortedThreads.length, 7);
+ // Draft and unresolved
+ assert.equal(element._sortedThreads[0].thread.rootId,
+ 'ecf0b9fa_fe1a5f62');
+ // Unresolved robot comment
+ assert.equal(element._sortedThreads[1].thread.rootId,
+ 'rc2');
+ // Unresolved robot comment
+ assert.equal(element._sortedThreads[2].thread.rootId,
+ 'rc1');
+ // unresolved
+ assert.equal(element._sortedThreads[3].thread.rootId,
+ 'scaddf38_44770ec1');
+ // unresolved
+ assert.equal(element._sortedThreads[4].thread.rootId,
+ '8caddf38_44770ec1');
+ // resolved and draft
+ assert.equal(element._sortedThreads[5].thread.rootId,
+ 'zcf0b9fa_fe1a5f62');
+ // resolved
+ assert.equal(element._sortedThreads[6].thread.rootId,
+ '09a9fb0a_1484e6cf');
+ });
- test('filtered threads do not contain robot comments without reply', () => {
- const thread = element.threads.find(thread => thread.rootId === 'rc1');
- assert.equal(element._filteredThreads.includes(thread), false);
- });
+ test('filtered threads do not contain robot comments without reply', () => {
+ const thread = element.threads.find(thread => thread.rootId === 'rc1');
+ assert.equal(element._filteredThreads.includes(thread), false);
+ });
- test('filtered threads contains robot comments with reply', () => {
- const thread = element.threads.find(thread => thread.rootId === 'rc2');
- assert.equal(element._filteredThreads.includes(thread), true);
- });
+ test('filtered threads contains robot comments with reply', () => {
+ const thread = element.threads.find(thread => thread.rootId === 'rc2');
+ assert.equal(element._filteredThreads.includes(thread), true);
+ });
- test('thread removal', () => {
- threadElements[1].fire('thread-discard', {rootId: 'rc2'});
- flushAsynchronousOperations();
- assert.equal(element._sortedThreads.length, 6);
- assert.equal(element._sortedThreads[0].thread.rootId,
- 'ecf0b9fa_fe1a5f62');
- // Unresolved robot comment
- assert.equal(element._sortedThreads[1].thread.rootId,
- 'rc1');
- // unresolved
- assert.equal(element._sortedThreads[2].thread.rootId,
- 'scaddf38_44770ec1');
- // unresolved
- assert.equal(element._sortedThreads[3].thread.rootId,
- '8caddf38_44770ec1');
- // resolved and draft
- assert.equal(element._sortedThreads[4].thread.rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved
- assert.equal(element._sortedThreads[5].thread.rootId,
- '09a9fb0a_1484e6cf');
- });
+ test('thread removal', () => {
+ threadElements[1].fire('thread-discard', {rootId: 'rc2'});
+ flushAsynchronousOperations();
+ assert.equal(element._sortedThreads.length, 6);
+ assert.equal(element._sortedThreads[0].thread.rootId,
+ 'ecf0b9fa_fe1a5f62');
+ // Unresolved robot comment
+ assert.equal(element._sortedThreads[1].thread.rootId,
+ 'rc1');
+ // unresolved
+ assert.equal(element._sortedThreads[2].thread.rootId,
+ 'scaddf38_44770ec1');
+ // unresolved
+ assert.equal(element._sortedThreads[3].thread.rootId,
+ '8caddf38_44770ec1');
+ // resolved and draft
+ assert.equal(element._sortedThreads[4].thread.rootId,
+ 'zcf0b9fa_fe1a5f62');
+ // resolved
+ assert.equal(element._sortedThreads[5].thread.rootId,
+ '09a9fb0a_1484e6cf');
+ });
- test('toggle unresolved only shows unresolved comments', () => {
- MockInteractions.tap(element.shadowRoot.querySelector(
- '#unresolvedToggle'));
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 5);
- });
+ test('toggle unresolved only shows unresolved comments', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector(
+ '#unresolvedToggle'));
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 5);
+ });
- test('toggle drafts only shows threads with draft comments', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 2);
- });
+ test('toggle drafts only shows threads with draft comments', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 2);
+ });
- test('toggle drafts and unresolved only shows threads with drafts and ' +
- 'publicly unresolved ', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
- MockInteractions.tap(element.shadowRoot.querySelector(
- '#unresolvedToggle'));
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root)
- .querySelectorAll('gr-comment-thread').length, 2);
- });
+ test('toggle drafts and unresolved only shows threads with drafts and ' +
+ 'publicly unresolved ', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
+ MockInteractions.tap(element.shadowRoot.querySelector(
+ '#unresolvedToggle'));
+ flushAsynchronousOperations();
+ assert.equal(dom(element.root)
+ .querySelectorAll('gr-comment-thread').length, 2);
+ });
- test('modification events are consumed and displatched', () => {
- sandbox.spy(element, '_handleCommentsChanged');
- const dispatchSpy = sandbox.stub();
- element.addEventListener('thread-list-modified', dispatchSpy);
- threadElements[0].fire('thread-changed', {
- rootId: 'ecf0b9fa_fe1a5f62', path: '/COMMIT_MSG'});
- assert.isTrue(element._handleCommentsChanged.called);
- assert.isTrue(dispatchSpy.called);
- assert.equal(dispatchSpy.lastCall.args[0].detail.rootId,
- 'ecf0b9fa_fe1a5f62');
- assert.equal(dispatchSpy.lastCall.args[0].detail.path, '/COMMIT_MSG');
- });
+ test('modification events are consumed and displatched', () => {
+ sandbox.spy(element, '_handleCommentsChanged');
+ const dispatchSpy = sandbox.stub();
+ element.addEventListener('thread-list-modified', dispatchSpy);
+ threadElements[0].fire('thread-changed', {
+ rootId: 'ecf0b9fa_fe1a5f62', path: '/COMMIT_MSG'});
+ assert.isTrue(element._handleCommentsChanged.called);
+ assert.isTrue(dispatchSpy.called);
+ assert.equal(dispatchSpy.lastCall.args[0].detail.rootId,
+ 'ecf0b9fa_fe1a5f62');
+ assert.equal(dispatchSpy.lastCall.args[0].detail.path, '/COMMIT_MSG');
+ });
- suite('findings tab', () => {
- setup(done => {
- element.hideToggleButtons = true;
- flush(() => {
- done();
- });
+ suite('findings tab', () => {
+ setup(done => {
+ element.hideToggleButtons = true;
+ flush(() => {
+ done();
});
- test('toggle buttons are hidden', () => {
- assert.equal(element.shadowRoot.querySelector('.header').style.display,
- 'none');
- });
+ });
+ test('toggle buttons are hidden', () => {
+ assert.equal(element.shadowRoot.querySelector('.header').style.display,
+ 'none');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index 60cbd42..1ab3926 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -14,133 +14,144 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const COMMIT_COMMAND = 'git add . && git commit --amend --no-edit';
- const PUSH_COMMAND_PREFIX = 'git push origin HEAD:refs/for/';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-shell-command/gr-shell-command.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-upload-help-dialog_html.js';
- // Command names correspond to download plugin definitions.
- const PREFERRED_FETCH_COMMAND_ORDER = [
- 'checkout',
- 'cherry pick',
- 'pull',
- ];
+const COMMIT_COMMAND = 'git add . && git commit --amend --no-edit';
+const PUSH_COMMAND_PREFIX = 'git push origin HEAD:refs/for/';
+// Command names correspond to download plugin definitions.
+const PREFERRED_FETCH_COMMAND_ORDER = [
+ 'checkout',
+ 'cherry pick',
+ 'pull',
+];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrUploadHelpDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-upload-help-dialog'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the user presses the close button.
+ *
+ * @event close
*/
- class GrUploadHelpDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-upload-help-dialog'; }
- /**
- * Fired when the user presses the close button.
- *
- * @event close
- */
- static get properties() {
- return {
- revision: Object,
- targetBranch: String,
- _commitCommand: {
- type: String,
- value: COMMIT_COMMAND,
- readOnly: true,
- },
- _fetchCommand: {
- type: String,
- computed: '_computeFetchCommand(revision, ' +
- '_preferredDownloadCommand, _preferredDownloadScheme)',
- },
- _preferredDownloadCommand: String,
- _preferredDownloadScheme: String,
- _pushCommand: {
- type: String,
- computed: '_computePushCommand(targetBranch)',
- },
- };
- }
+ static get properties() {
+ return {
+ revision: Object,
+ targetBranch: String,
+ _commitCommand: {
+ type: String,
+ value: COMMIT_COMMAND,
+ readOnly: true,
+ },
+ _fetchCommand: {
+ type: String,
+ computed: '_computeFetchCommand(revision, ' +
+ '_preferredDownloadCommand, _preferredDownloadScheme)',
+ },
+ _preferredDownloadCommand: String,
+ _preferredDownloadScheme: String,
+ _pushCommand: {
+ type: String,
+ computed: '_computePushCommand(targetBranch)',
+ },
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- this.$.restAPI.getLoggedIn()
- .then(loggedIn => {
- if (loggedIn) {
- return this.$.restAPI.getPreferences();
- }
- })
- .then(prefs => {
- if (prefs) {
- this._preferredDownloadCommand = prefs.download_command;
- this._preferredDownloadScheme = prefs.download_scheme;
- }
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this.$.restAPI.getLoggedIn()
+ .then(loggedIn => {
+ if (loggedIn) {
+ return this.$.restAPI.getPreferences();
+ }
+ })
+ .then(prefs => {
+ if (prefs) {
+ this._preferredDownloadCommand = prefs.download_command;
+ this._preferredDownloadScheme = prefs.download_scheme;
+ }
+ });
+ }
- _handleCloseTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('close', null, {bubbles: false});
- }
+ _handleCloseTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('close', null, {bubbles: false});
+ }
- _computeFetchCommand(revision, preferredDownloadCommand,
- preferredDownloadScheme) {
- // Polymer 2: check for undefined
- if ([
- revision,
- preferredDownloadCommand,
- preferredDownloadScheme,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!revision) { return; }
- if (!revision || !revision.fetch) { return; }
-
- let scheme = preferredDownloadScheme;
- if (!scheme) {
- const keys = Object.keys(revision.fetch).sort();
- if (keys.length === 0) {
- return;
- }
- scheme = keys[0];
- }
-
- if (!revision.fetch[scheme] || !revision.fetch[scheme].commands) {
- return;
- }
-
- const cmds = {};
- Object.entries(revision.fetch[scheme].commands).forEach(([key, cmd]) => {
- cmds[key.toLowerCase()] = cmd;
- });
-
- if (preferredDownloadCommand &&
- cmds[preferredDownloadCommand.toLowerCase()]) {
- return cmds[preferredDownloadCommand.toLowerCase()];
- }
-
- // If no supported command preference is given, look for known commands
- // from the downloads plugin in order of preference.
- for (let i = 0; i < PREFERRED_FETCH_COMMAND_ORDER.length; i++) {
- if (cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]) {
- return cmds[PREFERRED_FETCH_COMMAND_ORDER[i]];
- }
- }
-
+ _computeFetchCommand(revision, preferredDownloadCommand,
+ preferredDownloadScheme) {
+ // Polymer 2: check for undefined
+ if ([
+ revision,
+ preferredDownloadCommand,
+ preferredDownloadScheme,
+ ].some(arg => arg === undefined)) {
return undefined;
}
- _computePushCommand(targetBranch) {
- return PUSH_COMMAND_PREFIX + targetBranch;
+ if (!revision) { return; }
+ if (!revision || !revision.fetch) { return; }
+
+ let scheme = preferredDownloadScheme;
+ if (!scheme) {
+ const keys = Object.keys(revision.fetch).sort();
+ if (keys.length === 0) {
+ return;
+ }
+ scheme = keys[0];
}
+
+ if (!revision.fetch[scheme] || !revision.fetch[scheme].commands) {
+ return;
+ }
+
+ const cmds = {};
+ Object.entries(revision.fetch[scheme].commands).forEach(([key, cmd]) => {
+ cmds[key.toLowerCase()] = cmd;
+ });
+
+ if (preferredDownloadCommand &&
+ cmds[preferredDownloadCommand.toLowerCase()]) {
+ return cmds[preferredDownloadCommand.toLowerCase()];
+ }
+
+ // If no supported command preference is given, look for known commands
+ // from the downloads plugin in order of preference.
+ for (let i = 0; i < PREFERRED_FETCH_COMMAND_ORDER.length; i++) {
+ if (cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]) {
+ return cmds[PREFERRED_FETCH_COMMAND_ORDER[i]];
+ }
+ }
+
+ return undefined;
}
- customElements.define(GrUploadHelpDialog.is, GrUploadHelpDialog);
-})();
+ _computePushCommand(targetBranch) {
+ return PUSH_COMMAND_PREFIX + targetBranch;
+ }
+}
+
+customElements.define(GrUploadHelpDialog.is, GrUploadHelpDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
index e3cee56..ccf22e6 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
@@ -1,29 +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="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-upload-help-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
background-color: var(--dialog-background-color);
@@ -40,10 +33,7 @@
margin-bottom: var(--spacing-m);
}
</style>
- <gr-dialog
- confirm-label="Done"
- cancel-label=""
- on-confirm="_handleCloseTap">
+ <gr-dialog confirm-label="Done" cancel-label="" on-confirm="_handleCloseTap">
<div class="header" slot="header">How to update this change:</div>
<div class="main" slot="main">
<ol>
@@ -77,6 +67,4 @@
</div>
</gr-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-upload-help-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
index 3af5449..2aa71c6 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-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-upload-help-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-upload-help-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-upload-help-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-upload-help-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,93 +40,95 @@
</template>
</test-fixture>
-<script>
- suite('gr-upload-help-dialog tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-upload-help-dialog.js';
+suite('gr-upload-help-dialog tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- });
+ setup(() => {
+ element = fixture('basic');
+ });
- test('constructs push command from branch', () => {
- element.targetBranch = 'foo';
- assert.equal(element._pushCommand, 'git push origin HEAD:refs/for/foo');
+ test('constructs push command from branch', () => {
+ element.targetBranch = 'foo';
+ assert.equal(element._pushCommand, 'git push origin HEAD:refs/for/foo');
- element.targetBranch = 'master';
- assert.equal(element._pushCommand,
- 'git push origin HEAD:refs/for/master');
- });
+ element.targetBranch = 'master';
+ assert.equal(element._pushCommand,
+ 'git push origin HEAD:refs/for/master');
+ });
- suite('fetch command', () => {
- const testRev = {
- fetch: {
- http: {
- commands: {
- Checkout: 'http checkout',
- Pull: 'http pull',
- },
- },
- ssh: {
- commands: {
- Pull: 'ssh pull',
- },
+ suite('fetch command', () => {
+ const testRev = {
+ fetch: {
+ http: {
+ commands: {
+ Checkout: 'http checkout',
+ Pull: 'http pull',
},
},
- };
+ ssh: {
+ commands: {
+ Pull: 'ssh pull',
+ },
+ },
+ },
+ };
- test('null cases', () => {
- assert.isUndefined(element._computeFetchCommand());
- assert.isUndefined(element._computeFetchCommand({}));
- assert.isUndefined(element._computeFetchCommand({fetch: null}));
- assert.isUndefined(element._computeFetchCommand({fetch: {}}));
- });
+ test('null cases', () => {
+ assert.isUndefined(element._computeFetchCommand());
+ assert.isUndefined(element._computeFetchCommand({}));
+ assert.isUndefined(element._computeFetchCommand({fetch: null}));
+ assert.isUndefined(element._computeFetchCommand({fetch: {}}));
+ });
- test('not all defined', () => {
- assert.isUndefined(
- element._computeFetchCommand(testRev, undefined, ''));
- assert.isUndefined(
- element._computeFetchCommand(testRev, '', undefined));
- assert.isUndefined(
- element._computeFetchCommand(undefined, '', ''));
- });
+ test('not all defined', () => {
+ assert.isUndefined(
+ element._computeFetchCommand(testRev, undefined, ''));
+ assert.isUndefined(
+ element._computeFetchCommand(testRev, '', undefined));
+ assert.isUndefined(
+ element._computeFetchCommand(undefined, '', ''));
+ });
- test('insufficiently defined scheme', () => {
- assert.isUndefined(
- element._computeFetchCommand(testRev, '', 'badscheme'));
+ test('insufficiently defined scheme', () => {
+ assert.isUndefined(
+ element._computeFetchCommand(testRev, '', 'badscheme'));
- const rev = Object.assign({}, testRev);
- rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}});
- assert.isUndefined(
- element._computeFetchCommand(rev, '', 'nocmds'));
+ const rev = Object.assign({}, testRev);
+ rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}});
+ assert.isUndefined(
+ element._computeFetchCommand(rev, '', 'nocmds'));
- rev.fetch.nocmds.commands.unsupported = 'unsupported';
- assert.isUndefined(
- element._computeFetchCommand(rev, '', 'nocmds'));
- });
+ rev.fetch.nocmds.commands.unsupported = 'unsupported';
+ assert.isUndefined(
+ element._computeFetchCommand(rev, '', 'nocmds'));
+ });
- test('default scheme and command', () => {
- const cmd = element._computeFetchCommand(testRev, '', '');
- assert.isTrue(cmd === 'http checkout' || cmd === 'ssh pull');
- });
+ test('default scheme and command', () => {
+ const cmd = element._computeFetchCommand(testRev, '', '');
+ assert.isTrue(cmd === 'http checkout' || cmd === 'ssh pull');
+ });
- test('default command', () => {
- assert.strictEqual(
- element._computeFetchCommand(testRev, '', 'http'),
- 'http checkout');
- assert.strictEqual(
- element._computeFetchCommand(testRev, '', 'ssh'),
- 'ssh pull');
- });
+ test('default command', () => {
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, '', 'http'),
+ 'http checkout');
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, '', 'ssh'),
+ 'ssh pull');
+ });
- test('user preferred scheme and command', () => {
- assert.strictEqual(
- element._computeFetchCommand(testRev, 'PULL', 'http'),
- 'http pull');
- assert.strictEqual(
- element._computeFetchCommand(testRev, 'badcmd', 'http'),
- 'http checkout');
- });
+ test('user preferred scheme and command', () => {
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, 'PULL', 'http'),
+ 'http pull');
+ assert.strictEqual(
+ element._computeFetchCommand(testRev, 'badcmd', 'http'),
+ 'http checkout');
});
});
+});
</script>
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>
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.html b/polygerrit-ui/app/elements/custom-dark-theme_test.html
index cd07a67..308e2ee 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.html
+++ b/polygerrit-ui/app/elements/custom-dark-theme_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-app-it_test</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-app.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-app.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../test/test-pre-setup.js';
+import '../test/common-test-setup.js';
+import './gr-app.js';
+void(0);
+</script>
<test-fixture id="element">
<template>
@@ -35,69 +40,71 @@
</template>
</test-fixture>
-<script>
- suite('gr-app custom dark theme tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../test/test-pre-setup.js';
+import '../test/common-test-setup.js';
+import './gr-app.js';
+suite('gr-app custom dark theme tests', () => {
+ let sandbox;
+ let element;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-reporting', {
- appStarted: sandbox.stub(),
- });
- stub('gr-account-dropdown', {
- _getTopContent: sinon.stub(),
- });
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- getAccountCapabilities() { return Promise.resolve({}); },
- getConfig() {
- return Promise.resolve({
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html', window.location.href).toString(),
- ],
- },
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-reporting', {
+ appStarted: sandbox.stub(),
+ });
+ stub('gr-account-dropdown', {
+ _getTopContent: sinon.stub(),
+ });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ getConfig() {
+ return Promise.resolve({
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html', window.location.href).toString(),
+ ],
+ },
+ });
+ },
+ getVersion() { return Promise.resolve(42); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+
+ window.localStorage.setItem('dark-theme', 'true');
+
+ element = fixture('element');
+
+ const importSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForAll,
+ '_import');
+ const importForThemeSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForTheme,
+ '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+ .then(() => {
+ flush(done);
});
- },
- getVersion() { return Promise.resolve(42); },
- getLoggedIn() { return Promise.resolve(false); },
- });
-
- window.localStorage.setItem('dark-theme', 'true');
-
- element = fixture('element');
-
- const importSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForAll,
- '_import');
- const importForThemeSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForTheme,
- '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
- .then(() => {
- flush(done);
- });
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('applies the right theme', () => {
- assert.equal(
- util.getComputedStyleValue('--primary-text-color', element),
- 'red');
- assert.equal(
- util.getComputedStyleValue('--header-background-color', element),
- 'black');
- assert.equal(
- util.getComputedStyleValue('--footer-background-color', element),
- 'yellow');
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('applies the right theme', () => {
+ assert.equal(
+ util.getComputedStyleValue('--primary-text-color', element),
+ 'red');
+ assert.equal(
+ util.getComputedStyleValue('--header-background-color', element),
+ 'black');
+ assert.equal(
+ util.getComputedStyleValue('--footer-background-color', element),
+ 'yellow');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.html b/polygerrit-ui/app/elements/custom-light-theme_test.html
index 13b872e..66567be 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.html
+++ b/polygerrit-ui/app/elements/custom-light-theme_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-app-it_test</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-app.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-app.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../test/test-pre-setup.js';
+import '../test/common-test-setup.js';
+import './gr-app.js';
+void(0);
+</script>
<test-fixture id="element">
<template>
@@ -35,69 +40,71 @@
</template>
</test-fixture>
-<script>
- suite('gr-app custom light theme tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../test/test-pre-setup.js';
+import '../test/common-test-setup.js';
+import './gr-app.js';
+suite('gr-app custom light theme tests', () => {
+ let sandbox;
+ let element;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-reporting', {
- appStarted: sandbox.stub(),
- });
- stub('gr-account-dropdown', {
- _getTopContent: sinon.stub(),
- });
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- getAccountCapabilities() { return Promise.resolve({}); },
- getConfig() {
- return Promise.resolve({
- plugin: {
- js_resource_paths: [],
- html_resource_paths: [
- new URL('test/plugin.html', window.location.href).toString(),
- ],
- },
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-reporting', {
+ appStarted: sandbox.stub(),
+ });
+ stub('gr-account-dropdown', {
+ _getTopContent: sinon.stub(),
+ });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ getConfig() {
+ return Promise.resolve({
+ plugin: {
+ js_resource_paths: [],
+ html_resource_paths: [
+ new URL('test/plugin.html', window.location.href).toString(),
+ ],
+ },
+ });
+ },
+ getVersion() { return Promise.resolve(42); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+
+ window.localStorage.removeItem('dark-theme');
+
+ element = fixture('element');
+
+ const importSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForAll,
+ '_import');
+ const importForThemeSpy = sandbox.spy(
+ element.$['app-element'].$.externalStyleForTheme,
+ '_import');
+ Gerrit.awaitPluginsLoaded().then(() => {
+ Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
+ .then(() => {
+ flush(done);
});
- },
- getVersion() { return Promise.resolve(42); },
- getLoggedIn() { return Promise.resolve(false); },
- });
-
- window.localStorage.removeItem('dark-theme');
-
- element = fixture('element');
-
- const importSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForAll,
- '_import');
- const importForThemeSpy = sandbox.spy(
- element.$['app-element'].$.externalStyleForTheme,
- '_import');
- Gerrit.awaitPluginsLoaded().then(() => {
- Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues))
- .then(() => {
- flush(done);
- });
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('applies the right theme', () => {
- assert.equal(
- util.getComputedStyleValue('--primary-text-color', element),
- '#F00BAA');
- assert.equal(
- util.getComputedStyleValue('--header-background-color', element),
- '#F01BAA');
- assert.equal(
- util.getComputedStyleValue('--footer-background-color', element),
- '#F02BAA');
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('applies the right theme', () => {
+ assert.equal(
+ util.getComputedStyleValue('--primary-text-color', element),
+ '#F00BAA');
+ assert.equal(
+ util.getComputedStyleValue('--header-background-color', element),
+ '#F01BAA');
+ assert.equal(
+ util.getComputedStyleValue('--footer-background-color', element),
+ '#F02BAA');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
index 9104b90..d5075d7 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -14,200 +14,213 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-diff/gr-diff.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-apply-fix-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrApplyFixDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-apply-fix-dialog'; }
+
+ static get properties() {
+ return {
+ // Diff rendering preference API response.
+ prefs: Array,
+ // ChangeInfo API response object.
+ change: Object,
+ changeNum: String,
+ _patchNum: Number,
+ // robot ID associated with a robot comment.
+ _robotId: String,
+ // Selected FixSuggestionInfo entity from robot comment API response.
+ _currentFix: Object,
+ // Flattened /preview API response DiffInfo map object.
+ _currentPreviews: {type: Array, value: () => []},
+ // FixSuggestionInfo entities from robot comment API response.
+ _fixSuggestions: Array,
+ _isApplyFixLoading: {
+ type: Boolean,
+ value: false,
+ },
+ // Index of currently showing suggested fix.
+ _selectedFixIdx: Number,
+ };
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Given robot comment CustomEvent objevt, fetch diffs associated
+ * with first robot comment suggested fix and open dialog.
+ *
+ * @param {*} e CustomEvent to be passed from gr-comment with
+ * robot comment detail.
+ * @return {Promise<undefined>} Promise that resolves either when all
+ * preview diffs are fetched or no fix suggestions in custom event detail.
*/
- class GrApplyFixDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-apply-fix-dialog'; }
-
- static get properties() {
- return {
- // Diff rendering preference API response.
- prefs: Array,
- // ChangeInfo API response object.
- change: Object,
- changeNum: String,
- _patchNum: Number,
- // robot ID associated with a robot comment.
- _robotId: String,
- // Selected FixSuggestionInfo entity from robot comment API response.
- _currentFix: Object,
- // Flattened /preview API response DiffInfo map object.
- _currentPreviews: {type: Array, value: () => []},
- // FixSuggestionInfo entities from robot comment API response.
- _fixSuggestions: Array,
- _isApplyFixLoading: {
- type: Boolean,
- value: false,
- },
- // Index of currently showing suggested fix.
- _selectedFixIdx: Number,
- };
+ open(e) {
+ this._patchNum = e.detail.patchNum;
+ this._fixSuggestions = e.detail.comment.fix_suggestions;
+ this._robotId = e.detail.comment.robot_id;
+ if (this._fixSuggestions == null || this._fixSuggestions.length == 0) {
+ return Promise.resolve();
}
+ this._selectedFixIdx = 0;
+ const promises = [];
+ promises.push(
+ this._showSelectedFixSuggestion(this._fixSuggestions[0]),
+ this.$.applyFixOverlay.open()
+ );
+ return Promise.all(promises)
+ .then(() => {
+ // ensures gr-overlay repositions overlay in center
+ this.$.applyFixOverlay.fire('iron-resize');
+ });
+ }
- /**
- * Given robot comment CustomEvent objevt, fetch diffs associated
- * with first robot comment suggested fix and open dialog.
- *
- * @param {*} e CustomEvent to be passed from gr-comment with
- * robot comment detail.
- * @return {Promise<undefined>} Promise that resolves either when all
- * preview diffs are fetched or no fix suggestions in custom event detail.
- */
- open(e) {
- this._patchNum = e.detail.patchNum;
- this._fixSuggestions = e.detail.comment.fix_suggestions;
- this._robotId = e.detail.comment.robot_id;
- if (this._fixSuggestions == null || this._fixSuggestions.length == 0) {
- return Promise.resolve();
- }
- this._selectedFixIdx = 0;
- const promises = [];
- promises.push(
- this._showSelectedFixSuggestion(this._fixSuggestions[0]),
- this.$.applyFixOverlay.open()
- );
- return Promise.all(promises)
- .then(() => {
- // ensures gr-overlay repositions overlay in center
- this.$.applyFixOverlay.fire('iron-resize');
- });
+ attached() {
+ super.attached();
+ this.refitOverlay = () => {
+ // re-center the dialog as content changed
+ this.$.applyFixOverlay.fire('iron-resize');
+ };
+ this.addEventListener('diff-context-expanded', this.refitOverlay);
+ }
+
+ detached() {
+ super.detached();
+ this.removeEventListener('diff-context-expanded', this.refitOverlay);
+ }
+
+ _showSelectedFixSuggestion(fixSuggestion) {
+ this._currentFix = fixSuggestion;
+ return this._fetchFixPreview(fixSuggestion.fix_id);
+ }
+
+ _fetchFixPreview(fixId) {
+ return this.$.restAPI
+ .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
+ .then(res => {
+ if (res != null) {
+ const previews = Object.keys(res).map(key => {
+ return {filepath: key, preview: res[key]};
+ });
+ this._currentPreviews = previews;
+ }
+ })
+ .catch(err => {
+ this._close();
+ throw err;
+ });
+ }
+
+ hasSingleFix(_fixSuggestions) {
+ return (_fixSuggestions || {}).length === 1;
+ }
+
+ overridePartialPrefs(prefs) {
+ // generate a smaller gr-diff than fullscreen for dialog
+ return Object.assign({}, prefs, {line_length: 50});
+ }
+
+ onCancel(e) {
+ if (e) {
+ e.stopPropagation();
}
+ this._close();
+ }
- attached() {
- super.attached();
- this.refitOverlay = () => {
- // re-center the dialog as content changed
- this.$.applyFixOverlay.fire('iron-resize');
- };
- this.addEventListener('diff-context-expanded', this.refitOverlay);
- }
+ addOneTo(_selectedFixIdx) {
+ return _selectedFixIdx + 1;
+ }
- detached() {
- super.detached();
- this.removeEventListener('diff-context-expanded', this.refitOverlay);
- }
-
- _showSelectedFixSuggestion(fixSuggestion) {
- this._currentFix = fixSuggestion;
- return this._fetchFixPreview(fixSuggestion.fix_id);
- }
-
- _fetchFixPreview(fixId) {
- return this.$.restAPI
- .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
- .then(res => {
- if (res != null) {
- const previews = Object.keys(res).map(key => {
- return {filepath: key, preview: res[key]};
- });
- this._currentPreviews = previews;
- }
- })
- .catch(err => {
- this._close();
- throw err;
- });
- }
-
- hasSingleFix(_fixSuggestions) {
- return (_fixSuggestions || {}).length === 1;
- }
-
- overridePartialPrefs(prefs) {
- // generate a smaller gr-diff than fullscreen for dialog
- return Object.assign({}, prefs, {line_length: 50});
- }
-
- onCancel(e) {
- if (e) {
- e.stopPropagation();
- }
- this._close();
- }
-
- addOneTo(_selectedFixIdx) {
- return _selectedFixIdx + 1;
- }
-
- _onPrevFixClick(e) {
- if (e) e.stopPropagation();
- if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) {
- this._selectedFixIdx -= 1;
- return this._showSelectedFixSuggestion(
- this._fixSuggestions[this._selectedFixIdx]);
- }
- }
-
- _onNextFixClick(e) {
- if (e) e.stopPropagation();
- if (this._fixSuggestions &&
- this._selectedFixIdx < this._fixSuggestions.length) {
- this._selectedFixIdx += 1;
- return this._showSelectedFixSuggestion(
- this._fixSuggestions[this._selectedFixIdx]);
- }
- }
-
- _noPrevFix(_selectedFixIdx) {
- return _selectedFixIdx === 0;
- }
-
- _noNextFix(_selectedFixIdx, fixSuggestions) {
- if (fixSuggestions == null) return true;
- return _selectedFixIdx === fixSuggestions.length - 1;
- }
-
- _close() {
- this._currentFix = {};
- this._currentPreviews = [];
- this._isApplyFixLoading = false;
-
- this.dispatchEvent(new CustomEvent('close-fix-preview', {
- bubbles: true,
- composed: true,
- }));
- this.$.applyFixOverlay.close();
- }
-
- _getApplyFixButtonLabel(isLoading) {
- return isLoading ? 'Saving...' : 'Apply Fix';
- }
-
- _handleApplyFix(e) {
- if (e) {
- e.stopPropagation();
- }
- if (this._currentFix == null || this._currentFix.fix_id == null) {
- return;
- }
- this._isApplyFixLoading = true;
- return this.$.restAPI
- .applyFixSuggestion(
- this.changeNum, this._patchNum, this._currentFix.fix_id
- )
- .then(res => {
- if (res && res.ok) {
- Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum);
- this._close();
- }
- this._isApplyFixLoading = false;
- });
- }
-
- getFixDescription(currentFix) {
- return currentFix != null && currentFix.description ?
- currentFix.description : '';
+ _onPrevFixClick(e) {
+ if (e) e.stopPropagation();
+ if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) {
+ this._selectedFixIdx -= 1;
+ return this._showSelectedFixSuggestion(
+ this._fixSuggestions[this._selectedFixIdx]);
}
}
- customElements.define(GrApplyFixDialog.is, GrApplyFixDialog);
-})();
+ _onNextFixClick(e) {
+ if (e) e.stopPropagation();
+ if (this._fixSuggestions &&
+ this._selectedFixIdx < this._fixSuggestions.length) {
+ this._selectedFixIdx += 1;
+ return this._showSelectedFixSuggestion(
+ this._fixSuggestions[this._selectedFixIdx]);
+ }
+ }
+
+ _noPrevFix(_selectedFixIdx) {
+ return _selectedFixIdx === 0;
+ }
+
+ _noNextFix(_selectedFixIdx, fixSuggestions) {
+ if (fixSuggestions == null) return true;
+ return _selectedFixIdx === fixSuggestions.length - 1;
+ }
+
+ _close() {
+ this._currentFix = {};
+ this._currentPreviews = [];
+ this._isApplyFixLoading = false;
+
+ this.dispatchEvent(new CustomEvent('close-fix-preview', {
+ bubbles: true,
+ composed: true,
+ }));
+ this.$.applyFixOverlay.close();
+ }
+
+ _getApplyFixButtonLabel(isLoading) {
+ return isLoading ? 'Saving...' : 'Apply Fix';
+ }
+
+ _handleApplyFix(e) {
+ if (e) {
+ e.stopPropagation();
+ }
+ if (this._currentFix == null || this._currentFix.fix_id == null) {
+ return;
+ }
+ this._isApplyFixLoading = true;
+ return this.$.restAPI
+ .applyFixSuggestion(
+ this.changeNum, this._patchNum, this._currentFix.fix_id
+ )
+ .then(res => {
+ if (res && res.ok) {
+ Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum);
+ this._close();
+ }
+ this._isApplyFixLoading = false;
+ });
+ }
+
+ getFixDescription(currentFix) {
+ return currentFix != null && currentFix.description ?
+ currentFix.description : '';
+ }
+}
+
+customElements.define(GrApplyFixDialog.is, GrApplyFixDialog);
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
index c650bb5..f6cd1ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
@@ -1,30 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 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="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-diff/gr-diff.html">
-
-<dom-module id="gr-apply-fix-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
gr-diff {
--content-width: 90vw;
@@ -52,13 +44,8 @@
margin-right: var(--spacing-l);
}
</style>
- <gr-overlay id="applyFixOverlay" with-backdrop>
- <gr-dialog
- id="applyFixDialog"
- on-confirm="_handleApplyFix"
- confirm-label="[[_getApplyFixButtonLabel(_isApplyFixLoading)]]"
- disabled="[[_isApplyFixLoading]]"
- on-cancel="onCancel">
+ <gr-overlay id="applyFixOverlay" with-backdrop="">
+ <gr-dialog id="applyFixDialog" on-confirm="_handleApplyFix" confirm-label="[[_getApplyFixButtonLabel(_isApplyFixLoading)]]" disabled="[[_isApplyFixLoading]]" on-cancel="onCancel">
<div slot="header">[[_robotId]] - [[getFixDescription(_currentFix)]]</div>
<div slot="main">
<template is="dom-repeat" items="[[_currentPreviews]]">
@@ -66,26 +53,20 @@
<span>[[item.filepath]]</span>
</div>
<div class="diffContainer">
- <gr-diff
- prefs="[[overridePartialPrefs(prefs)]]"
- change-num="[[changeNum]]"
- path="[[item.filepath]]"
- diff="[[item.preview]]"></gr-diff>
+ <gr-diff prefs="[[overridePartialPrefs(prefs)]]" change-num="[[changeNum]]" path="[[item.filepath]]" diff="[[item.preview]]"></gr-diff>
</div>
</template>
</div>
- <div slot="footer" class="fix-picker" hidden$="[[hasSingleFix(_fixSuggestions)]]">
+ <div slot="footer" class="fix-picker" hidden\$="[[hasSingleFix(_fixSuggestions)]]">
<span>Suggested fix [[addOneTo(_selectedFixIdx)]] of [[_fixSuggestions.length]]</span>
- <gr-button id="prevFix" on-click="_onPrevFixClick" disabled$="[[_noPrevFix(_selectedFixIdx)]]">
+ <gr-button id="prevFix" on-click="_onPrevFixClick" disabled\$="[[_noPrevFix(_selectedFixIdx)]]">
<iron-icon icon="gr-icons:chevron-left"></iron-icon>
</gr-button>
- <gr-button id="nextFix" on-click="_onNextFixClick" disabled$="[[_noNextFix(_selectedFixIdx, _fixSuggestions)]]">
+ <gr-button id="nextFix" on-click="_onNextFixClick" disabled\$="[[_noNextFix(_selectedFixIdx, _fixSuggestions)]]">
<iron-icon icon="gr-icons:chevron-right"></iron-icon>
</gr-button>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-apply-fix-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
index f22ab57..386f829 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
@@ -17,17 +17,23 @@
-->
<meta name='viewport' content='width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes'>
<title>gr-apply-fix-dialog</title>
-<link rel='import' href='../../../test/common-test-setup.html'>
-<script src='/bower_components/webcomponentsjs/custom-elements-es5-adapter.js'></script>
+<script type="module" src="../../../test/common-test-setup.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>
-<link rel='import' href='./gr-apply-fix-dialog.html'>
+<script type="module" src="./gr-apply-fix-dialog.js"></script>
-<script>void (0);</script>
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-apply-fix-dialog.js';
+void (0);
+</script>
<test-fixture id='basic'>
<template>
@@ -35,229 +41,232 @@
</template>
</test-fixture>
-<script>
- suite('gr-apply-fix-dialog tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const ROBOT_COMMENT_WITH_TWO_FIXES = {
- robot_id: 'robot_1',
- fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
+<script type="module">
+import '../../../test/common-test-setup.js';
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-apply-fix-dialog.js';
+suite('gr-apply-fix-dialog tests', () => {
+ let element;
+ let sandbox;
+ const ROBOT_COMMENT_WITH_TWO_FIXES = {
+ robot_id: 'robot_1',
+ fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
+ };
+
+ const ROBOT_COMMENT_WITH_ONE_FIX = {
+ robot_id: 'robot_1',
+ fix_suggestions: [{fix_id: 'fix_1'}],
+ };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.changeNum = '1';
+ element._patchNum = 2;
+ element.change = {
+ _number: '1',
+ project: 'project',
};
-
- const ROBOT_COMMENT_WITH_ONE_FIX = {
- robot_id: 'robot_1',
- fix_suggestions: [{fix_id: 'fix_1'}],
+ element.prefs = {
+ font_size: 12,
+ line_length: 100,
+ tab_size: 4,
};
+ });
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.changeNum = '1';
- element._patchNum = 2;
- element.change = {
- _number: '1',
- project: 'project',
- };
- element.prefs = {
- font_size: 12,
- line_length: 100,
- tab_size: 4,
- };
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ test('dialog opens fetch and sets previews', done => {
+ sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+ .returns(Promise.resolve({
+ f1: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['loqlwkqll'],
+ },
+ {
+ b: ['qwqqsqw'],
+ },
+ {
+ ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+ },
+ ],
+ },
+ f2: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['eqweqweqwex'],
+ },
+ {
+ b: ['zassdasd'],
+ },
+ {
+ ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+ },
+ ],
+ },
+ }));
+ sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
- test('dialog opens fetch and sets previews', done => {
- sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
- .returns(Promise.resolve({
- f1: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['loqlwkqll'],
- },
- {
- b: ['qwqqsqw'],
- },
- {
- ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
- },
- ],
- },
- f2: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['eqweqweqwex'],
- },
- {
- b: ['zassdasd'],
- },
- {
- ab: ['zassdasd', 'dasdasda', 'asdasdad'],
- },
- ],
- },
- }));
- sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
- element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
- .then(() => {
- assert.equal(element._currentFix.fix_id, 'fix_1');
- assert.equal(element._currentPreviews.length, 2);
- assert.equal(element._robotId, 'robot_1');
- done();
- });
- });
-
- test('next button state updated when suggestions changed', done => {
- sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
- .returns(Promise.resolve({}));
- sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
- element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
- .then(() => assert.isTrue(element.$.nextFix.disabled))
- .then(() =>
- element.open({detail: {patchNum: 2,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES}}))
- .then(() => {
- assert.isFalse(element.$.nextFix.disabled);
- done();
- });
- });
-
- test('preview endpoint throws error should reset dialog', done => {
- sandbox.stub(window, 'fetch', (url => {
- if (url.endsWith('/preview')) {
- return Promise.reject(new Error('backend error'));
- }
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(''); },
- status: 200,
+ element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
+ .then(() => {
+ assert.equal(element._currentFix.fix_id, 'fix_1');
+ assert.equal(element._currentPreviews.length, 2);
+ assert.equal(element._robotId, 'robot_1');
+ done();
});
- }));
- const errorStub = sinon.stub();
- document.addEventListener('network-error', errorStub);
- element.open({detail: {patchNum: 2,
- comment: ROBOT_COMMENT_WITH_TWO_FIXES}});
- flush(() => {
- assert.isTrue(errorStub.called);
- assert.deepEqual(element._currentFix, {});
- done();
- });
- });
+ });
- test('apply fix button should call apply ' +
- 'and navigate to change view', done => {
- sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
- .returns(Promise.resolve({ok: true}));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._currentFix = {fix_id: '123'};
+ test('next button state updated when suggestions changed', done => {
+ sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+ .returns(Promise.resolve({}));
+ sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
- element._handleApplyFix().then(() => {
- assert.isTrue(element.$.restAPI.applyFixSuggestion
- .calledWithExactly('1', 2, '123'));
- assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({
- _number: '1',
- project: 'project',
- }, 'edit', 2));
-
- // reset gr-apply-fix-dialog and close
- assert.deepEqual(element._currentFix, {});
- assert.equal(element._currentPreviews.length, 0);
- done();
- });
- });
-
- test('should not navigate to change view if incorect reponse', done => {
- sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
- .returns(Promise.resolve({}));
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._currentFix = {fix_id: '123'};
-
- element._handleApplyFix().then(() => {
- assert.isTrue(element.$.restAPI.applyFixSuggestion
- .calledWithExactly('1', 2, '123'));
- assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
-
- assert.equal(element._isApplyFixLoading, false);
- done();
- });
- });
-
- test('select fix forward and back of multiple suggested fixes', done => {
- sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
- .returns(Promise.resolve({
- f1: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['loqlwkqll'],
- },
- {
- b: ['qwqqsqw'],
- },
- {
- ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
- },
- ],
- },
- f2: {
- meta_a: {},
- meta_b: {},
- content: [
- {
- ab: ['eqweqweqwex'],
- },
- {
- b: ['zassdasd'],
- },
- {
- ab: ['zassdasd', 'dasdasda', 'asdasdad'],
- },
- ],
- },
- }));
- sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
- element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
- .then(() => {
- element._onNextFixClick();
- assert.equal(element._currentFix.fix_id, 'fix_2');
- element._onPrevFixClick();
- assert.equal(element._currentFix.fix_id, 'fix_1');
- done();
- });
- });
-
- test('server-error should throw for failed apply call', done => {
- sandbox.stub(window, 'fetch', (url => {
- if (url.endsWith('/apply')) {
- return Promise.reject(new Error('backend error'));
- }
- return Promise.resolve({
- ok: true,
- text() { return Promise.resolve(''); },
- status: 200,
+ element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
+ .then(() => assert.isTrue(element.$.nextFix.disabled))
+ .then(() =>
+ element.open({detail: {patchNum: 2,
+ comment: ROBOT_COMMENT_WITH_TWO_FIXES}}))
+ .then(() => {
+ assert.isFalse(element.$.nextFix.disabled);
+ done();
});
- }));
- const errorStub = sinon.stub();
- document.addEventListener('network-error', errorStub);
- sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._currentFix = {fix_id: '123'};
- element._handleApplyFix();
- flush(() => {
- assert.isFalse(Gerrit.Nav.navigateToChange.called);
- assert.isTrue(errorStub.called);
- done();
+ });
+
+ test('preview endpoint throws error should reset dialog', done => {
+ sandbox.stub(window, 'fetch', (url => {
+ if (url.endsWith('/preview')) {
+ return Promise.reject(new Error('backend error'));
+ }
+ return Promise.resolve({
+ ok: true,
+ text() { return Promise.resolve(''); },
+ status: 200,
});
+ }));
+ const errorStub = sinon.stub();
+ document.addEventListener('network-error', errorStub);
+ element.open({detail: {patchNum: 2,
+ comment: ROBOT_COMMENT_WITH_TWO_FIXES}});
+ flush(() => {
+ assert.isTrue(errorStub.called);
+ assert.deepEqual(element._currentFix, {});
+ done();
});
});
+
+ test('apply fix button should call apply ' +
+ 'and navigate to change view', done => {
+ sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
+ .returns(Promise.resolve({ok: true}));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._currentFix = {fix_id: '123'};
+
+ element._handleApplyFix().then(() => {
+ assert.isTrue(element.$.restAPI.applyFixSuggestion
+ .calledWithExactly('1', 2, '123'));
+ assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({
+ _number: '1',
+ project: 'project',
+ }, 'edit', 2));
+
+ // reset gr-apply-fix-dialog and close
+ assert.deepEqual(element._currentFix, {});
+ assert.equal(element._currentPreviews.length, 0);
+ done();
+ });
+ });
+
+ test('should not navigate to change view if incorect reponse', done => {
+ sandbox.stub(element.$.restAPI, 'applyFixSuggestion')
+ .returns(Promise.resolve({}));
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._currentFix = {fix_id: '123'};
+
+ element._handleApplyFix().then(() => {
+ assert.isTrue(element.$.restAPI.applyFixSuggestion
+ .calledWithExactly('1', 2, '123'));
+ assert.isTrue(Gerrit.Nav.navigateToChange.notCalled);
+
+ assert.equal(element._isApplyFixLoading, false);
+ done();
+ });
+ });
+
+ test('select fix forward and back of multiple suggested fixes', done => {
+ sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview')
+ .returns(Promise.resolve({
+ f1: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['loqlwkqll'],
+ },
+ {
+ b: ['qwqqsqw'],
+ },
+ {
+ ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+ },
+ ],
+ },
+ f2: {
+ meta_a: {},
+ meta_b: {},
+ content: [
+ {
+ ab: ['eqweqweqwex'],
+ },
+ {
+ b: ['zassdasd'],
+ },
+ {
+ ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+ },
+ ],
+ },
+ }));
+ sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+ element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
+ .then(() => {
+ element._onNextFixClick();
+ assert.equal(element._currentFix.fix_id, 'fix_2');
+ element._onPrevFixClick();
+ assert.equal(element._currentFix.fix_id, 'fix_1');
+ done();
+ });
+ });
+
+ test('server-error should throw for failed apply call', done => {
+ sandbox.stub(window, 'fetch', (url => {
+ if (url.endsWith('/apply')) {
+ return Promise.reject(new Error('backend error'));
+ }
+ return Promise.resolve({
+ ok: true,
+ text() { return Promise.resolve(''); },
+ status: 200,
+ });
+ }));
+ const errorStub = sinon.stub();
+ document.addEventListener('network-error', errorStub);
+ sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._currentFix = {fix_id: '123'};
+ element._handleApplyFix();
+ flush(() => {
+ assert.isFalse(Gerrit.Nav.navigateToChange.called);
+ assert.isTrue(errorStub.called);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 490367a..95de0d1 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -14,520 +14,528 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PARENT = 'PARENT';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-comment-api_html.js';
- /**
- * Construct a change comments object, which can be data-bound to child
- * elements of that which uses the gr-comment-api.
- *
- * @constructor
- * @param {!Object} comments
- * @param {!Object} robotComments
- * @param {!Object} drafts
- * @param {number} changeNum
- */
- function ChangeComments(comments, robotComments, drafts, changeNum) {
- this._comments = comments;
- this._robotComments = robotComments;
- this._drafts = drafts;
- this._changeNum = changeNum;
+const PARENT = 'PARENT';
+
+/**
+ * Construct a change comments object, which can be data-bound to child
+ * elements of that which uses the gr-comment-api.
+ *
+ * @constructor
+ * @param {!Object} comments
+ * @param {!Object} robotComments
+ * @param {!Object} drafts
+ * @param {number} changeNum
+ */
+function ChangeComments(comments, robotComments, drafts, changeNum) {
+ this._comments = comments;
+ this._robotComments = robotComments;
+ this._drafts = drafts;
+ this._changeNum = changeNum;
+}
+
+ChangeComments.prototype = {
+ get comments() {
+ return this._comments;
+ },
+ get drafts() {
+ return this._drafts;
+ },
+ get robotComments() {
+ return this._robotComments;
+ },
+};
+
+ChangeComments.prototype._patchNumEquals =
+ Gerrit.PatchSetBehavior.patchNumEquals;
+ChangeComments.prototype._isMergeParent =
+ Gerrit.PatchSetBehavior.isMergeParent;
+ChangeComments.prototype._getParentIndex =
+ Gerrit.PatchSetBehavior.getParentIndex;
+
+/**
+ * Get an object mapping file paths to a boolean representing whether that
+ * path contains diff comments in the given patch set (including drafts and
+ * robot comments).
+ *
+ * Paths with comments are mapped to true, whereas paths without comments
+ * are not mapped.
+ *
+ * @param {Gerrit.PatchRange=} opt_patchRange The patch-range object containing
+ * patchNum and basePatchNum properties to represent the range.
+ * @return {!Object}
+ */
+ChangeComments.prototype.getPaths = function(opt_patchRange) {
+ const responses = [this.comments, this.drafts, this.robotComments];
+ const commentMap = {};
+ for (const response of responses) {
+ for (const path in response) {
+ if (response.hasOwnProperty(path) &&
+ response[path].some(c => {
+ // If don't care about patch range, we know that the path exists.
+ if (!opt_patchRange) { return true; }
+ return this._isInPatchRange(c, opt_patchRange);
+ })) {
+ commentMap[path] = true;
+ }
+ }
}
+ return commentMap;
+};
- ChangeComments.prototype = {
- get comments() {
- return this._comments;
- },
- get drafts() {
- return this._drafts;
- },
- get robotComments() {
- return this._robotComments;
- },
- };
+/**
+ * Gets all the comments and robot comments for the given change.
+ *
+ * @param {number=} opt_patchNum
+ * @return {!Object}
+ */
+ChangeComments.prototype.getAllPublishedComments = function(opt_patchNum) {
+ return this.getAllComments(false, opt_patchNum);
+};
- ChangeComments.prototype._patchNumEquals =
- Gerrit.PatchSetBehavior.patchNumEquals;
- ChangeComments.prototype._isMergeParent =
- Gerrit.PatchSetBehavior.isMergeParent;
- ChangeComments.prototype._getParentIndex =
- Gerrit.PatchSetBehavior.getParentIndex;
+/**
+ * Gets all the comments for a particular thread group. Used for refreshing
+ * comments after the thread group has already been built.
+ *
+ * @param {string} rootId
+ * @return {!Array} an array of comments
+ */
+ChangeComments.prototype.getCommentsForThread = function(rootId) {
+ const allThreads = this.getAllThreadsForChange();
+ const threadMatch = allThreads.find(t => t.rootId === rootId);
- /**
- * Get an object mapping file paths to a boolean representing whether that
- * path contains diff comments in the given patch set (including drafts and
- * robot comments).
- *
- * Paths with comments are mapped to true, whereas paths without comments
- * are not mapped.
- *
- * @param {Gerrit.PatchRange=} opt_patchRange The patch-range object containing
- * patchNum and basePatchNum properties to represent the range.
- * @return {!Object}
- */
- ChangeComments.prototype.getPaths = function(opt_patchRange) {
- const responses = [this.comments, this.drafts, this.robotComments];
- const commentMap = {};
- for (const response of responses) {
- for (const path in response) {
- if (response.hasOwnProperty(path) &&
- response[path].some(c => {
- // If don't care about patch range, we know that the path exists.
- if (!opt_patchRange) { return true; }
- return this._isInPatchRange(c, opt_patchRange);
- })) {
- commentMap[path] = true;
- }
- }
+ // In the event that a single draft comment was removed by the thread-list
+ // and the diff view is updating comments, there will no longer be a thread
+ // found. In this case, return null.
+ return threadMatch ? threadMatch.comments : null;
+};
+
+/**
+ * Filters an array of comments by line and side
+ *
+ * @param {!Array} comments
+ * @param {boolean} parentOnly whether the only comments returned should have
+ * the side attribute set to PARENT
+ * @param {string} commentSide whether the comment was left on the left or the
+ * right side regardless or unified or side-by-side
+ * @param {number=} opt_line line number, can be undefined if file comment
+ * @return {!Array} an array of comments
+ */
+ChangeComments.prototype._filterCommentsBySideAndLine = function(comments,
+ parentOnly, commentSide, opt_line) {
+ return comments.filter(c => {
+ // if parentOnly, only match comments with PARENT for the side.
+ let sideMatch = parentOnly ? c.side === PARENT : c.side !== PARENT;
+ if (parentOnly) {
+ sideMatch = sideMatch && c.side === PARENT;
}
- return commentMap;
- };
+ return sideMatch && c.line === opt_line;
+ }).map(c => {
+ c.__commentSide = commentSide;
+ return c;
+ });
+};
- /**
- * Gets all the comments and robot comments for the given change.
- *
- * @param {number=} opt_patchNum
- * @return {!Object}
- */
- ChangeComments.prototype.getAllPublishedComments = function(opt_patchNum) {
- return this.getAllComments(false, opt_patchNum);
- };
-
- /**
- * Gets all the comments for a particular thread group. Used for refreshing
- * comments after the thread group has already been built.
- *
- * @param {string} rootId
- * @return {!Array} an array of comments
- */
- ChangeComments.prototype.getCommentsForThread = function(rootId) {
- const allThreads = this.getAllThreadsForChange();
- const threadMatch = allThreads.find(t => t.rootId === rootId);
-
- // In the event that a single draft comment was removed by the thread-list
- // and the diff view is updating comments, there will no longer be a thread
- // found. In this case, return null.
- return threadMatch ? threadMatch.comments : null;
- };
-
- /**
- * Filters an array of comments by line and side
- *
- * @param {!Array} comments
- * @param {boolean} parentOnly whether the only comments returned should have
- * the side attribute set to PARENT
- * @param {string} commentSide whether the comment was left on the left or the
- * right side regardless or unified or side-by-side
- * @param {number=} opt_line line number, can be undefined if file comment
- * @return {!Array} an array of comments
- */
- ChangeComments.prototype._filterCommentsBySideAndLine = function(comments,
- parentOnly, commentSide, opt_line) {
- return comments.filter(c => {
- // if parentOnly, only match comments with PARENT for the side.
- let sideMatch = parentOnly ? c.side === PARENT : c.side !== PARENT;
- if (parentOnly) {
- sideMatch = sideMatch && c.side === PARENT;
- }
- return sideMatch && c.line === opt_line;
- }).map(c => {
- c.__commentSide = commentSide;
- return c;
- });
- };
-
- /**
- * Gets all the comments and robot comments for the given change.
- *
- * @param {boolean=} opt_includeDrafts
- * @param {number=} opt_patchNum
- * @return {!Object}
- */
- ChangeComments.prototype.getAllComments = function(opt_includeDrafts,
- opt_patchNum) {
- const paths = this.getPaths();
- const publishedComments = {};
- for (const path of Object.keys(paths)) {
- let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum);
- if (opt_includeDrafts) {
- const drafts = this.getAllDraftsForPath(path, opt_patchNum)
- .map(d => Object.assign({__draft: true}, d));
- commentsToAdd = commentsToAdd.concat(drafts);
- }
- publishedComments[path] = commentsToAdd;
- }
- return publishedComments;
- };
-
- /**
- * Gets all the comments and robot comments for the given change.
- *
- * @param {number=} opt_patchNum
- * @return {!Object}
- */
- ChangeComments.prototype.getAllDrafts = function(opt_patchNum) {
- const paths = this.getPaths();
- const drafts = {};
- for (const path of Object.keys(paths)) {
- drafts[path] = this.getAllDraftsForPath(path, opt_patchNum);
- }
- return drafts;
- };
-
- /**
- * Get the comments (robot comments) for a path and optional patch num.
- *
- * @param {!string} path
- * @param {number=} opt_patchNum
- * @param {boolean=} opt_includeDrafts
- * @return {!Array}
- */
- ChangeComments.prototype.getAllCommentsForPath = function(path,
- opt_patchNum, opt_includeDrafts) {
- const comments = this._comments[path] || [];
- const robotComments = this._robotComments[path] || [];
- let allComments = comments.concat(robotComments);
+/**
+ * Gets all the comments and robot comments for the given change.
+ *
+ * @param {boolean=} opt_includeDrafts
+ * @param {number=} opt_patchNum
+ * @return {!Object}
+ */
+ChangeComments.prototype.getAllComments = function(opt_includeDrafts,
+ opt_patchNum) {
+ const paths = this.getPaths();
+ const publishedComments = {};
+ for (const path of Object.keys(paths)) {
+ let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum);
if (opt_includeDrafts) {
- const drafts = this.getAllDraftsForPath(path)
+ const drafts = this.getAllDraftsForPath(path, opt_patchNum)
.map(d => Object.assign({__draft: true}, d));
- allComments = allComments.concat(drafts);
+ commentsToAdd = commentsToAdd.concat(drafts);
}
- if (!opt_patchNum) { return allComments; }
- return (allComments || []).filter(c =>
- this._patchNumEquals(c.patch_set, opt_patchNum)
- );
- };
+ publishedComments[path] = commentsToAdd;
+ }
+ return publishedComments;
+};
- /**
- * Get the drafts for a path and optional patch num.
- *
- * @param {!string} path
- * @param {number=} opt_patchNum
- * @return {!Array}
- */
- ChangeComments.prototype.getAllDraftsForPath = function(path,
- opt_patchNum) {
- const comments = this._drafts[path] || [];
- if (!opt_patchNum) { return comments; }
- return (comments || []).filter(c =>
- this._patchNumEquals(c.patch_set, opt_patchNum)
- );
- };
+/**
+ * Gets all the comments and robot comments for the given change.
+ *
+ * @param {number=} opt_patchNum
+ * @return {!Object}
+ */
+ChangeComments.prototype.getAllDrafts = function(opt_patchNum) {
+ const paths = this.getPaths();
+ const drafts = {};
+ for (const path of Object.keys(paths)) {
+ drafts[path] = this.getAllDraftsForPath(path, opt_patchNum);
+ }
+ return drafts;
+};
- /**
- * Get the comments (with drafts and robot comments) for a path and
- * patch-range. Returns an object with left and right properties mapping to
- * arrays of comments in on either side of the patch range for that path.
- *
- * @param {!string} path
- * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum
- * and basePatchNum properties to represent the range.
- * @param {Object=} opt_projectConfig Optional project config object to
- * include in the meta sub-object.
- * @return {!Gerrit.CommentsBySide}
- */
- ChangeComments.prototype.getCommentsBySideForPath = function(path,
- patchRange, opt_projectConfig) {
- let comments = [];
- let drafts = [];
- let robotComments = [];
- if (this.comments && this.comments[path]) {
- comments = this.comments[path];
- }
- if (this.drafts && this.drafts[path]) {
- drafts = this.drafts[path];
- }
- if (this.robotComments && this.robotComments[path]) {
- robotComments = this.robotComments[path];
- }
+/**
+ * Get the comments (robot comments) for a path and optional patch num.
+ *
+ * @param {!string} path
+ * @param {number=} opt_patchNum
+ * @param {boolean=} opt_includeDrafts
+ * @return {!Array}
+ */
+ChangeComments.prototype.getAllCommentsForPath = function(path,
+ opt_patchNum, opt_includeDrafts) {
+ const comments = this._comments[path] || [];
+ const robotComments = this._robotComments[path] || [];
+ let allComments = comments.concat(robotComments);
+ if (opt_includeDrafts) {
+ const drafts = this.getAllDraftsForPath(path)
+ .map(d => Object.assign({__draft: true}, d));
+ allComments = allComments.concat(drafts);
+ }
+ if (!opt_patchNum) { return allComments; }
+ return (allComments || []).filter(c =>
+ this._patchNumEquals(c.patch_set, opt_patchNum)
+ );
+};
- drafts.forEach(d => { d.__draft = true; });
+/**
+ * Get the drafts for a path and optional patch num.
+ *
+ * @param {!string} path
+ * @param {number=} opt_patchNum
+ * @return {!Array}
+ */
+ChangeComments.prototype.getAllDraftsForPath = function(path,
+ opt_patchNum) {
+ const comments = this._drafts[path] || [];
+ if (!opt_patchNum) { return comments; }
+ return (comments || []).filter(c =>
+ this._patchNumEquals(c.patch_set, opt_patchNum)
+ );
+};
- const all = comments.concat(drafts).concat(robotComments);
-
- const baseComments = all.filter(c =>
- this._isInBaseOfPatchRange(c, patchRange));
- const revisionComments = all.filter(c =>
- this._isInRevisionOfPatchRange(c, patchRange));
-
- return {
- meta: {
- changeNum: this._changeNum,
- path,
- patchRange,
- projectConfig: opt_projectConfig,
- },
- left: baseComments,
- right: revisionComments,
- };
- };
-
- /**
- * @param {!Object} comments Object keyed by file, with a value of an array
- * of comments left on that file.
- * @return {!Array} A flattened list of all comments, where each comment
- * also includes the file that it was left on, which was the key of the
- * originall object.
- */
- ChangeComments.prototype._commentObjToArrayWithFile = function(comments) {
- let commentArr = [];
- for (const file of Object.keys(comments)) {
- const commentsForFile = [];
- for (const comment of comments[file]) {
- commentsForFile.push(Object.assign({__path: file}, comment));
- }
- commentArr = commentArr.concat(commentsForFile);
- }
- return commentArr;
- };
-
- ChangeComments.prototype._commentObjToArray = function(comments) {
- let commentArr = [];
- for (const file of Object.keys(comments)) {
- commentArr = commentArr.concat(comments[file]);
- }
- return commentArr;
- };
-
- /**
- * Computes a string counting the number of commens in a given file and path.
- *
- * @param {number} patchNum
- * @param {string=} opt_path
- * @return {number}
- */
- ChangeComments.prototype.computeCommentCount = function(patchNum, opt_path) {
- if (opt_path) {
- return this.getAllCommentsForPath(opt_path, patchNum).length;
- }
- const allComments = this.getAllPublishedComments(patchNum);
- return this._commentObjToArray(allComments).length;
- };
-
- /**
- * Computes a string counting the number of draft comments in the entire
- * change, optionally filtered by path and/or patchNum.
- *
- * @param {number=} opt_patchNum
- * @param {string=} opt_path
- * @return {number}
- */
- ChangeComments.prototype.computeDraftCount = function(opt_patchNum,
- opt_path) {
- if (opt_path) {
- return this.getAllDraftsForPath(opt_path, opt_patchNum).length;
- }
- const allDrafts = this.getAllDrafts(opt_patchNum);
- return this._commentObjToArray(allDrafts).length;
- };
-
- /**
- * Computes a number of unresolved comment threads in a given file and path.
- *
- * @param {number} patchNum
- * @param {string=} opt_path
- * @return {number}
- */
- ChangeComments.prototype.computeUnresolvedNum = function(patchNum,
- opt_path) {
- let comments = [];
- let drafts = [];
-
- if (opt_path) {
- comments = this.getAllCommentsForPath(opt_path, patchNum);
- drafts = this.getAllDraftsForPath(opt_path, patchNum);
- } else {
- comments = this._commentObjToArray(
- this.getAllPublishedComments(patchNum));
- }
-
- comments = comments.concat(drafts);
-
- const threads = this.getCommentThreads(this._sortComments(comments));
-
- const unresolvedThreads = threads
- .filter(thread =>
- thread.comments.length &&
- thread.comments[thread.comments.length - 1].unresolved);
-
- return unresolvedThreads.length;
- };
-
- ChangeComments.prototype.getAllThreadsForChange = function() {
- const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
- const sortedComments = this._sortComments(comments);
- return this.getCommentThreads(sortedComments);
- };
-
- ChangeComments.prototype._sortComments = function(comments) {
- return comments.slice(0)
- .sort(
- (c1, c2) => util.parseDate(c1.updated) - util.parseDate(c2.updated)
- );
- };
-
- /**
- * Computes all of the comments in thread format.
- *
- * @param {!Array} comments sorted by updated timestamp.
- * @return {!Array}
- */
- ChangeComments.prototype.getCommentThreads = function(comments) {
- const threads = [];
- const idThreadMap = {};
- for (const comment of comments) {
- // If the comment is in reply to another comment, find that comment's
- // thread and append to it.
- if (comment.in_reply_to) {
- const thread = idThreadMap[comment.in_reply_to];
- if (thread) {
- thread.comments.push(comment);
- idThreadMap[comment.id] = thread;
- continue;
- }
- }
-
- // Otherwise, this comment starts its own thread.
- const newThread = {
- comments: [comment],
- patchNum: comment.patch_set,
- path: comment.__path,
- line: comment.line,
- rootId: comment.id,
- };
- if (comment.side) {
- newThread.commentSide = comment.side;
- }
- threads.push(newThread);
- idThreadMap[comment.id] = newThread;
- }
- return threads;
- };
-
- /**
- * Whether the given comment should be included in the base side of the
- * given patch range.
- *
- * @param {!Object} comment
- * @param {!Gerrit.PatchRange} range
- * @return {boolean}
- */
- ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
- // If the base of the patch range is a parent of a merge, and the comment
- // appears on a specific parent then only show the comment if the parent
- // index of the comment matches that of the range.
- if (comment.parent && comment.side === PARENT) {
- return this._isMergeParent(range.basePatchNum) &&
- comment.parent === this._getParentIndex(range.basePatchNum);
- }
-
- // If the base of the range is the parent of the patch:
- if (range.basePatchNum === PARENT &&
- comment.side === PARENT &&
- this._patchNumEquals(comment.patch_set, range.patchNum)) {
- return true;
- }
- // If the base of the range is not the parent of the patch:
- if (range.basePatchNum !== PARENT &&
- comment.side !== PARENT &&
- this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
- return true;
- }
- return false;
- };
-
- /**
- * Whether the given comment should be included in the revision side of the
- * given patch range.
- *
- * @param {!Object} comment
- * @param {!Gerrit.PatchRange} range
- * @return {boolean}
- */
- ChangeComments.prototype._isInRevisionOfPatchRange = function(comment,
- range) {
- return comment.side !== PARENT &&
- this._patchNumEquals(comment.patch_set, range.patchNum);
- };
-
- /**
- * Whether the given comment should be included in the given patch range.
- *
- * @param {!Object} comment
- * @param {!Gerrit.PatchRange} range
- * @return {boolean|undefined}
- */
- ChangeComments.prototype._isInPatchRange = function(comment, range) {
- return this._isInBaseOfPatchRange(comment, range) ||
- this._isInRevisionOfPatchRange(comment, range);
- };
-
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrCommentApi extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment-api'; }
-
- static get properties() {
- return {
- _changeComments: Object,
- };
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('reload-drafts',
- changeNum => this.reloadDrafts(changeNum));
- }
-
- /**
- * Load all comments (with drafts and robot comments) for the given change
- * number. The returned promise resolves when the comments have loaded, but
- * does not yield the comment data.
- *
- * @param {number} changeNum
- * @return {!Promise<!Object>}
- */
- loadAll(changeNum) {
- const promises = [];
- promises.push(this.$.restAPI.getDiffComments(changeNum));
- promises.push(this.$.restAPI.getDiffRobotComments(changeNum));
- promises.push(this.$.restAPI.getDiffDrafts(changeNum));
-
- return Promise.all(promises).then(([comments, robotComments, drafts]) => {
- this._changeComments = new ChangeComments(comments,
- robotComments, drafts, changeNum);
- return this._changeComments;
- });
- }
-
- /**
- * Re-initialize _changeComments with a new ChangeComments object, that
- * uses the previous values for comments and robot comments, but fetches
- * updated draft comments.
- *
- * @param {number} changeNum
- * @return {!Promise<!Object>}
- */
- reloadDrafts(changeNum) {
- if (!this._changeComments) {
- return this.loadAll(changeNum);
- }
- return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => {
- this._changeComments = new ChangeComments(this._changeComments.comments,
- this._changeComments.robotComments, drafts, changeNum);
- return this._changeComments;
- });
- }
+/**
+ * Get the comments (with drafts and robot comments) for a path and
+ * patch-range. Returns an object with left and right properties mapping to
+ * arrays of comments in on either side of the patch range for that path.
+ *
+ * @param {!string} path
+ * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum
+ * and basePatchNum properties to represent the range.
+ * @param {Object=} opt_projectConfig Optional project config object to
+ * include in the meta sub-object.
+ * @return {!Gerrit.CommentsBySide}
+ */
+ChangeComments.prototype.getCommentsBySideForPath = function(path,
+ patchRange, opt_projectConfig) {
+ let comments = [];
+ let drafts = [];
+ let robotComments = [];
+ if (this.comments && this.comments[path]) {
+ comments = this.comments[path];
+ }
+ if (this.drafts && this.drafts[path]) {
+ drafts = this.drafts[path];
+ }
+ if (this.robotComments && this.robotComments[path]) {
+ robotComments = this.robotComments[path];
}
- customElements.define(GrCommentApi.is, GrCommentApi);
-})();
+ drafts.forEach(d => { d.__draft = true; });
+
+ const all = comments.concat(drafts).concat(robotComments);
+
+ const baseComments = all.filter(c =>
+ this._isInBaseOfPatchRange(c, patchRange));
+ const revisionComments = all.filter(c =>
+ this._isInRevisionOfPatchRange(c, patchRange));
+
+ return {
+ meta: {
+ changeNum: this._changeNum,
+ path,
+ patchRange,
+ projectConfig: opt_projectConfig,
+ },
+ left: baseComments,
+ right: revisionComments,
+ };
+};
+
+/**
+ * @param {!Object} comments Object keyed by file, with a value of an array
+ * of comments left on that file.
+ * @return {!Array} A flattened list of all comments, where each comment
+ * also includes the file that it was left on, which was the key of the
+ * originall object.
+ */
+ChangeComments.prototype._commentObjToArrayWithFile = function(comments) {
+ let commentArr = [];
+ for (const file of Object.keys(comments)) {
+ const commentsForFile = [];
+ for (const comment of comments[file]) {
+ commentsForFile.push(Object.assign({__path: file}, comment));
+ }
+ commentArr = commentArr.concat(commentsForFile);
+ }
+ return commentArr;
+};
+
+ChangeComments.prototype._commentObjToArray = function(comments) {
+ let commentArr = [];
+ for (const file of Object.keys(comments)) {
+ commentArr = commentArr.concat(comments[file]);
+ }
+ return commentArr;
+};
+
+/**
+ * Computes a string counting the number of commens in a given file and path.
+ *
+ * @param {number} patchNum
+ * @param {string=} opt_path
+ * @return {number}
+ */
+ChangeComments.prototype.computeCommentCount = function(patchNum, opt_path) {
+ if (opt_path) {
+ return this.getAllCommentsForPath(opt_path, patchNum).length;
+ }
+ const allComments = this.getAllPublishedComments(patchNum);
+ return this._commentObjToArray(allComments).length;
+};
+
+/**
+ * Computes a string counting the number of draft comments in the entire
+ * change, optionally filtered by path and/or patchNum.
+ *
+ * @param {number=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {number}
+ */
+ChangeComments.prototype.computeDraftCount = function(opt_patchNum,
+ opt_path) {
+ if (opt_path) {
+ return this.getAllDraftsForPath(opt_path, opt_patchNum).length;
+ }
+ const allDrafts = this.getAllDrafts(opt_patchNum);
+ return this._commentObjToArray(allDrafts).length;
+};
+
+/**
+ * Computes a number of unresolved comment threads in a given file and path.
+ *
+ * @param {number} patchNum
+ * @param {string=} opt_path
+ * @return {number}
+ */
+ChangeComments.prototype.computeUnresolvedNum = function(patchNum,
+ opt_path) {
+ let comments = [];
+ let drafts = [];
+
+ if (opt_path) {
+ comments = this.getAllCommentsForPath(opt_path, patchNum);
+ drafts = this.getAllDraftsForPath(opt_path, patchNum);
+ } else {
+ comments = this._commentObjToArray(
+ this.getAllPublishedComments(patchNum));
+ }
+
+ comments = comments.concat(drafts);
+
+ const threads = this.getCommentThreads(this._sortComments(comments));
+
+ const unresolvedThreads = threads
+ .filter(thread =>
+ thread.comments.length &&
+ thread.comments[thread.comments.length - 1].unresolved);
+
+ return unresolvedThreads.length;
+};
+
+ChangeComments.prototype.getAllThreadsForChange = function() {
+ const comments = this._commentObjToArrayWithFile(this.getAllComments(true));
+ const sortedComments = this._sortComments(comments);
+ return this.getCommentThreads(sortedComments);
+};
+
+ChangeComments.prototype._sortComments = function(comments) {
+ return comments.slice(0)
+ .sort(
+ (c1, c2) => util.parseDate(c1.updated) - util.parseDate(c2.updated)
+ );
+};
+
+/**
+ * Computes all of the comments in thread format.
+ *
+ * @param {!Array} comments sorted by updated timestamp.
+ * @return {!Array}
+ */
+ChangeComments.prototype.getCommentThreads = function(comments) {
+ const threads = [];
+ const idThreadMap = {};
+ for (const comment of comments) {
+ // If the comment is in reply to another comment, find that comment's
+ // thread and append to it.
+ if (comment.in_reply_to) {
+ const thread = idThreadMap[comment.in_reply_to];
+ if (thread) {
+ thread.comments.push(comment);
+ idThreadMap[comment.id] = thread;
+ continue;
+ }
+ }
+
+ // Otherwise, this comment starts its own thread.
+ const newThread = {
+ comments: [comment],
+ patchNum: comment.patch_set,
+ path: comment.__path,
+ line: comment.line,
+ rootId: comment.id,
+ };
+ if (comment.side) {
+ newThread.commentSide = comment.side;
+ }
+ threads.push(newThread);
+ idThreadMap[comment.id] = newThread;
+ }
+ return threads;
+};
+
+/**
+ * Whether the given comment should be included in the base side of the
+ * given patch range.
+ *
+ * @param {!Object} comment
+ * @param {!Gerrit.PatchRange} range
+ * @return {boolean}
+ */
+ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
+ // If the base of the patch range is a parent of a merge, and the comment
+ // appears on a specific parent then only show the comment if the parent
+ // index of the comment matches that of the range.
+ if (comment.parent && comment.side === PARENT) {
+ return this._isMergeParent(range.basePatchNum) &&
+ comment.parent === this._getParentIndex(range.basePatchNum);
+ }
+
+ // If the base of the range is the parent of the patch:
+ if (range.basePatchNum === PARENT &&
+ comment.side === PARENT &&
+ this._patchNumEquals(comment.patch_set, range.patchNum)) {
+ return true;
+ }
+ // If the base of the range is not the parent of the patch:
+ if (range.basePatchNum !== PARENT &&
+ comment.side !== PARENT &&
+ this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Whether the given comment should be included in the revision side of the
+ * given patch range.
+ *
+ * @param {!Object} comment
+ * @param {!Gerrit.PatchRange} range
+ * @return {boolean}
+ */
+ChangeComments.prototype._isInRevisionOfPatchRange = function(comment,
+ range) {
+ return comment.side !== PARENT &&
+ this._patchNumEquals(comment.patch_set, range.patchNum);
+};
+
+/**
+ * Whether the given comment should be included in the given patch range.
+ *
+ * @param {!Object} comment
+ * @param {!Gerrit.PatchRange} range
+ * @return {boolean|undefined}
+ */
+ChangeComments.prototype._isInPatchRange = function(comment, range) {
+ return this._isInBaseOfPatchRange(comment, range) ||
+ this._isInRevisionOfPatchRange(comment, range);
+};
+
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrCommentApi extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-comment-api'; }
+
+ static get properties() {
+ return {
+ _changeComments: Object,
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('reload-drafts',
+ changeNum => this.reloadDrafts(changeNum));
+ }
+
+ /**
+ * Load all comments (with drafts and robot comments) for the given change
+ * number. The returned promise resolves when the comments have loaded, but
+ * does not yield the comment data.
+ *
+ * @param {number} changeNum
+ * @return {!Promise<!Object>}
+ */
+ loadAll(changeNum) {
+ const promises = [];
+ promises.push(this.$.restAPI.getDiffComments(changeNum));
+ promises.push(this.$.restAPI.getDiffRobotComments(changeNum));
+ promises.push(this.$.restAPI.getDiffDrafts(changeNum));
+
+ return Promise.all(promises).then(([comments, robotComments, drafts]) => {
+ this._changeComments = new ChangeComments(comments,
+ robotComments, drafts, changeNum);
+ return this._changeComments;
+ });
+ }
+
+ /**
+ * Re-initialize _changeComments with a new ChangeComments object, that
+ * uses the previous values for comments and robot comments, but fetches
+ * updated draft comments.
+ *
+ * @param {number} changeNum
+ * @return {!Promise<!Object>}
+ */
+ reloadDrafts(changeNum) {
+ if (!this._changeComments) {
+ return this.loadAll(changeNum);
+ }
+ return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => {
+ this._changeComments = new ChangeComments(this._changeComments.comments,
+ this._changeComments.robotComments, drafts, changeNum);
+ return this._changeComments;
+ });
+ }
+}
+
+customElements.define(GrCommentApi.is, GrCommentApi);
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
index 317e9e5..215bfac 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
@@ -1,27 +1,21 @@
-<!--
-@license
-Copyright (C) 2017 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-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-comment-api">
- <template>
+export const htmlTemplate = html`
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-comment-api.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index f2f7c0f..e39319a 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-comment-api</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>
-<link rel="import" href="./gr-comment-api.html">
+<script type="module" src="./gr-comment-api.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-comment-api.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,697 +41,699 @@
</template>
</test-fixture>
-<script>
- suite('gr-comment-api tests', async () => {
- await readyToTest();
- const PARENT = 'PARENT';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-comment-api.js';
+suite('gr-comment-api tests', () => {
+ const PARENT = 'PARENT';
- let element;
- let sandbox;
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('loads logged-out', () => {
+ const changeNum = 1234;
+
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(false));
+ sandbox.stub(element.$.restAPI, 'getDiffComments')
+ .returns(Promise.resolve({
+ 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
+ .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
+ sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ .returns(Promise.resolve({}));
+
+ return element.loadAll(changeNum).then(() => {
+ assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
+ changeNum));
+ assert.isOk(element._changeComments._comments);
+ assert.isOk(element._changeComments._robotComments);
+ assert.deepEqual(element._changeComments._drafts, {});
+ });
+ });
+
+ test('loads logged-in', () => {
+ const changeNum = 1234;
+
+ sandbox.stub(element.$.restAPI, 'getLoggedIn')
+ .returns(Promise.resolve(true));
+ sandbox.stub(element.$.restAPI, 'getDiffComments')
+ .returns(Promise.resolve({
+ 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
+ .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
+ sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]}));
+
+ return element.loadAll(changeNum).then(() => {
+ assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
+ changeNum));
+ assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
+ changeNum));
+ assert.isOk(element._changeComments._comments);
+ assert.isOk(element._changeComments._robotComments);
+ assert.notDeepEqual(element._changeComments._drafts, {});
+ });
+ });
+
+ suite('reloadDrafts', () => {
+ let commentStub;
+ let robotCommentStub;
+ let draftStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('loads logged-out', () => {
- const changeNum = 1234;
-
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(false));
- sandbox.stub(element.$.restAPI, 'getDiffComments')
- .returns(Promise.resolve({
- 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
- }));
- sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
- .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
- sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments')
.returns(Promise.resolve({}));
+ robotCommentStub = sandbox.stub(element.$.restAPI,
+ 'getDiffRobotComments').returns(Promise.resolve({}));
+ draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ .returns(Promise.resolve({}));
+ });
- return element.loadAll(changeNum).then(() => {
- assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
- changeNum));
- assert.isOk(element._changeComments._comments);
- assert.isOk(element._changeComments._robotComments);
- assert.deepEqual(element._changeComments._drafts, {});
+ test('without loadAll first', done => {
+ assert.isNotOk(element._changeComments);
+ sandbox.spy(element, 'loadAll');
+ element.reloadDrafts().then(() => {
+ assert.isTrue(element.loadAll.called);
+ assert.isOk(element._changeComments);
+ assert.equal(commentStub.callCount, 1);
+ assert.equal(robotCommentStub.callCount, 1);
+ assert.equal(draftStub.callCount, 1);
+ done();
});
});
- test('loads logged-in', () => {
+ test('with loadAll first', done => {
+ assert.isNotOk(element._changeComments);
+ element.loadAll()
+ .then(() => {
+ assert.isOk(element._changeComments);
+ assert.equal(commentStub.callCount, 1);
+ assert.equal(robotCommentStub.callCount, 1);
+ assert.equal(draftStub.callCount, 1);
+ return element.reloadDrafts();
+ })
+ .then(() => {
+ assert.isOk(element._changeComments);
+ assert.equal(commentStub.callCount, 1);
+ assert.equal(robotCommentStub.callCount, 1);
+ assert.equal(draftStub.callCount, 2);
+ done();
+ });
+ });
+ });
+
+ suite('_changeComment methods', () => {
+ setup(done => {
const changeNum = 1234;
-
- sandbox.stub(element.$.restAPI, 'getLoggedIn')
- .returns(Promise.resolve(true));
- sandbox.stub(element.$.restAPI, 'getDiffComments')
- .returns(Promise.resolve({
- 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
- }));
- sandbox.stub(element.$.restAPI, 'getDiffRobotComments')
- .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
- sandbox.stub(element.$.restAPI, 'getDiffDrafts')
- .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]}));
-
- return element.loadAll(changeNum).then(() => {
- assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly(
- changeNum));
- assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly(
- changeNum));
- assert.isOk(element._changeComments._comments);
- assert.isOk(element._changeComments._robotComments);
- assert.notDeepEqual(element._changeComments._drafts, {});
+ stub('gr-rest-api-interface', {
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+ element.loadAll(changeNum).then(() => {
+ done();
});
});
- suite('reloadDrafts', () => {
- let commentStub;
- let robotCommentStub;
- let draftStub;
+ test('_isInBaseOfPatchRange', () => {
+ const comment = {patch_set: 1};
+ const patchRange = {basePatchNum: 1, patchNum: 2};
+ assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ patchRange.basePatchNum = PARENT;
+ assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ comment.side = PARENT;
+ assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ comment.patch_set = 2;
+ assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ patchRange.basePatchNum = -2;
+ comment.side = PARENT;
+ comment.parent = 1;
+ assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+
+ comment.parent = 2;
+ assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
+ patchRange));
+ });
+
+ test('_isInRevisionOfPatchRange', () => {
+ const comment = {patch_set: 123};
+ const patchRange = {basePatchNum: 122, patchNum: 124};
+ assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
+ comment, patchRange));
+
+ patchRange.patchNum = 123;
+ assert.isTrue(element._changeComments._isInRevisionOfPatchRange(
+ comment, patchRange));
+
+ comment.side = PARENT;
+ assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
+ comment, patchRange));
+ });
+
+ test('_isInPatchRange', () => {
+ const patchRange1 = {basePatchNum: 122, patchNum: 124};
+ const patchRange2 = {basePatchNum: 123, patchNum: 125};
+ const patchRange3 = {basePatchNum: 124, patchNum: 125};
+
+ const isInBasePatchStub = sandbox.stub(element._changeComments,
+ '_isInBaseOfPatchRange');
+ const isInRevisionPatchStub = sandbox.stub(element._changeComments,
+ '_isInRevisionOfPatchRange');
+
+ isInBasePatchStub.withArgs({}, patchRange1).returns(true);
+ isInBasePatchStub.withArgs({}, patchRange2).returns(false);
+ isInBasePatchStub.withArgs({}, patchRange3).returns(false);
+
+ isInRevisionPatchStub.withArgs({}, patchRange1).returns(false);
+ isInRevisionPatchStub.withArgs({}, patchRange2).returns(true);
+ isInRevisionPatchStub.withArgs({}, patchRange3).returns(false);
+
+ assert.isTrue(element._changeComments._isInPatchRange({}, patchRange1));
+ assert.isTrue(element._changeComments._isInPatchRange({}, patchRange2));
+ assert.isFalse(element._changeComments._isInPatchRange({},
+ patchRange3));
+ });
+
+ suite('comment ranges and paths', () => {
+ function makeTime(mins) {
+ return `2013-02-26 15:0${mins}:43.986000000`;
+ }
+
setup(() => {
- commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments')
- .returns(Promise.resolve({}));
- robotCommentStub = sandbox.stub(element.$.restAPI,
- 'getDiffRobotComments').returns(Promise.resolve({}));
- draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts')
- .returns(Promise.resolve({}));
+ element._changeComments._drafts = {
+ 'file/one': [
+ {
+ id: 11,
+ patch_set: 2,
+ side: PARENT,
+ line: 1,
+ updated: makeTime(3),
+ },
+ {
+ id: 12,
+ in_reply_to: 2,
+ patch_set: 2,
+ line: 1,
+ updated: makeTime(3),
+ },
+ ],
+ 'file/two': [
+ {
+ id: 5,
+ patch_set: 3,
+ line: 1,
+ updated: makeTime(3),
+ },
+ ],
+ };
+ element._changeComments._robotComments = {
+ 'file/one': [
+ {
+ id: 1,
+ patch_set: 2,
+ side: PARENT,
+ line: 1,
+ updated: makeTime(1),
+ range: {
+ start_line: 1,
+ start_character: 2,
+ end_line: 2,
+ end_character: 2,
+ },
+ }, {
+ id: 2,
+ in_reply_to: 4,
+ patch_set: 2,
+ unresolved: true,
+ line: 1,
+ updated: makeTime(2),
+ },
+ ],
+ };
+ element._changeComments._comments = {
+ 'file/one': [
+ {id: 3, patch_set: 2, side: PARENT, line: 2, updated: makeTime(1)},
+ {id: 4, patch_set: 2, line: 1, updated: makeTime(1)},
+ ],
+ 'file/two': [
+ {id: 5, patch_set: 2, line: 2, updated: makeTime(1)},
+ {id: 6, patch_set: 3, line: 2, updated: makeTime(1)},
+ ],
+ 'file/three': [
+ {
+ id: 7,
+ patch_set: 2,
+ side: PARENT,
+ unresolved: true,
+ line: 1,
+ updated: makeTime(1),
+ },
+ {id: 8, patch_set: 3, line: 1, updated: makeTime(1)},
+ ],
+ 'file/four': [
+ {id: 9, patch_set: 5, side: PARENT, line: 1, updated: makeTime(1)},
+ {id: 10, patch_set: 5, line: 1, updated: makeTime(1)},
+ ],
+ };
});
- test('without loadAll first', done => {
- assert.isNotOk(element._changeComments);
- sandbox.spy(element, 'loadAll');
- element.reloadDrafts().then(() => {
- assert.isTrue(element.loadAll.called);
- assert.isOk(element._changeComments);
- assert.equal(commentStub.callCount, 1);
- assert.equal(robotCommentStub.callCount, 1);
- assert.equal(draftStub.callCount, 1);
- done();
- });
- });
-
- test('with loadAll first', done => {
- assert.isNotOk(element._changeComments);
- element.loadAll()
- .then(() => {
- assert.isOk(element._changeComments);
- assert.equal(commentStub.callCount, 1);
- assert.equal(robotCommentStub.callCount, 1);
- assert.equal(draftStub.callCount, 1);
- return element.reloadDrafts();
- })
- .then(() => {
- assert.isOk(element._changeComments);
- assert.equal(commentStub.callCount, 1);
- assert.equal(robotCommentStub.callCount, 1);
- assert.equal(draftStub.callCount, 2);
- done();
- });
- });
- });
-
- suite('_changeComment methods', () => {
- setup(done => {
- const changeNum = 1234;
- stub('gr-rest-api-interface', {
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- });
- element.loadAll(changeNum).then(() => {
- done();
- });
- });
-
- test('_isInBaseOfPatchRange', () => {
- const comment = {patch_set: 1};
- const patchRange = {basePatchNum: 1, patchNum: 2};
- assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ test('getPaths', () => {
+ const patchRange = {basePatchNum: 1, patchNum: 4};
+ let paths = element._changeComments.getPaths(patchRange);
+ assert.equal(Object.keys(paths).length, 0);
patchRange.basePatchNum = PARENT;
- assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ patchRange.patchNum = 3;
+ paths = element._changeComments.getPaths(patchRange);
+ assert.notProperty(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.notProperty(paths, 'file/four');
- comment.side = PARENT;
- assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ patchRange.patchNum = 2;
+ paths = element._changeComments.getPaths(patchRange);
+ assert.property(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.notProperty(paths, 'file/four');
- comment.patch_set = 2;
- assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
-
- patchRange.basePatchNum = -2;
- comment.side = PARENT;
- comment.parent = 1;
- assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
-
- comment.parent = 2;
- assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment,
- patchRange));
+ paths = element._changeComments.getPaths();
+ assert.property(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.property(paths, 'file/four');
});
- test('_isInRevisionOfPatchRange', () => {
- const comment = {patch_set: 123};
- const patchRange = {basePatchNum: 122, patchNum: 124};
- assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
- comment, patchRange));
+ test('getCommentsBySideForPath', () => {
+ const patchRange = {basePatchNum: 1, patchNum: 3};
+ let path = 'file/one';
+ let comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.meta.changeNum, 1234);
+ assert.equal(comments.left.length, 0);
+ assert.equal(comments.right.length, 0);
- patchRange.patchNum = 123;
- assert.isTrue(element._changeComments._isInRevisionOfPatchRange(
- comment, patchRange));
+ path = 'file/two';
+ comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.left.length, 0);
+ assert.equal(comments.right.length, 2);
- comment.side = PARENT;
- assert.isFalse(element._changeComments._isInRevisionOfPatchRange(
- comment, patchRange));
+ patchRange.basePatchNum = 2;
+ comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.left.length, 1);
+ assert.equal(comments.right.length, 2);
+
+ patchRange.basePatchNum = PARENT;
+ path = 'file/three';
+ comments = element._changeComments.getCommentsBySideForPath(path,
+ patchRange);
+ assert.equal(comments.left.length, 0);
+ assert.equal(comments.right.length, 1);
});
- test('_isInPatchRange', () => {
- const patchRange1 = {basePatchNum: 122, patchNum: 124};
- const patchRange2 = {basePatchNum: 123, patchNum: 125};
- const patchRange3 = {basePatchNum: 124, patchNum: 125};
-
- const isInBasePatchStub = sandbox.stub(element._changeComments,
- '_isInBaseOfPatchRange');
- const isInRevisionPatchStub = sandbox.stub(element._changeComments,
- '_isInRevisionOfPatchRange');
-
- isInBasePatchStub.withArgs({}, patchRange1).returns(true);
- isInBasePatchStub.withArgs({}, patchRange2).returns(false);
- isInBasePatchStub.withArgs({}, patchRange3).returns(false);
-
- isInRevisionPatchStub.withArgs({}, patchRange1).returns(false);
- isInRevisionPatchStub.withArgs({}, patchRange2).returns(true);
- isInRevisionPatchStub.withArgs({}, patchRange3).returns(false);
-
- assert.isTrue(element._changeComments._isInPatchRange({}, patchRange1));
- assert.isTrue(element._changeComments._isInPatchRange({}, patchRange2));
- assert.isFalse(element._changeComments._isInPatchRange({},
- patchRange3));
+ test('getAllCommentsForPath', () => {
+ let path = 'file/one';
+ let comments = element._changeComments.getAllCommentsForPath(path);
+ assert.deepEqual(comments.length, 4);
+ path = 'file/two';
+ comments = element._changeComments.getAllCommentsForPath(path, 2);
+ assert.deepEqual(comments.length, 1);
});
- suite('comment ranges and paths', () => {
- function makeTime(mins) {
- return `2013-02-26 15:0${mins}:43.986000000`;
- }
+ test('getAllDraftsForPath', () => {
+ const path = 'file/one';
+ const drafts = element._changeComments.getAllDraftsForPath(path);
+ assert.deepEqual(drafts.length, 2);
+ });
- setup(() => {
- element._changeComments._drafts = {
- 'file/one': [
- {
- id: 11,
- patch_set: 2,
- side: PARENT,
- line: 1,
- updated: makeTime(3),
- },
- {
- id: 12,
- in_reply_to: 2,
- patch_set: 2,
- line: 1,
- updated: makeTime(3),
- },
- ],
- 'file/two': [
+ test('computeUnresolvedNum', () => {
+ assert.equal(element._changeComments
+ .computeUnresolvedNum(2, 'file/one'), 0);
+ assert.equal(element._changeComments
+ .computeUnresolvedNum(1, 'file/one'), 0);
+ assert.equal(element._changeComments
+ .computeUnresolvedNum(2, 'file/three'), 1);
+ });
+
+ test('computeUnresolvedNum w/ non-linear thread', () => {
+ element._changeComments._drafts = {};
+ element._changeComments._robotComments = {};
+ element._changeComments._comments = {
+ path: [{
+ id: '9c6ba3c6_28b7d467',
+ patch_set: 1,
+ updated: '2018-02-28 14:41:13.000000000',
+ unresolved: true,
+ }, {
+ id: '3df7b331_0bead405',
+ patch_set: 1,
+ in_reply_to: '1c346623_ab85d14a',
+ updated: '2018-02-28 23:07:55.000000000',
+ unresolved: false,
+ }, {
+ id: '6153dce6_69958d1e',
+ patch_set: 1,
+ in_reply_to: '9c6ba3c6_28b7d467',
+ updated: '2018-02-28 17:11:31.000000000',
+ unresolved: true,
+ }, {
+ id: '1c346623_ab85d14a',
+ patch_set: 1,
+ in_reply_to: '9c6ba3c6_28b7d467',
+ updated: '2018-02-28 23:01:39.000000000',
+ unresolved: false,
+ }],
+ };
+ assert.equal(
+ element._changeComments.computeUnresolvedNum(1, 'path'), 0);
+ });
+
+ test('computeCommentCount', () => {
+ assert.equal(element._changeComments
+ .computeCommentCount(2, 'file/one'), 4);
+ assert.equal(element._changeComments
+ .computeCommentCount(1, 'file/one'), 0);
+ assert.equal(element._changeComments
+ .computeCommentCount(2, 'file/three'), 1);
+ });
+
+ test('computeDraftCount', () => {
+ assert.equal(element._changeComments
+ .computeDraftCount(2, 'file/one'), 2);
+ assert.equal(element._changeComments
+ .computeDraftCount(1, 'file/one'), 0);
+ assert.equal(element._changeComments
+ .computeDraftCount(2, 'file/three'), 0);
+ assert.equal(element._changeComments
+ .computeDraftCount(), 3);
+ });
+
+ test('getAllPublishedComments', () => {
+ let publishedComments = element._changeComments
+ .getAllPublishedComments();
+ assert.equal(Object.keys(publishedComments).length, 4);
+ assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
+ assert.equal(Object.keys(publishedComments[['file/two']]).length, 2);
+ publishedComments = element._changeComments
+ .getAllPublishedComments(2);
+ assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
+ assert.equal(Object.keys(publishedComments[['file/two']]).length, 1);
+ });
+
+ test('getAllComments', () => {
+ let comments = element._changeComments.getAllComments();
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 4);
+ assert.equal(Object.keys(comments[['file/two']]).length, 2);
+ comments = element._changeComments.getAllComments(false, 2);
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 4);
+ assert.equal(Object.keys(comments[['file/two']]).length, 1);
+ // Include drafts
+ comments = element._changeComments.getAllComments(true);
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 6);
+ assert.equal(Object.keys(comments[['file/two']]).length, 3);
+ comments = element._changeComments.getAllComments(true, 2);
+ assert.equal(Object.keys(comments).length, 4);
+ assert.equal(Object.keys(comments[['file/one']]).length, 6);
+ assert.equal(Object.keys(comments[['file/two']]).length, 1);
+ });
+
+ test('computeAllThreads', () => {
+ const expectedThreads = [
+ {
+ comments: [
{
id: 5,
- patch_set: 3,
- line: 1,
- updated: makeTime(3),
+ patch_set: 2,
+ line: 2,
+ __path: 'file/two',
+ updated: '2013-02-26 15:01:43.986000000',
},
],
- };
- element._changeComments._robotComments = {
- 'file/one': [
+ patchNum: 2,
+ path: 'file/two',
+ line: 2,
+ rootId: 5,
+ }, {
+ comments: [
+ {
+ id: 3,
+ patch_set: 2,
+ side: 'PARENT',
+ line: 2,
+ __path: 'file/one',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
+ line: 2,
+ rootId: 3,
+ }, {
+ comments: [
{
id: 1,
patch_set: 2,
- side: PARENT,
+ side: 'PARENT',
line: 1,
- updated: makeTime(1),
+ updated: '2013-02-26 15:01:43.986000000',
range: {
start_line: 1,
start_character: 2,
end_line: 2,
end_character: 2,
},
- }, {
+ __path: 'file/one',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
+ line: 1,
+ rootId: 1,
+ }, {
+ comments: [
+ {
+ id: 9,
+ patch_set: 5,
+ side: 'PARENT',
+ line: 1,
+ __path: 'file/four',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 5,
+ path: 'file/four',
+ line: 1,
+ rootId: 9,
+ }, {
+ comments: [
+ {
+ id: 8,
+ patch_set: 3,
+ line: 1,
+ __path: 'file/three',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ patchNum: 3,
+ path: 'file/three',
+ line: 1,
+ rootId: 8,
+ }, {
+ comments: [
+ {
+ id: 7,
+ patch_set: 2,
+ side: 'PARENT',
+ unresolved: true,
+ line: 1,
+ __path: 'file/three',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/three',
+ line: 1,
+ rootId: 7,
+ }, {
+ comments: [
+ {
+ id: 4,
+ patch_set: 2,
+ line: 1,
+ __path: 'file/one',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ {
id: 2,
in_reply_to: 4,
patch_set: 2,
unresolved: true,
line: 1,
- updated: makeTime(2),
+ __path: 'file/one',
+ updated: '2013-02-26 15:02:43.986000000',
},
- ],
- };
- element._changeComments._comments = {
- 'file/one': [
- {id: 3, patch_set: 2, side: PARENT, line: 2, updated: makeTime(1)},
- {id: 4, patch_set: 2, line: 1, updated: makeTime(1)},
- ],
- 'file/two': [
- {id: 5, patch_set: 2, line: 2, updated: makeTime(1)},
- {id: 6, patch_set: 3, line: 2, updated: makeTime(1)},
- ],
- 'file/three': [
{
- id: 7,
+ id: 12,
+ in_reply_to: 2,
patch_set: 2,
- side: PARENT,
- unresolved: true,
line: 1,
- updated: makeTime(1),
+ __path: 'file/one',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
},
- {id: 8, patch_set: 3, line: 1, updated: makeTime(1)},
],
- 'file/four': [
- {id: 9, patch_set: 5, side: PARENT, line: 1, updated: makeTime(1)},
- {id: 10, patch_set: 5, line: 1, updated: makeTime(1)},
- ],
- };
- });
-
- test('getPaths', () => {
- const patchRange = {basePatchNum: 1, patchNum: 4};
- let paths = element._changeComments.getPaths(patchRange);
- assert.equal(Object.keys(paths).length, 0);
-
- patchRange.basePatchNum = PARENT;
- patchRange.patchNum = 3;
- paths = element._changeComments.getPaths(patchRange);
- assert.notProperty(paths, 'file/one');
- assert.property(paths, 'file/two');
- assert.property(paths, 'file/three');
- assert.notProperty(paths, 'file/four');
-
- patchRange.patchNum = 2;
- paths = element._changeComments.getPaths(patchRange);
- assert.property(paths, 'file/one');
- assert.property(paths, 'file/two');
- assert.property(paths, 'file/three');
- assert.notProperty(paths, 'file/four');
-
- paths = element._changeComments.getPaths();
- assert.property(paths, 'file/one');
- assert.property(paths, 'file/two');
- assert.property(paths, 'file/three');
- assert.property(paths, 'file/four');
- });
-
- test('getCommentsBySideForPath', () => {
- const patchRange = {basePatchNum: 1, patchNum: 3};
- let path = 'file/one';
- let comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.meta.changeNum, 1234);
- assert.equal(comments.left.length, 0);
- assert.equal(comments.right.length, 0);
-
- path = 'file/two';
- comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.left.length, 0);
- assert.equal(comments.right.length, 2);
-
- patchRange.basePatchNum = 2;
- comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.left.length, 1);
- assert.equal(comments.right.length, 2);
-
- patchRange.basePatchNum = PARENT;
- path = 'file/three';
- comments = element._changeComments.getCommentsBySideForPath(path,
- patchRange);
- assert.equal(comments.left.length, 0);
- assert.equal(comments.right.length, 1);
- });
-
- test('getAllCommentsForPath', () => {
- let path = 'file/one';
- let comments = element._changeComments.getAllCommentsForPath(path);
- assert.deepEqual(comments.length, 4);
- path = 'file/two';
- comments = element._changeComments.getAllCommentsForPath(path, 2);
- assert.deepEqual(comments.length, 1);
- });
-
- test('getAllDraftsForPath', () => {
- const path = 'file/one';
- const drafts = element._changeComments.getAllDraftsForPath(path);
- assert.deepEqual(drafts.length, 2);
- });
-
- test('computeUnresolvedNum', () => {
- assert.equal(element._changeComments
- .computeUnresolvedNum(2, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeUnresolvedNum(1, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeUnresolvedNum(2, 'file/three'), 1);
- });
-
- test('computeUnresolvedNum w/ non-linear thread', () => {
- element._changeComments._drafts = {};
- element._changeComments._robotComments = {};
- element._changeComments._comments = {
- path: [{
- id: '9c6ba3c6_28b7d467',
- patch_set: 1,
- updated: '2018-02-28 14:41:13.000000000',
- unresolved: true,
- }, {
- id: '3df7b331_0bead405',
- patch_set: 1,
- in_reply_to: '1c346623_ab85d14a',
- updated: '2018-02-28 23:07:55.000000000',
- unresolved: false,
- }, {
- id: '6153dce6_69958d1e',
- patch_set: 1,
- in_reply_to: '9c6ba3c6_28b7d467',
- updated: '2018-02-28 17:11:31.000000000',
- unresolved: true,
- }, {
- id: '1c346623_ab85d14a',
- patch_set: 1,
- in_reply_to: '9c6ba3c6_28b7d467',
- updated: '2018-02-28 23:01:39.000000000',
- unresolved: false,
- }],
- };
- assert.equal(
- element._changeComments.computeUnresolvedNum(1, 'path'), 0);
- });
-
- test('computeCommentCount', () => {
- assert.equal(element._changeComments
- .computeCommentCount(2, 'file/one'), 4);
- assert.equal(element._changeComments
- .computeCommentCount(1, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeCommentCount(2, 'file/three'), 1);
- });
-
- test('computeDraftCount', () => {
- assert.equal(element._changeComments
- .computeDraftCount(2, 'file/one'), 2);
- assert.equal(element._changeComments
- .computeDraftCount(1, 'file/one'), 0);
- assert.equal(element._changeComments
- .computeDraftCount(2, 'file/three'), 0);
- assert.equal(element._changeComments
- .computeDraftCount(), 3);
- });
-
- test('getAllPublishedComments', () => {
- let publishedComments = element._changeComments
- .getAllPublishedComments();
- assert.equal(Object.keys(publishedComments).length, 4);
- assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
- assert.equal(Object.keys(publishedComments[['file/two']]).length, 2);
- publishedComments = element._changeComments
- .getAllPublishedComments(2);
- assert.equal(Object.keys(publishedComments[['file/one']]).length, 4);
- assert.equal(Object.keys(publishedComments[['file/two']]).length, 1);
- });
-
- test('getAllComments', () => {
- let comments = element._changeComments.getAllComments();
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 4);
- assert.equal(Object.keys(comments[['file/two']]).length, 2);
- comments = element._changeComments.getAllComments(false, 2);
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 4);
- assert.equal(Object.keys(comments[['file/two']]).length, 1);
- // Include drafts
- comments = element._changeComments.getAllComments(true);
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 6);
- assert.equal(Object.keys(comments[['file/two']]).length, 3);
- comments = element._changeComments.getAllComments(true, 2);
- assert.equal(Object.keys(comments).length, 4);
- assert.equal(Object.keys(comments[['file/one']]).length, 6);
- assert.equal(Object.keys(comments[['file/two']]).length, 1);
- });
-
- test('computeAllThreads', () => {
- const expectedThreads = [
- {
- comments: [
- {
- id: 5,
- patch_set: 2,
- line: 2,
- __path: 'file/two',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- patchNum: 2,
- path: 'file/two',
- line: 2,
- rootId: 5,
- }, {
- comments: [
- {
- id: 3,
- patch_set: 2,
- side: 'PARENT',
- line: 2,
- __path: 'file/one',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/one',
- line: 2,
- rootId: 3,
- }, {
- comments: [
- {
- id: 1,
- patch_set: 2,
- side: 'PARENT',
- line: 1,
- updated: '2013-02-26 15:01:43.986000000',
- range: {
- start_line: 1,
- start_character: 2,
- end_line: 2,
- end_character: 2,
- },
- __path: 'file/one',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/one',
- line: 1,
- rootId: 1,
- }, {
- comments: [
- {
- id: 9,
- patch_set: 5,
- side: 'PARENT',
- line: 1,
- __path: 'file/four',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 5,
- path: 'file/four',
- line: 1,
- rootId: 9,
- }, {
- comments: [
- {
- id: 8,
- patch_set: 3,
- line: 1,
- __path: 'file/three',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- patchNum: 3,
- path: 'file/three',
- line: 1,
- rootId: 8,
- }, {
- comments: [
- {
- id: 7,
- patch_set: 2,
- side: 'PARENT',
- unresolved: true,
- line: 1,
- __path: 'file/three',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/three',
- line: 1,
- rootId: 7,
- }, {
- comments: [
- {
- id: 4,
- patch_set: 2,
- line: 1,
- __path: 'file/one',
- updated: '2013-02-26 15:01:43.986000000',
- },
- {
- id: 2,
- in_reply_to: 4,
- patch_set: 2,
- unresolved: true,
- line: 1,
- __path: 'file/one',
- updated: '2013-02-26 15:02:43.986000000',
- },
- {
- id: 12,
- in_reply_to: 2,
- patch_set: 2,
- line: 1,
- __path: 'file/one',
- __draft: true,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ],
- patchNum: 2,
- path: 'file/one',
- line: 1,
- rootId: 4,
- }, {
- comments: [
- {
- id: 6,
- patch_set: 3,
- line: 2,
- __path: 'file/two',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- patchNum: 3,
- path: 'file/two',
- line: 2,
- rootId: 6,
- }, {
- comments: [
- {
- id: 10,
- patch_set: 5,
- line: 1,
- __path: 'file/four',
- updated: '2013-02-26 15:01:43.986000000',
- },
- ],
- rootId: 10,
- patchNum: 5,
- path: 'file/four',
- line: 1,
- }, {
- comments: [
- {
- id: 5,
- patch_set: 3,
- line: 1,
- __path: 'file/two',
- __draft: true,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ],
- rootId: 5,
- patchNum: 3,
- path: 'file/two',
- line: 1,
- }, {
- comments: [
- {
- id: 11,
- patch_set: 2,
- side: 'PARENT',
- line: 1,
- __path: 'file/one',
- __draft: true,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ],
- rootId: 11,
- commentSide: 'PARENT',
- patchNum: 2,
- path: 'file/one',
- line: 1,
- },
- ];
- const threads = element._changeComments.getAllThreadsForChange();
- assert.deepEqual(threads, expectedThreads);
- });
-
- test('getCommentsForThreadGroup', () => {
- let expectedComments = [
- {
- __path: 'file/one',
- id: 4,
- patch_set: 2,
- line: 1,
- updated: '2013-02-26 15:01:43.986000000',
- },
- {
- __path: 'file/one',
- id: 2,
- in_reply_to: 4,
- patch_set: 2,
- unresolved: true,
- line: 1,
- updated: '2013-02-26 15:02:43.986000000',
- },
- {
- __path: 'file/one',
- __draft: true,
- id: 12,
- in_reply_to: 2,
- patch_set: 2,
- line: 1,
- updated: '2013-02-26 15:03:43.986000000',
- },
- ];
- assert.deepEqual(element._changeComments.getCommentsForThread(4),
- expectedComments);
-
- expectedComments = [{
- id: 11,
- patch_set: 2,
- side: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
line: 1,
+ rootId: 4,
+ }, {
+ comments: [
+ {
+ id: 6,
+ patch_set: 3,
+ line: 2,
+ __path: 'file/two',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ patchNum: 3,
+ path: 'file/two',
+ line: 2,
+ rootId: 6,
+ }, {
+ comments: [
+ {
+ id: 10,
+ patch_set: 5,
+ line: 1,
+ __path: 'file/four',
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ ],
+ rootId: 10,
+ patchNum: 5,
+ path: 'file/four',
+ line: 1,
+ }, {
+ comments: [
+ {
+ id: 5,
+ patch_set: 3,
+ line: 1,
+ __path: 'file/two',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
+ },
+ ],
+ rootId: 5,
+ patchNum: 3,
+ path: 'file/two',
+ line: 1,
+ }, {
+ comments: [
+ {
+ id: 11,
+ patch_set: 2,
+ side: 'PARENT',
+ line: 1,
+ __path: 'file/one',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
+ },
+ ],
+ rootId: 11,
+ commentSide: 'PARENT',
+ patchNum: 2,
+ path: 'file/one',
+ line: 1,
+ },
+ ];
+ const threads = element._changeComments.getAllThreadsForChange();
+ assert.deepEqual(threads, expectedThreads);
+ });
+
+ test('getCommentsForThreadGroup', () => {
+ let expectedComments = [
+ {
+ __path: 'file/one',
+ id: 4,
+ patch_set: 2,
+ line: 1,
+ updated: '2013-02-26 15:01:43.986000000',
+ },
+ {
+ __path: 'file/one',
+ id: 2,
+ in_reply_to: 4,
+ patch_set: 2,
+ unresolved: true,
+ line: 1,
+ updated: '2013-02-26 15:02:43.986000000',
+ },
+ {
__path: 'file/one',
__draft: true,
+ id: 12,
+ in_reply_to: 2,
+ patch_set: 2,
+ line: 1,
updated: '2013-02-26 15:03:43.986000000',
- }];
+ },
+ ];
+ assert.deepEqual(element._changeComments.getCommentsForThread(4),
+ expectedComments);
- assert.deepEqual(element._changeComments.getCommentsForThread(11),
- expectedComments);
+ expectedComments = [{
+ id: 11,
+ patch_set: 2,
+ side: 'PARENT',
+ line: 1,
+ __path: 'file/one',
+ __draft: true,
+ updated: '2013-02-26 15:03:43.986000000',
+ }];
- assert.deepEqual(element._changeComments.getCommentsForThread(1000),
- null);
- });
+ assert.deepEqual(element._changeComments.getCommentsForThread(11),
+ expectedComments);
+
+ assert.deepEqual(element._changeComments.getCommentsForThread(1000),
+ null);
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
index 1bc4674..529c559 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -14,101 +14,108 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const TOOLTIP_MAP = new Map([
- [Gerrit.CoverageType.COVERED, 'Covered by tests.'],
- [Gerrit.CoverageType.NOT_COVERED, 'Not covered by tests.'],
- [Gerrit.CoverageType.PARTIALLY_COVERED, 'Partially covered by tests.'],
- [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
- ]);
+import '../../../types/types.js';
+import '../gr-diff-highlight/gr-annotation.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-coverage-layer_html.js';
- /** @extends Polymer.Element */
- class GrCoverageLayer extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-coverage-layer'; }
+const TOOLTIP_MAP = new Map([
+ [Gerrit.CoverageType.COVERED, 'Covered by tests.'],
+ [Gerrit.CoverageType.NOT_COVERED, 'Not covered by tests.'],
+ [Gerrit.CoverageType.PARTIALLY_COVERED, 'Partially covered by tests.'],
+ [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
+]);
- static get properties() {
- return {
- /**
- * Must be sorted by code_range.start_line.
- * Must only contain ranges that match the side.
- *
- * @type {!Array<!Gerrit.CoverageRange>}
- */
- coverageRanges: Array,
- side: String,
+/** @extends Polymer.Element */
+class GrCoverageLayer extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * We keep track of the line number from the previous annotate() call,
- * and also of the index of the coverage range that had matched.
- * annotate() calls are coming in with increasing line numbers and
- * coverage ranges are sorted by line number. So this is a very simple
- * and efficient way for finding the coverage range that matches a given
- * line number.
- */
- _lineNumber: {
- type: Number,
- value: 0,
- },
- _index: {
- type: Number,
- value: 0,
- },
- };
- }
+ static get is() { return 'gr-coverage-layer'; }
+ static get properties() {
+ return {
/**
- * Layer method to add annotations to a line.
+ * Must be sorted by code_range.start_line.
+ * Must only contain ranges that match the side.
*
- * @param {!HTMLElement} el Not used for this layer.
- * @param {!HTMLElement} lineNumberEl The <td> element with the line number.
- * @param {!Object} line Not used for this layer.
+ * @type {!Array<!Gerrit.CoverageRange>}
*/
- annotate(el, lineNumberEl, line) {
- if (!lineNumberEl || !lineNumberEl.classList.contains(this.side)) {
- return;
- }
- const elementLineNumber = parseInt(
- lineNumberEl.getAttribute('data-value'), 10);
- if (!elementLineNumber || elementLineNumber < 1) return;
+ coverageRanges: Array,
+ side: String,
- // If the line number is smaller than before, then we have to reset our
- // algorithm and start searching the coverage ranges from the beginning.
- // That happens for example when you expand diff sections.
- if (elementLineNumber < this._lineNumber) {
- this._index = 0;
- }
- this._lineNumber = elementLineNumber;
-
- // We simply loop through all the coverage ranges until we find one that
- // matches the line number.
- while (this._index < this.coverageRanges.length) {
- const coverageRange = this.coverageRanges[this._index];
-
- // If the line number has moved past the current coverage range, then
- // try the next coverage range.
- if (this._lineNumber > coverageRange.code_range.end_line) {
- this._index++;
- continue;
- }
-
- // If the line number has not reached the next coverage range (and the
- // range before also did not match), then this line has not been
- // instrumented. Nothing to do for this line.
- if (this._lineNumber < coverageRange.code_range.start_line) {
- return;
- }
-
- // The line number is within the current coverage range. Style it!
- lineNumberEl.classList.add(coverageRange.type);
- lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type);
- return;
- }
- }
+ /**
+ * We keep track of the line number from the previous annotate() call,
+ * and also of the index of the coverage range that had matched.
+ * annotate() calls are coming in with increasing line numbers and
+ * coverage ranges are sorted by line number. So this is a very simple
+ * and efficient way for finding the coverage range that matches a given
+ * line number.
+ */
+ _lineNumber: {
+ type: Number,
+ value: 0,
+ },
+ _index: {
+ type: Number,
+ value: 0,
+ },
+ };
}
- customElements.define(GrCoverageLayer.is, GrCoverageLayer);
-})();
+ /**
+ * Layer method to add annotations to a line.
+ *
+ * @param {!HTMLElement} el Not used for this layer.
+ * @param {!HTMLElement} lineNumberEl The <td> element with the line number.
+ * @param {!Object} line Not used for this layer.
+ */
+ annotate(el, lineNumberEl, line) {
+ if (!lineNumberEl || !lineNumberEl.classList.contains(this.side)) {
+ return;
+ }
+ const elementLineNumber = parseInt(
+ lineNumberEl.getAttribute('data-value'), 10);
+ if (!elementLineNumber || elementLineNumber < 1) return;
+
+ // If the line number is smaller than before, then we have to reset our
+ // algorithm and start searching the coverage ranges from the beginning.
+ // That happens for example when you expand diff sections.
+ if (elementLineNumber < this._lineNumber) {
+ this._index = 0;
+ }
+ this._lineNumber = elementLineNumber;
+
+ // We simply loop through all the coverage ranges until we find one that
+ // matches the line number.
+ while (this._index < this.coverageRanges.length) {
+ const coverageRange = this.coverageRanges[this._index];
+
+ // If the line number has moved past the current coverage range, then
+ // try the next coverage range.
+ if (this._lineNumber > coverageRange.code_range.end_line) {
+ this._index++;
+ continue;
+ }
+
+ // If the line number has not reached the next coverage range (and the
+ // range before also did not match), then this line has not been
+ // instrumented. Nothing to do for this line.
+ if (this._lineNumber < coverageRange.code_range.start_line) {
+ return;
+ }
+
+ // The line number is within the current coverage range. Style it!
+ lineNumberEl.classList.add(coverageRange.type);
+ lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type);
+ return;
+ }
+ }
+}
+
+customElements.define(GrCoverageLayer.is, GrCoverageLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
index 63517cf..29757e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
@@ -1,26 +1,21 @@
-<!--
-@license
-Copyright (C) 2019 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
+export const htmlTemplate = html`
-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">
-<script src="../../../types/types.js"></script>
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-
-<dom-module id="gr-coverage-layer">
- <template>
- </template>
- <script src="gr-coverage-layer.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
index 8439a22..69948c3 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-coverage-layer</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="../gr-diff/gr-diff-line.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="../gr-diff/gr-diff-line.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
-<link rel="import" href="gr-coverage-layer.html">
+<script type="module" src="./gr-coverage-layer.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../gr-diff/gr-diff-line.js';
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-coverage-layer.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,106 +43,109 @@
</template>
</test-fixture>
-<script>
- suite('gr-coverage-layer', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../gr-diff/gr-diff-line.js';
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-coverage-layer.js';
+suite('gr-coverage-layer', () => {
+ let element;
- setup(() => {
- const initialCoverageRanges = [
- {
- type: 'COVERED',
- side: 'right',
- code_range: {
- start_line: 1,
- end_line: 2,
- },
+ setup(() => {
+ const initialCoverageRanges = [
+ {
+ type: 'COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 1,
+ end_line: 2,
},
- {
- type: 'NOT_COVERED',
- side: 'right',
- code_range: {
- start_line: 3,
- end_line: 4,
- },
+ },
+ {
+ type: 'NOT_COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 3,
+ end_line: 4,
},
- {
- type: 'PARTIALLY_COVERED',
- side: 'right',
- code_range: {
- start_line: 5,
- end_line: 6,
- },
+ },
+ {
+ type: 'PARTIALLY_COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 5,
+ end_line: 6,
},
- {
- type: 'NOT_INSTRUMENTED',
- side: 'right',
- code_range: {
- start_line: 8,
- end_line: 9,
- },
+ },
+ {
+ type: 'NOT_INSTRUMENTED',
+ side: 'right',
+ code_range: {
+ start_line: 8,
+ end_line: 9,
},
- ];
+ },
+ ];
- element = fixture('basic');
- element.coverageRanges = initialCoverageRanges;
- element.side = 'right';
+ element = fixture('basic');
+ element.coverageRanges = initialCoverageRanges;
+ element.side = 'right';
+ });
+
+ suite('annotate', () => {
+ function createLine(lineNumber) {
+ const lineEl = document.createElement('div');
+ lineEl.setAttribute('data-side', 'right');
+ lineEl.setAttribute('data-value', lineNumber);
+ lineEl.className = 'right';
+ return lineEl;
+ }
+
+ function checkLine(lineNumber, className, opt_negated) {
+ const line = createLine(lineNumber);
+ element.annotate(undefined, line, undefined);
+ let contains = line.classList.contains(className);
+ if (opt_negated) contains = !contains;
+ assert.isTrue(contains);
+ }
+
+ test('line 1-2 are covered', () => {
+ checkLine(1, 'COVERED');
+ checkLine(2, 'COVERED');
});
- suite('annotate', () => {
- function createLine(lineNumber) {
- const lineEl = document.createElement('div');
- lineEl.setAttribute('data-side', 'right');
- lineEl.setAttribute('data-value', lineNumber);
- lineEl.className = 'right';
- return lineEl;
- }
+ test('line 3-4 are not covered', () => {
+ checkLine(3, 'NOT_COVERED');
+ checkLine(4, 'NOT_COVERED');
+ });
- function checkLine(lineNumber, className, opt_negated) {
- const line = createLine(lineNumber);
- element.annotate(undefined, line, undefined);
- let contains = line.classList.contains(className);
- if (opt_negated) contains = !contains;
- assert.isTrue(contains);
- }
+ test('line 5-6 are partially covered', () => {
+ checkLine(5, 'PARTIALLY_COVERED');
+ checkLine(6, 'PARTIALLY_COVERED');
+ });
- test('line 1-2 are covered', () => {
- checkLine(1, 'COVERED');
- checkLine(2, 'COVERED');
- });
+ test('line 7 is implicitly not instrumented', () => {
+ checkLine(7, 'COVERED', true);
+ checkLine(7, 'NOT_COVERED', true);
+ checkLine(7, 'PARTIALLY_COVERED', true);
+ checkLine(7, 'NOT_INSTRUMENTED', true);
+ });
- test('line 3-4 are not covered', () => {
- checkLine(3, 'NOT_COVERED');
- checkLine(4, 'NOT_COVERED');
- });
+ test('line 8-9 are not instrumented', () => {
+ checkLine(8, 'NOT_INSTRUMENTED');
+ checkLine(9, 'NOT_INSTRUMENTED');
+ });
- test('line 5-6 are partially covered', () => {
- checkLine(5, 'PARTIALLY_COVERED');
- checkLine(6, 'PARTIALLY_COVERED');
- });
-
- test('line 7 is implicitly not instrumented', () => {
- checkLine(7, 'COVERED', true);
- checkLine(7, 'NOT_COVERED', true);
- checkLine(7, 'PARTIALLY_COVERED', true);
- checkLine(7, 'NOT_INSTRUMENTED', true);
- });
-
- test('line 8-9 are not instrumented', () => {
- checkLine(8, 'NOT_INSTRUMENTED');
- checkLine(9, 'NOT_INSTRUMENTED');
- });
-
- test('coverage correct, if annotate is called out of order', () => {
- checkLine(8, 'NOT_INSTRUMENTED');
- checkLine(1, 'COVERED');
- checkLine(5, 'PARTIALLY_COVERED');
- checkLine(3, 'NOT_COVERED');
- checkLine(6, 'PARTIALLY_COVERED');
- checkLine(4, 'NOT_COVERED');
- checkLine(9, 'NOT_INSTRUMENTED');
- checkLine(2, 'COVERED');
- });
+ test('coverage correct, if annotate is called out of order', () => {
+ checkLine(8, 'NOT_INSTRUMENTED');
+ checkLine(1, 'COVERED');
+ checkLine(5, 'PARTIALLY_COVERED');
+ checkLine(3, 'NOT_COVERED');
+ checkLine(6, 'PARTIALLY_COVERED');
+ checkLine(4, 'NOT_COVERED');
+ checkLine(9, 'NOT_INSTRUMENTED');
+ checkLine(2, 'COVERED');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
index 9e1ec9e..637c8f7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -1,417 +1,438 @@
/**
* @license
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the 'License');
+ * 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,
+ * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-coverage-layer/gr-coverage-layer.js';
+import '../gr-diff-processor/gr-diff-processor.js';
+import '../../shared/gr-hovercard/gr-hovercard.js';
+import '../gr-ranged-comment-layer/gr-ranged-comment-layer.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import './gr-diff-builder-side-by-side.js';
+import './gr-diff-builder-unified.js';
+import './gr-diff-builder-image.js';
+import './gr-diff-builder-binary.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-diff-builder-element_html.js';
- const TRAILING_WHITESPACE_PATTERN = /\s+$/;
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- // https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
- const COMMIT_MSG_PATH = '/COMMIT_MSG';
- const COMMIT_MSG_LINE_LENGTH = 72;
+const TRAILING_WHITESPACE_PATTERN = /\s+$/;
+
+// https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
+const COMMIT_MSG_PATH = '/COMMIT_MSG';
+const COMMIT_MSG_LINE_LENGTH = 72;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ */
+class GrDiffBuilderElement extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-builder'; }
+ /**
+ * Fired when the diff begins rendering.
+ *
+ * @event render-start
+ */
/**
- * @appliesMixin Gerrit.FireMixin
+ * Fired when the diff finishes rendering text content.
+ *
+ * @event render-content
*/
- class GrDiffBuilderElement extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-builder'; }
- /**
- * Fired when the diff begins rendering.
- *
- * @event render-start
- */
- /**
- * Fired when the diff finishes rendering text content.
- *
- * @event render-content
- */
+ static get properties() {
+ return {
+ diff: Object,
+ changeNum: String,
+ patchNum: String,
+ viewMode: String,
+ isImageDiff: Boolean,
+ baseImage: Object,
+ revisionImage: Object,
+ parentIndex: Number,
+ path: String,
+ projectName: String,
- static get properties() {
- return {
- diff: Object,
- changeNum: String,
- patchNum: String,
- viewMode: String,
- isImageDiff: Boolean,
- baseImage: Object,
- revisionImage: Object,
- parentIndex: Number,
- path: String,
- projectName: String,
+ _builder: Object,
+ _groups: Array,
+ _layers: Array,
+ _showTabs: Boolean,
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ commentRanges: {
+ type: Array,
+ value: () => [],
+ },
+ /** @type {!Array<!Gerrit.CoverageRange>} */
+ coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
+ _leftCoverageRanges: {
+ type: Array,
+ computed: '_computeLeftCoverageRanges(coverageRanges)',
+ },
+ _rightCoverageRanges: {
+ type: Array,
+ computed: '_computeRightCoverageRanges(coverageRanges)',
+ },
+ /**
+ * The promise last returned from `render()` while the asynchronous
+ * rendering is running - `null` otherwise. Provides a `cancel()`
+ * method that rejects it with `{isCancelled: true}`.
+ *
+ * @type {?Object}
+ */
+ _cancelableRenderPromise: Object,
+ layers: {
+ type: Array,
+ value: [],
+ },
+ };
+ }
- _builder: Object,
- _groups: Array,
- _layers: Array,
- _showTabs: Boolean,
- /** @type {!Array<!Gerrit.HoveredRange>} */
- commentRanges: {
- type: Array,
- value: () => [],
- },
- /** @type {!Array<!Gerrit.CoverageRange>} */
- coverageRanges: {
- type: Array,
- value: () => [],
- },
- _leftCoverageRanges: {
- type: Array,
- computed: '_computeLeftCoverageRanges(coverageRanges)',
- },
- _rightCoverageRanges: {
- type: Array,
- computed: '_computeRightCoverageRanges(coverageRanges)',
- },
- /**
- * The promise last returned from `render()` while the asynchronous
- * rendering is running - `null` otherwise. Provides a `cancel()`
- * method that rejects it with `{isCancelled: true}`.
- *
- * @type {?Object}
- */
- _cancelableRenderPromise: Object,
- layers: {
- type: Array,
- value: [],
- },
- };
+ get diffElement() {
+ return this.queryEffectiveChildren('#diffTable');
+ }
+
+ static get observers() {
+ return [
+ '_groupsChanged(_groups.splices)',
+ ];
+ }
+
+ _computeLeftCoverageRanges(coverageRanges) {
+ return coverageRanges.filter(range => range && range.side === 'left');
+ }
+
+ _computeRightCoverageRanges(coverageRanges) {
+ return coverageRanges.filter(range => range && range.side === 'right');
+ }
+
+ render(keyLocations, prefs) {
+ // Setting up annotation layers must happen after plugins are
+ // installed, and |render| satisfies the requirement, however,
+ // |attached| doesn't because in the diff view page, the element is
+ // attached before plugins are installed.
+ this._setupAnnotationLayers();
+
+ this._showTabs = !!prefs.show_tabs;
+ this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
+
+ // Stop the processor if it's running.
+ this.cancel();
+
+ this._builder = this._getDiffBuilder(this.diff, prefs);
+
+ this.$.processor.context = prefs.context;
+ this.$.processor.keyLocations = keyLocations;
+
+ this._clearDiffContent();
+ this._builder.addColumns(this.diffElement, prefs.font_size);
+
+ const isBinary = !!(this.isImageDiff || this.diff.binary);
+
+ this.dispatchEvent(new CustomEvent(
+ 'render-start', {bubbles: true, composed: true}));
+ this._cancelableRenderPromise = util.makeCancelable(
+ this.$.processor.process(this.diff.content, isBinary)
+ .then(() => {
+ if (this.isImageDiff) {
+ this._builder.renderDiff();
+ }
+ this.dispatchEvent(new CustomEvent('render-content',
+ {bubbles: true, composed: true}));
+ }));
+ return this._cancelableRenderPromise
+ .finally(() => { this._cancelableRenderPromise = null; })
+ // Mocca testing does not like uncaught rejections, so we catch
+ // the cancels which are expected and should not throw errors in
+ // tests.
+ .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
+ }
+
+ _setupAnnotationLayers() {
+ const layers = [
+ this._createTrailingWhitespaceLayer(),
+ this._createIntralineLayer(),
+ this._createTabIndicatorLayer(),
+ this.$.rangeLayer,
+ this.$.coverageLayerLeft,
+ this.$.coverageLayerRight,
+ ];
+
+ if (this.layers) {
+ layers.push(...this.layers);
}
+ this._layers = layers;
+ }
- get diffElement() {
- return this.queryEffectiveChildren('#diffTable');
- }
-
- static get observers() {
- return [
- '_groupsChanged(_groups.splices)',
- ];
- }
-
- _computeLeftCoverageRanges(coverageRanges) {
- return coverageRanges.filter(range => range && range.side === 'left');
- }
-
- _computeRightCoverageRanges(coverageRanges) {
- return coverageRanges.filter(range => range && range.side === 'right');
- }
-
- render(keyLocations, prefs) {
- // Setting up annotation layers must happen after plugins are
- // installed, and |render| satisfies the requirement, however,
- // |attached| doesn't because in the diff view page, the element is
- // attached before plugins are installed.
- this._setupAnnotationLayers();
-
- this._showTabs = !!prefs.show_tabs;
- this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
-
- // Stop the processor if it's running.
- this.cancel();
-
- this._builder = this._getDiffBuilder(this.diff, prefs);
-
- this.$.processor.context = prefs.context;
- this.$.processor.keyLocations = keyLocations;
-
- this._clearDiffContent();
- this._builder.addColumns(this.diffElement, prefs.font_size);
-
- const isBinary = !!(this.isImageDiff || this.diff.binary);
-
- this.dispatchEvent(new CustomEvent(
- 'render-start', {bubbles: true, composed: true}));
- this._cancelableRenderPromise = util.makeCancelable(
- this.$.processor.process(this.diff.content, isBinary)
- .then(() => {
- if (this.isImageDiff) {
- this._builder.renderDiff();
- }
- this.dispatchEvent(new CustomEvent('render-content',
- {bubbles: true, composed: true}));
- }));
- return this._cancelableRenderPromise
- .finally(() => { this._cancelableRenderPromise = null; })
- // Mocca testing does not like uncaught rejections, so we catch
- // the cancels which are expected and should not throw errors in
- // tests.
- .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
- }
-
- _setupAnnotationLayers() {
- const layers = [
- this._createTrailingWhitespaceLayer(),
- this._createIntralineLayer(),
- this._createTabIndicatorLayer(),
- this.$.rangeLayer,
- this.$.coverageLayerLeft,
- this.$.coverageLayerRight,
- ];
-
- if (this.layers) {
- layers.push(...this.layers);
- }
- this._layers = layers;
- }
-
- getLineElByChild(node) {
- while (node) {
- if (node instanceof Element) {
- if (node.classList.contains('lineNum')) {
- return node;
- }
- if (node.classList.contains('section')) {
- return null;
- }
+ getLineElByChild(node) {
+ while (node) {
+ if (node instanceof Element) {
+ if (node.classList.contains('lineNum')) {
+ return node;
}
- node = node.previousSibling || node.parentElement;
- }
- return null;
- }
-
- getLineNumberByChild(node) {
- const lineEl = this.getLineElByChild(node);
- return lineEl ?
- parseInt(lineEl.getAttribute('data-value'), 10) :
- null;
- }
-
- getContentByLine(lineNumber, opt_side, opt_root) {
- return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
- }
-
- getContentByLineEl(lineEl) {
- const root = Polymer.dom(lineEl.parentElement);
- const side = this.getSideByLineEl(lineEl);
- const line = lineEl.getAttribute('data-value');
- return this.getContentByLine(line, side, root);
- }
-
- getLineElByNumber(lineNumber, opt_side) {
- const sideSelector = opt_side ? ('.' + opt_side) : '';
- return this.diffElement.querySelector(
- '.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
- }
-
- getContentsByLineRange(startLine, endLine, opt_side) {
- const result = [];
- this._builder.findLinesByRange(startLine, endLine, opt_side, null,
- result);
- return result;
- }
-
- getSideByLineEl(lineEl) {
- return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
- GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
- }
-
- emitGroup(group, sectionEl) {
- this._builder.emitGroup(group, sectionEl);
- }
-
- showContext(newGroups, sectionEl) {
- const groups = this._builder.groups;
-
- const contextIndex = groups.findIndex(group =>
- group.element === sectionEl
- );
- groups.splice(contextIndex, 1, ...newGroups);
-
- for (const newGroup of newGroups) {
- this._builder.emitGroup(newGroup, sectionEl);
- }
- sectionEl.parentNode.removeChild(sectionEl);
-
- this.async(() => this.fire('render-content'), 1);
- }
-
- cancel() {
- this.$.processor.cancel();
- if (this._cancelableRenderPromise) {
- this._cancelableRenderPromise.cancel();
- this._cancelableRenderPromise = null;
- }
- }
-
- _handlePreferenceError(pref) {
- const message = `The value of the '${pref}' user preference is ` +
- `invalid. Fix in diff preferences`;
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {
- message,
- }, bubbles: true, composed: true}));
- throw Error(`Invalid preference value: ${pref}`);
- }
-
- _getDiffBuilder(diff, prefs) {
- if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
- this._handlePreferenceError('tab size');
- return;
- }
-
- if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
- this._handlePreferenceError('diff width');
- return;
- }
-
- const localPrefs = Object.assign({}, prefs);
- if (this.path === COMMIT_MSG_PATH) {
- // override line_length for commit msg the same way as
- // in gr-diff
- localPrefs.line_length = COMMIT_MSG_LINE_LENGTH;
- }
-
- let builder = null;
- if (this.isImageDiff) {
- builder = new GrDiffBuilderImage(
- diff,
- localPrefs,
- this.diffElement,
- this.baseImage,
- this.revisionImage);
- } else if (diff.binary) {
- // If the diff is binary, but not an image.
- return new GrDiffBuilderBinary(
- diff,
- localPrefs,
- this.diffElement);
- } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
- builder = new GrDiffBuilderSideBySide(
- diff,
- localPrefs,
- this.diffElement,
- this._layers
- );
- } else if (this.viewMode === DiffViewMode.UNIFIED) {
- builder = new GrDiffBuilderUnified(
- diff,
- localPrefs,
- this.diffElement,
- this._layers);
- }
- if (!builder) {
- throw Error('Unsupported diff view mode: ' + this.viewMode);
- }
- return builder;
- }
-
- _clearDiffContent() {
- this.diffElement.innerHTML = null;
- }
-
- _groupsChanged(changeRecord) {
- if (!changeRecord) { return; }
- for (const splice of changeRecord.indexSplices) {
- let group;
- for (let i = 0; i < splice.addedCount; i++) {
- group = splice.object[splice.index + i];
- this._builder.groups.push(group);
- this._builder.emitGroup(group);
+ if (node.classList.contains('section')) {
+ return null;
}
}
+ node = node.previousSibling || node.parentElement;
}
+ return null;
+ }
- _createIntralineLayer() {
- return {
- // Take a DIV.contentText element and a line object with intraline
- // differences to highlight and apply them to the element as
- // annotations.
- annotate(contentEl, lineNumberEl, line) {
- const HL_CLASS = 'style-scope gr-diff intraline';
- for (const highlight of line.highlights) {
- // The start and end indices could be the same if a highlight is
- // meant to start at the end of a line and continue onto the
- // next one. Ignore it.
- if (highlight.startIndex === highlight.endIndex) { continue; }
+ getLineNumberByChild(node) {
+ const lineEl = this.getLineElByChild(node);
+ return lineEl ?
+ parseInt(lineEl.getAttribute('data-value'), 10) :
+ null;
+ }
- // If endIndex isn't present, continue to the end of the line.
- const endIndex = highlight.endIndex === undefined ?
- line.text.length :
- highlight.endIndex;
+ getContentByLine(lineNumber, opt_side, opt_root) {
+ return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
+ }
- GrAnnotation.annotateElement(
- contentEl,
- highlight.startIndex,
- endIndex - highlight.startIndex,
- HL_CLASS);
- }
- },
- };
+ getContentByLineEl(lineEl) {
+ const root = dom(lineEl.parentElement);
+ const side = this.getSideByLineEl(lineEl);
+ const line = lineEl.getAttribute('data-value');
+ return this.getContentByLine(line, side, root);
+ }
+
+ getLineElByNumber(lineNumber, opt_side) {
+ const sideSelector = opt_side ? ('.' + opt_side) : '';
+ return this.diffElement.querySelector(
+ '.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
+ }
+
+ getContentsByLineRange(startLine, endLine, opt_side) {
+ const result = [];
+ this._builder.findLinesByRange(startLine, endLine, opt_side, null,
+ result);
+ return result;
+ }
+
+ getSideByLineEl(lineEl) {
+ return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
+ GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
+ }
+
+ emitGroup(group, sectionEl) {
+ this._builder.emitGroup(group, sectionEl);
+ }
+
+ showContext(newGroups, sectionEl) {
+ const groups = this._builder.groups;
+
+ const contextIndex = groups.findIndex(group =>
+ group.element === sectionEl
+ );
+ groups.splice(contextIndex, 1, ...newGroups);
+
+ for (const newGroup of newGroups) {
+ this._builder.emitGroup(newGroup, sectionEl);
}
+ sectionEl.parentNode.removeChild(sectionEl);
- _createTabIndicatorLayer() {
- const show = () => this._showTabs;
- return {
- annotate(contentEl, lineNumberEl, line) {
- // If visible tabs are disabled, do nothing.
- if (!show()) { return; }
+ this.async(() => this.fire('render-content'), 1);
+ }
- // Find and annotate the locations of tabs.
- const split = line.text.split('\t');
- if (!split) { return; }
- for (let i = 0, pos = 0; i < split.length - 1; i++) {
- // Skip forward by the length of the content
- pos += split[i].length;
-
- GrAnnotation.annotateElement(contentEl, pos, 1,
- 'style-scope gr-diff tab-indicator');
-
- // Skip forward by one tab character.
- pos++;
- }
- },
- };
- }
-
- _createTrailingWhitespaceLayer() {
- const show = function() {
- return this._showTrailingWhitespace;
- }.bind(this);
-
- return {
- annotate(contentEl, lineNumberEl, line) {
- if (!show()) { return; }
-
- const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
- if (match) {
- // Normalize string positions in case there is unicode before or
- // within the match.
- const index = GrAnnotation.getStringLength(
- line.text.substr(0, match.index));
- const length = GrAnnotation.getStringLength(match[0]);
- GrAnnotation.annotateElement(contentEl, index, length,
- 'style-scope gr-diff trailing-whitespace');
- }
- },
- };
- }
-
- setBlame(blame) {
- if (!this._builder || !blame) { return; }
- this._builder.setBlame(blame);
+ cancel() {
+ this.$.processor.cancel();
+ if (this._cancelableRenderPromise) {
+ this._cancelableRenderPromise.cancel();
+ this._cancelableRenderPromise = null;
}
}
- customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);
-})();
+ _handlePreferenceError(pref) {
+ const message = `The value of the '${pref}' user preference is ` +
+ `invalid. Fix in diff preferences`;
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {
+ message,
+ }, bubbles: true, composed: true}));
+ throw Error(`Invalid preference value: ${pref}`);
+ }
+
+ _getDiffBuilder(diff, prefs) {
+ if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
+ this._handlePreferenceError('tab size');
+ return;
+ }
+
+ if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
+ this._handlePreferenceError('diff width');
+ return;
+ }
+
+ const localPrefs = Object.assign({}, prefs);
+ if (this.path === COMMIT_MSG_PATH) {
+ // override line_length for commit msg the same way as
+ // in gr-diff
+ localPrefs.line_length = COMMIT_MSG_LINE_LENGTH;
+ }
+
+ let builder = null;
+ if (this.isImageDiff) {
+ builder = new GrDiffBuilderImage(
+ diff,
+ localPrefs,
+ this.diffElement,
+ this.baseImage,
+ this.revisionImage);
+ } else if (diff.binary) {
+ // If the diff is binary, but not an image.
+ return new GrDiffBuilderBinary(
+ diff,
+ localPrefs,
+ this.diffElement);
+ } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
+ builder = new GrDiffBuilderSideBySide(
+ diff,
+ localPrefs,
+ this.diffElement,
+ this._layers
+ );
+ } else if (this.viewMode === DiffViewMode.UNIFIED) {
+ builder = new GrDiffBuilderUnified(
+ diff,
+ localPrefs,
+ this.diffElement,
+ this._layers);
+ }
+ if (!builder) {
+ throw Error('Unsupported diff view mode: ' + this.viewMode);
+ }
+ return builder;
+ }
+
+ _clearDiffContent() {
+ this.diffElement.innerHTML = null;
+ }
+
+ _groupsChanged(changeRecord) {
+ if (!changeRecord) { return; }
+ for (const splice of changeRecord.indexSplices) {
+ let group;
+ for (let i = 0; i < splice.addedCount; i++) {
+ group = splice.object[splice.index + i];
+ this._builder.groups.push(group);
+ this._builder.emitGroup(group);
+ }
+ }
+ }
+
+ _createIntralineLayer() {
+ return {
+ // Take a DIV.contentText element and a line object with intraline
+ // differences to highlight and apply them to the element as
+ // annotations.
+ annotate(contentEl, lineNumberEl, line) {
+ const HL_CLASS = 'style-scope gr-diff intraline';
+ for (const highlight of line.highlights) {
+ // The start and end indices could be the same if a highlight is
+ // meant to start at the end of a line and continue onto the
+ // next one. Ignore it.
+ if (highlight.startIndex === highlight.endIndex) { continue; }
+
+ // If endIndex isn't present, continue to the end of the line.
+ const endIndex = highlight.endIndex === undefined ?
+ line.text.length :
+ highlight.endIndex;
+
+ GrAnnotation.annotateElement(
+ contentEl,
+ highlight.startIndex,
+ endIndex - highlight.startIndex,
+ HL_CLASS);
+ }
+ },
+ };
+ }
+
+ _createTabIndicatorLayer() {
+ const show = () => this._showTabs;
+ return {
+ annotate(contentEl, lineNumberEl, line) {
+ // If visible tabs are disabled, do nothing.
+ if (!show()) { return; }
+
+ // Find and annotate the locations of tabs.
+ const split = line.text.split('\t');
+ if (!split) { return; }
+ for (let i = 0, pos = 0; i < split.length - 1; i++) {
+ // Skip forward by the length of the content
+ pos += split[i].length;
+
+ GrAnnotation.annotateElement(contentEl, pos, 1,
+ 'style-scope gr-diff tab-indicator');
+
+ // Skip forward by one tab character.
+ pos++;
+ }
+ },
+ };
+ }
+
+ _createTrailingWhitespaceLayer() {
+ const show = function() {
+ return this._showTrailingWhitespace;
+ }.bind(this);
+
+ return {
+ annotate(contentEl, lineNumberEl, line) {
+ if (!show()) { return; }
+
+ const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
+ if (match) {
+ // Normalize string positions in case there is unicode before or
+ // within the match.
+ const index = GrAnnotation.getStringLength(
+ line.text.substr(0, match.index));
+ const length = GrAnnotation.getStringLength(match[0]);
+ GrAnnotation.annotateElement(contentEl, index, length,
+ 'style-scope gr-diff trailing-whitespace');
+ }
+ },
+ };
+ }
+
+ setBlame(blame) {
+ if (!this._builder || !blame) { return; }
+ this._builder.setBlame(blame);
+ }
+}
+
+customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
index bf8b0dc..c8df78f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
@@ -1,54 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html">
-<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
-<link rel="import" href="../../../elements/shared/gr-hovercard/gr-hovercard.html">
-<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
-<script src="../../../scripts/util.js"></script>
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../gr-diff/gr-diff-group.js"></script>
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-<script src="gr-diff-builder.js"></script>
-<script src="gr-diff-builder-side-by-side.js"></script>
-<script src="gr-diff-builder-unified.js"></script>
-<script src="gr-diff-builder-image.js"></script>
-<script src="gr-diff-builder-binary.js"></script>
-
-<dom-module id="gr-diff-builder">
- <template>
+export const htmlTemplate = html`
<div class="contentWrapper">
<slot></slot>
</div>
- <gr-ranged-comment-layer
- id="rangeLayer"
- comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
- <gr-coverage-layer
- id="coverageLayerLeft"
- coverage-ranges="[[_leftCoverageRanges]]"
- side="left"></gr-coverage-layer>
- <gr-coverage-layer
- id="coverageLayerRight"
- coverage-ranges="[[_rightCoverageRanges]]"
- side="right"></gr-coverage-layer>
- <gr-diff-processor
- id="processor"
- groups="{{_groups}}"></gr-diff-processor>
- </template>
- <script src="gr-diff-builder-element.js"></script>
-</dom-module>
+ <gr-ranged-comment-layer id="rangeLayer" comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
+ <gr-coverage-layer id="coverageLayerLeft" coverage-ranges="[[_leftCoverageRanges]]" side="left"></gr-coverage-layer>
+ <gr-coverage-layer id="coverageLayerRight" coverage-ranges="[[_rightCoverageRanges]]" side="right"></gr-coverage-layer>
+ <gr-diff-processor id="processor" groups="{{_groups}}"></gr-diff-processor>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index 3af5522..07edc7d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -19,23 +19,35 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-builder</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="../../../scripts/util.js"></script>
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../gr-diff/gr-diff-group.js"></script>
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-<script src="gr-diff-builder.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 type="module" src="../../../scripts/util.js"></script>
+<script type="module" src="../gr-diff/gr-diff-line.js"></script>
+<script type="module" src="../gr-diff/gr-diff-group.js"></script>
+<script type="module" src="../gr-diff-highlight/gr-annotation.js"></script>
+<script type="module" src="./gr-diff-builder.js"></script>
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-diff-builder-element.html">
+<script type="module" src="../../shared/gr-rest-api-interface/gr-rest-api-interface.js"></script>
+<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script>
+<script type="module" src="./gr-diff-builder-element.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-diff-builder-element.js';
+void(0);
+</script>
<test-fixture id="basic">
<template is="dom-template">
@@ -59,985 +71,1024 @@
</template>
</test-fixture>
-<script>
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-diff-builder-element.js';
+import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- suite('gr-diff-builder tests', async () => {
- await readyToTest();
- let prefs;
- let element;
- let builder;
- let sandbox;
- const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
+suite('gr-diff-builder tests', () => {
+ let prefs;
+ let element;
+ let builder;
+ let sandbox;
+ const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- getProjectConfig() { return Promise.resolve({}); },
- });
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- builder = new GrDiffBuilder({content: []}, prefs);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getProjectConfig() { return Promise.resolve({}); },
});
+ prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ builder = new GrDiffBuilder({content: []}, prefs);
+ });
- teardown(() => { sandbox.restore(); });
+ teardown(() => { sandbox.restore(); });
- test('_createElement classStr applies all classes', () => {
- const node = builder._createElement('div', 'test classes');
- assert.isTrue(node.classList.contains('gr-diff'));
- assert.isTrue(node.classList.contains('test'));
- assert.isTrue(node.classList.contains('classes'));
- });
+ test('_createElement classStr applies all classes', () => {
+ const node = builder._createElement('div', 'test classes');
+ assert.isTrue(node.classList.contains('gr-diff'));
+ assert.isTrue(node.classList.contains('test'));
+ assert.isTrue(node.classList.contains('classes'));
+ });
- test('context control buttons', () => {
- // Create 10 lines.
- const lines = [];
- for (let i = 0; i < 10; i++) {
- const line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.beforeNumber = i + 1;
- line.afterNumber = i + 1;
- line.text = 'lorem upsum';
- lines.push(line);
- }
-
- const contextLine = {
- contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
- };
-
- const section = {};
- // Does not include +10 buttons when there are fewer than 11 lines.
- let td = builder._createContextControl(section, contextLine);
- let buttons = td.querySelectorAll('gr-button.showContext');
-
- assert.equal(buttons.length, 1);
- assert.equal(Polymer.dom(buttons[0]).textContent, 'Show 10 common lines');
-
- // Add another line.
+ test('context control buttons', () => {
+ // Create 10 lines.
+ const lines = [];
+ for (let i = 0; i < 10; i++) {
const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = i + 1;
+ line.afterNumber = i + 1;
line.text = 'lorem upsum';
- line.beforeNumber = 11;
- line.afterNumber = 11;
- contextLine.contextGroups[0].addLine(line);
+ lines.push(line);
+ }
- // Includes +10 buttons when there are at least 11 lines.
- td = builder._createContextControl(section, contextLine);
- buttons = td.querySelectorAll('gr-button.showContext');
+ const contextLine = {
+ contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
+ };
- assert.equal(buttons.length, 3);
- assert.equal(Polymer.dom(buttons[0]).textContent, '+10 above');
- assert.equal(Polymer.dom(buttons[1]).textContent, 'Show 11 common lines');
- assert.equal(Polymer.dom(buttons[2]).textContent, '+10 below');
- });
+ const section = {};
+ // Does not include +10 buttons when there are fewer than 11 lines.
+ let td = builder._createContextControl(section, contextLine);
+ let buttons = td.querySelectorAll('gr-button.showContext');
- test('newlines 1', () => {
- let text = 'abcdef';
+ assert.equal(buttons.length, 1);
+ assert.equal(dom(buttons[0]).textContent, 'Show 10 common lines');
- assert.equal(builder._formatText(text, 4, 10).innerHTML, text);
- text = 'a'.repeat(20);
- assert.equal(builder._formatText(text, 4, 10).innerHTML,
- 'a'.repeat(10) +
- LINE_FEED_HTML +
- 'a'.repeat(10));
- });
+ // Add another line.
+ const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.text = 'lorem upsum';
+ line.beforeNumber = 11;
+ line.afterNumber = 11;
+ contextLine.contextGroups[0].addLine(line);
- test('newlines 2', () => {
- const text = '<span class="thumbsup">👍</span>';
- assert.equal(builder._formatText(text, 4, 10).innerHTML,
- '<span clas' +
- LINE_FEED_HTML +
- 's="thumbsu' +
- LINE_FEED_HTML +
- 'p">👍</span' +
- LINE_FEED_HTML +
- '>');
- });
+ // Includes +10 buttons when there are at least 11 lines.
+ td = builder._createContextControl(section, contextLine);
+ buttons = td.querySelectorAll('gr-button.showContext');
- test('newlines 3', () => {
- const text = '01234\t56789';
- assert.equal(builder._formatText(text, 4, 10).innerHTML,
- '01234' + builder._getTabWrapper(3).outerHTML + '56' +
- LINE_FEED_HTML +
- '789');
- });
+ assert.equal(buttons.length, 3);
+ assert.equal(dom(buttons[0]).textContent, '+10 above');
+ assert.equal(dom(buttons[1]).textContent, 'Show 11 common lines');
+ assert.equal(dom(buttons[2]).textContent, '+10 below');
+ });
- test('newlines 4', () => {
- const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍';
- assert.equal(builder._formatText(text, 4, 20).innerHTML,
- '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
- LINE_FEED_HTML +
- '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
- LINE_FEED_HTML +
- '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍');
- });
+ test('newlines 1', () => {
+ let text = 'abcdef';
- test('line_length ignored if line_wrapping is true', () => {
- builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
+ assert.equal(builder._formatText(text, 4, 10).innerHTML, text);
+ text = 'a'.repeat(20);
+ assert.equal(builder._formatText(text, 4, 10).innerHTML,
+ 'a'.repeat(10) +
+ LINE_FEED_HTML +
+ 'a'.repeat(10));
+ });
- const line = {text, highlights: []};
- const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, text);
- });
+ test('newlines 2', () => {
+ const text = '<span class="thumbsup">👍</span>';
+ assert.equal(builder._formatText(text, 4, 10).innerHTML,
+ '<span clas' +
+ LINE_FEED_HTML +
+ 's="thumbsu' +
+ LINE_FEED_HTML +
+ 'p">👍</span' +
+ LINE_FEED_HTML +
+ '>');
+ });
- test('line_length applied if line_wrapping is false', () => {
- builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
+ test('newlines 3', () => {
+ const text = '01234\t56789';
+ assert.equal(builder._formatText(text, 4, 10).innerHTML,
+ '01234' + builder._getTabWrapper(3).outerHTML + '56' +
+ LINE_FEED_HTML +
+ '789');
+ });
- const line = {text, highlights: []};
- const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a';
- const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, expected);
- });
+ test('newlines 4', () => {
+ const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍';
+ assert.equal(builder._formatText(text, 4, 20).innerHTML,
+ '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
+ LINE_FEED_HTML +
+ '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
+ LINE_FEED_HTML +
+ '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍');
+ });
- [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE]
- .forEach(mode => {
- test(`line_length used for regular files under ${mode}`, () => {
- element.path = '/a.txt';
- element.viewMode = mode;
- builder = element._getDiffBuilder(
- {}, {tab_size: 4, line_length: 50}
- );
- assert.equal(builder._prefs.line_length, 50);
- });
+ test('line_length ignored if line_wrapping is true', () => {
+ builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
+ const text = 'a'.repeat(51);
- test(`line_length ignored for commit msg under ${mode}`, () => {
- element.path = '/COMMIT_MSG';
- element.viewMode = mode;
- builder = element._getDiffBuilder(
- {}, {tab_size: 4, line_length: 50}
- );
- assert.equal(builder._prefs.line_length, 72);
- });
+ const line = {text, highlights: []};
+ const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
+ assert.equal(result, text);
+ });
+
+ test('line_length applied if line_wrapping is false', () => {
+ builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
+ const text = 'a'.repeat(51);
+
+ const line = {text, highlights: []};
+ const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a';
+ const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
+ assert.equal(result, expected);
+ });
+
+ [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE]
+ .forEach(mode => {
+ test(`line_length used for regular files under ${mode}`, () => {
+ element.path = '/a.txt';
+ element.viewMode = mode;
+ builder = element._getDiffBuilder(
+ {}, {tab_size: 4, line_length: 50}
+ );
+ assert.equal(builder._prefs.line_length, 50);
});
- test('_createTextEl linewrap with tabs', () => {
- const text = '\t'.repeat(7) + '!';
- const line = {text, highlights: []};
- const el = builder._createTextEl(undefined, line);
- assert.equal(el.innerText, text);
- // With line length 10 and tab size 2, there should be a line break
- // after every two tabs.
- const newlineEl = el.querySelector('.contentText > .br');
- assert.isOk(newlineEl);
- assert.equal(
- el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
- newlineEl);
- });
+ test(`line_length ignored for commit msg under ${mode}`, () => {
+ element.path = '/COMMIT_MSG';
+ element.viewMode = mode;
+ builder = element._getDiffBuilder(
+ {}, {tab_size: 4, line_length: 50}
+ );
+ assert.equal(builder._prefs.line_length, 72);
+ });
+ });
- test('text length with tabs and unicode', () => {
- function expectTextLength(text, tabSize, expected) {
- // Formatting to |expected| columns should not introduce line breaks.
- const result = builder._formatText(text, tabSize, expected);
- assert.isNotOk(result.querySelector('.contentText > .br'),
+ test('_createTextEl linewrap with tabs', () => {
+ const text = '\t'.repeat(7) + '!';
+ const line = {text, highlights: []};
+ const el = builder._createTextEl(undefined, line);
+ assert.equal(el.innerText, text);
+ // With line length 10 and tab size 2, there should be a line break
+ // after every two tabs.
+ const newlineEl = el.querySelector('.contentText > .br');
+ assert.isOk(newlineEl);
+ assert.equal(
+ el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
+ newlineEl);
+ });
+
+ test('text length with tabs and unicode', () => {
+ function expectTextLength(text, tabSize, expected) {
+ // Formatting to |expected| columns should not introduce line breaks.
+ const result = builder._formatText(text, tabSize, expected);
+ assert.isNotOk(result.querySelector('.contentText > .br'),
+ ` Expected the result of: \n` +
+ ` _formatText(${text}', ${tabSize}, ${expected})\n` +
+ ` to not contain a br. But the actual result HTML was:\n` +
+ ` '${result.innerHTML}'\nwhereupon`);
+
+ // Increasing the line limit should produce the same markup.
+ assert.equal(builder._formatText(text, tabSize, Infinity).innerHTML,
+ result.innerHTML);
+ assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML,
+ result.innerHTML);
+
+ // Decreasing the line limit should introduce line breaks.
+ if (expected > 0) {
+ const tooSmall = builder._formatText(text, tabSize, expected - 1);
+ assert.isOk(tooSmall.querySelector('.contentText > .br'),
` Expected the result of: \n` +
- ` _formatText(${text}', ${tabSize}, ${expected})\n` +
- ` to not contain a br. But the actual result HTML was:\n` +
- ` '${result.innerHTML}'\nwhereupon`);
-
- // Increasing the line limit should produce the same markup.
- assert.equal(builder._formatText(text, tabSize, Infinity).innerHTML,
- result.innerHTML);
- assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML,
- result.innerHTML);
-
- // Decreasing the line limit should introduce line breaks.
- if (expected > 0) {
- const tooSmall = builder._formatText(text, tabSize, expected - 1);
- assert.isOk(tooSmall.querySelector('.contentText > .br'),
- ` Expected the result of: \n` +
- ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
- ` to contain a br. But the actual result HTML was:\n` +
- ` '${tooSmall.innerHTML}'\nwhereupon`);
- }
+ ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
+ ` to contain a br. But the actual result HTML was:\n` +
+ ` '${tooSmall.innerHTML}'\nwhereupon`);
}
- expectTextLength('12345', 4, 5);
- expectTextLength('\t\t12', 4, 10);
- expectTextLength('abc💢123', 4, 7);
- expectTextLength('abc\t', 8, 8);
- expectTextLength('abc\t\t', 10, 20);
- expectTextLength('', 10, 0);
- expectTextLength('', 10, 0);
- // 17 Thai combining chars.
- expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
- expectTextLength('abc\tde', 10, 12);
- expectTextLength('abc\tde\t', 10, 20);
- expectTextLength('\t\t\t\t\t', 20, 100);
- });
+ }
+ expectTextLength('12345', 4, 5);
+ expectTextLength('\t\t12', 4, 10);
+ expectTextLength('abc💢123', 4, 7);
+ expectTextLength('abc\t', 8, 8);
+ expectTextLength('abc\t\t', 10, 20);
+ expectTextLength('', 10, 0);
+ expectTextLength('', 10, 0);
+ // 17 Thai combining chars.
+ expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
+ expectTextLength('abc\tde', 10, 12);
+ expectTextLength('abc\tde\t', 10, 20);
+ expectTextLength('\t\t\t\t\t', 20, 100);
+ });
- test('tab wrapper insertion', () => {
- const html = 'abc\tdef';
- const tabSize = builder._prefs.tab_size;
- const wrapper = builder._getTabWrapper(tabSize - 3);
- assert.ok(wrapper);
- assert.equal(wrapper.innerText, '\t');
- assert.equal(
- builder._formatText(html, tabSize, Infinity).innerHTML,
- 'abc' + wrapper.outerHTML + 'def');
- });
+ test('tab wrapper insertion', () => {
+ const html = 'abc\tdef';
+ const tabSize = builder._prefs.tab_size;
+ const wrapper = builder._getTabWrapper(tabSize - 3);
+ assert.ok(wrapper);
+ assert.equal(wrapper.innerText, '\t');
+ assert.equal(
+ builder._formatText(html, tabSize, Infinity).innerHTML,
+ 'abc' + wrapper.outerHTML + 'def');
+ });
- test('tab wrapper style', () => {
- const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' +
- 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$');
+ test('tab wrapper style', () => {
+ const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' +
+ 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$');
- for (const size of [1, 3, 8, 55]) {
- const html = builder._getTabWrapper(size).outerHTML;
- expect(html).to.match(pattern);
- assert.equal(html.match(pattern)[1], size);
+ for (const size of [1, 3, 8, 55]) {
+ const html = builder._getTabWrapper(size).outerHTML;
+ expect(html).to.match(pattern);
+ assert.equal(html.match(pattern)[1], size);
+ }
+ });
+
+ test('_handlePreferenceError called with invalid preference', () => {
+ sandbox.stub(element, '_handlePreferenceError');
+ const prefs = {tab_size: 0};
+ element._getDiffBuilder(element.diff, prefs);
+ assert.isTrue(element._handlePreferenceError.lastCall
+ .calledWithExactly('tab size'));
+ });
+
+ test('_handlePreferenceError triggers alert and javascript error', () => {
+ const errorStub = sinon.stub();
+ element.addEventListener('show-alert', errorStub);
+ assert.throws(element._handlePreferenceError.bind(element, 'tab size'));
+ assert.equal(errorStub.lastCall.args[0].detail.message,
+ `The value of the 'tab size' user preference is invalid. ` +
+ `Fix in diff preferences`);
+ });
+
+ suite('_isTotal', () => {
+ test('is total for add', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ for (let idx = 0; idx < 10; idx++) {
+ group.addLine(new GrDiffLine(GrDiffLine.Type.ADD));
}
+ assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
- test('_handlePreferenceError called with invalid preference', () => {
- sandbox.stub(element, '_handlePreferenceError');
- const prefs = {tab_size: 0};
- element._getDiffBuilder(element.diff, prefs);
- assert.isTrue(element._handlePreferenceError.lastCall
- .calledWithExactly('tab size'));
- });
-
- test('_handlePreferenceError triggers alert and javascript error', () => {
- const errorStub = sinon.stub();
- element.addEventListener('show-alert', errorStub);
- assert.throws(element._handlePreferenceError.bind(element, 'tab size'));
- assert.equal(errorStub.lastCall.args[0].detail.message,
- `The value of the 'tab size' user preference is invalid. ` +
- `Fix in diff preferences`);
- });
-
- suite('_isTotal', () => {
- test('is total for add', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLine.Type.ADD));
- }
- assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
- });
-
- test('is total for remove', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE));
- }
- assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
- });
-
- test('not total for empty', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
- assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
- });
-
- test('not total for non-delta', () => {
- const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH));
- }
- assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
- });
- });
-
- suite('intraline differences', () => {
- let el;
- let str;
- let annotateElementSpy;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- function slice(str, start, end) {
- return Array.from(str).slice(start, end)
- .join('');
+ test('is total for remove', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ for (let idx = 0; idx < 10; idx++) {
+ group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE));
}
-
- setup(() => {
- el = fixture('div-with-text');
- str = el.textContent;
- annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- layer = document.createElement('gr-diff-builder')
- ._createIntralineLayer();
- });
-
- test('annotate no highlights', () => {
- const line = {
- text: str,
- highlights: [],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- // The content is unchanged.
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(str, el.childNodes[0].textContent);
- });
-
- test('annotate with highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- {startIndex: 18, endIndex: 22},
- ],
- };
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12, 18);
- const str3 = slice(str, 18, 22);
- const str4 = slice(str, 22);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 5);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
-
- assert.notInstanceOf(el.childNodes[3], Text);
- assert.equal(el.childNodes[3].textContent, str3);
-
- assert.instanceOf(el.childNodes[4], Text);
- assert.equal(el.childNodes[4].textContent, str4);
- });
-
- test('annotate without endIndex', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28},
- ],
- };
-
- const str0 = slice(str, 0, 28);
- const str1 = slice(str, 28);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
-
- test('annotate ignores empty highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28, endIndex: 28},
- ],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- });
-
- test('annotate handles unicode', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 3);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
- });
-
- test('annotate handles unicode w/o endIndex', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
-
- const line = {
- text: str,
- highlights: [
- {startIndex: 6},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
+ assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
- suite('tab indicators', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = fixture('basic');
- element._showTabs = true;
- layer = element._createTabIndicatorLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no tabs', () => {
- const str = 'lorem ipsum no tabs';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates tab at beginning', () => {
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('does not annotate when disabled', () => {
- element._showTabs = false;
-
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates multiple in beginning', () => {
- const str = '\t\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 2);
-
- let args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
-
- args = annotateElementStub.getCalls()[1].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 1, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('annotates intermediate tabs', () => {
- const str = 'lorem\tupsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 5, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
+ test('not total for empty', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
+ assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
});
- suite('layers', () => {
- let element;
- let initialLayersCount;
- let withLayerCount;
+ test('not total for non-delta', () => {
+ const group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ for (let idx = 0; idx < 10; idx++) {
+ group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH));
+ }
+ assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
+ });
+ });
+
+ suite('intraline differences', () => {
+ let el;
+ let str;
+ let annotateElementSpy;
+ let layer;
+ const lineNumberEl = document.createElement('td');
+
+ function slice(str, start, end) {
+ return Array.from(str).slice(start, end)
+ .join('');
+ }
+
+ setup(() => {
+ el = fixture('div-with-text');
+ str = el.textContent;
+ annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ layer = document.createElement('gr-diff-builder')
+ ._createIntralineLayer();
+ });
+
+ test('annotate no highlights', () => {
+ const line = {
+ text: str,
+ highlights: [],
+ };
+
+ layer.annotate(el, lineNumberEl, line);
+
+ // The content is unchanged.
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(str, el.childNodes[0].textContent);
+ });
+
+ test('annotate with highlights', () => {
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 6, endIndex: 12},
+ {startIndex: 18, endIndex: 22},
+ ],
+ };
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12, 18);
+ const str3 = slice(str, 18, 22);
+ const str4 = slice(str, 22);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 5);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+
+ assert.notInstanceOf(el.childNodes[3], Text);
+ assert.equal(el.childNodes[3].textContent, str3);
+
+ assert.instanceOf(el.childNodes[4], Text);
+ assert.equal(el.childNodes[4].textContent, str4);
+ });
+
+ test('annotate without endIndex', () => {
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 28},
+ ],
+ };
+
+ const str0 = slice(str, 0, 28);
+ const str1 = slice(str, 28);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+
+ test('annotate ignores empty highlights', () => {
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 28, endIndex: 28},
+ ],
+ };
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ });
+
+ test('annotate handles unicode', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 6, endIndex: 12},
+ ],
+ };
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 3);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+ });
+
+ test('annotate handles unicode w/o endIndex', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+
+ const line = {
+ text: str,
+ highlights: [
+ {startIndex: 6},
+ ],
+ };
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6);
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+ });
+
+ suite('tab indicators', () => {
+ let element;
+ let layer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element = fixture('basic');
+ element._showTabs = true;
+ layer = element._createTabIndicatorLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const line = {text: ''};
+ const el = document.createElement('div');
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no tabs', () => {
+ const str = 'lorem ipsum no tabs';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates tab at beginning', () => {
+ const str = '\tlorem upsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('does not annotate when disabled', () => {
+ element._showTabs = false;
+
+ const str = '\tlorem upsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates multiple in beginning', () => {
+ const str = '\t\tlorem upsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.equal(annotateElementStub.callCount, 2);
+
+ let args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+
+ args = annotateElementStub.getCalls()[1].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 1, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('annotates intermediate tabs', () => {
+ const str = 'lorem\tupsum';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, line);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 5, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+ });
+
+ suite('layers', () => {
+ let element;
+ let initialLayersCount;
+ let withLayerCount;
+ setup(() => {
+ const layers = [];
+ element = fixture('basic');
+ element.layers = layers;
+ element._showTrailingWhitespace = true;
+ element._setupAnnotationLayers();
+ initialLayersCount = element._layers.length;
+ });
+
+ test('no layers', () => {
+ element._setupAnnotationLayers();
+ assert.equal(element._layers.length, initialLayersCount);
+ });
+
+ suite('with layers', () => {
+ const layers = [{}, {}];
setup(() => {
- const layers = [];
element = fixture('basic');
element.layers = layers;
element._showTrailingWhitespace = true;
element._setupAnnotationLayers();
- initialLayersCount = element._layers.length;
+ withLayerCount = element._layers.length;
});
-
- test('no layers', () => {
+ test('with layers', () => {
element._setupAnnotationLayers();
- assert.equal(element._layers.length, initialLayersCount);
+ assert.equal(element._layers.length, withLayerCount);
+ assert.equal(initialLayersCount + layers.length,
+ withLayerCount);
});
+ });
+ });
- suite('with layers', () => {
- const layers = [{}, {}];
- setup(() => {
- element = fixture('basic');
- element.layers = layers;
- element._showTrailingWhitespace = true;
- element._setupAnnotationLayers();
- withLayerCount = element._layers.length;
- });
- test('with layers', () => {
- element._setupAnnotationLayers();
- assert.equal(element._layers.length, withLayerCount);
- assert.equal(initialLayersCount + layers.length,
- withLayerCount);
- });
+ suite('trailing whitespace', () => {
+ let element;
+ let layer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element = fixture('basic');
+ element._showTrailingWhitespace = true;
+ layer = element._createTrailingWhitespaceLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const line = {text: ''};
+ const el = document.createElement('div');
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no trailing whitespace', () => {
+ const str = 'lorem ipsum blah blah';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates trailing spaces', () => {
+ const str = 'lorem ipsum ';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates trailing tabs', () => {
+ const str = 'lorem ipsum\t\t\t';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates mixed trailing whitespace', () => {
+ const str = 'lorem ipsum\t \t';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('unicode preceding trailing whitespace', () => {
+ const str = '💢\t';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 1);
+ assert.equal(annotateElementStub.lastCall.args[2], 1);
+ });
+
+ test('does not annotate when disabled', () => {
+ element._showTrailingWhitespace = false;
+ const str = 'lorem upsum\t \t ';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub =
+ sandbox.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, line);
+ assert.isFalse(annotateElementStub.called);
+ });
+ });
+
+ suite('rendering text, images and binary files', () => {
+ let processStub;
+ let keyLocations;
+ let prefs;
+ let content;
+
+ setup(() => {
+ element = fixture('basic');
+ element.viewMode = 'SIDE_BY_SIDE';
+ processStub = sandbox.stub(element.$.processor, 'process')
+ .returns(Promise.resolve());
+ keyLocations = {left: {}, right: {}};
+ prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ content = [{
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ }, {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ }];
+ });
+
+ test('text', () => {
+ element.diff = {content};
+ return element.render(keyLocations, prefs).then(() => {
+ assert.isTrue(processStub.calledOnce);
+ assert.isFalse(processStub.lastCall.args[1]);
});
});
- suite('trailing whitespace', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = fixture('basic');
- element._showTrailingWhitespace = true;
- layer = element._createTrailingWhitespaceLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no trailing whitespace', () => {
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates trailing spaces', () => {
- const str = 'lorem ipsum ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates trailing tabs', () => {
- const str = 'lorem ipsum\t\t\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates mixed trailing whitespace', () => {
- const str = 'lorem ipsum\t \t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('unicode preceding trailing whitespace', () => {
- const str = '💢\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 1);
- assert.equal(annotateElementStub.lastCall.args[2], 1);
- });
-
- test('does not annotate when disabled', () => {
- element._showTrailingWhitespace = false;
- const str = 'lorem upsum\t \t ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sandbox.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
+ test('image', () => {
+ element.diff = {content, binary: true};
+ element.isImageDiff = true;
+ return element.render(keyLocations, prefs).then(() => {
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
});
});
- suite('rendering text, images and binary files', () => {
- let processStub;
- let keyLocations;
- let prefs;
- let content;
+ test('binary', () => {
+ element.diff = {content, binary: true};
+ return element.render(keyLocations, prefs).then(() => {
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+ });
+ });
- setup(() => {
- element = fixture('basic');
- element.viewMode = 'SIDE_BY_SIDE';
- processStub = sandbox.stub(element.$.processor, 'process')
- .returns(Promise.resolve());
- keyLocations = {left: {}, right: {}};
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
- content = [{
+ suite('rendering', () => {
+ let content;
+ let outputEl;
+ let keyLocations;
+
+ setup(done => {
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ content = [
+ {
a: ['all work and no play make andybons a dull boy'],
b: ['elgoog elgoog elgoog'],
- }, {
+ },
+ {
ab: [
'Non eram nescius, Brute, cum, quae summis ingeniis ',
'exquisitaque doctrina philosophi Graeco sermone tractavissent',
],
- }];
+ },
+ ];
+ element = fixture('basic');
+ outputEl = element.queryEffectiveChildren('#diffTable');
+ keyLocations = {left: {}, right: {}};
+ sandbox.stub(element, '_getDiffBuilder', () => {
+ const builder = new GrDiffBuilder({content}, prefs, outputEl);
+ sandbox.stub(builder, 'addColumns');
+ builder.buildSectionElement = function(group) {
+ const section = document.createElement('stub');
+ section.textContent = group.lines
+ .reduce((acc, line) => acc + line.text, '');
+ return section;
+ };
+ return builder;
});
+ element.diff = {content};
+ element.render(keyLocations, prefs).then(done);
+ });
- test('text', () => {
- element.diff = {content};
- return element.render(keyLocations, prefs).then(() => {
- assert.isTrue(processStub.calledOnce);
- assert.isFalse(processStub.lastCall.args[1]);
- });
- });
+ test('addColumns is called', done => {
+ element.render(keyLocations, {}).then(done);
+ assert.isTrue(element._builder.addColumns.called);
+ });
- test('image', () => {
- element.diff = {content, binary: true};
- element.isImageDiff = true;
- return element.render(keyLocations, prefs).then(() => {
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
- });
+ test('getSectionsByLineRange one line', () => {
+ const section = outputEl.querySelector('stub:nth-of-type(2)');
+ const sections = element._builder.getSectionsByLineRange(1, 1, 'left');
+ assert.equal(sections.length, 1);
+ assert.strictEqual(sections[0], section);
+ });
- test('binary', () => {
- element.diff = {content, binary: true};
- return element.render(keyLocations, prefs).then(() => {
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
+ test('getSectionsByLineRange over diff', () => {
+ const section = [
+ outputEl.querySelector('stub:nth-of-type(2)'),
+ outputEl.querySelector('stub:nth-of-type(3)'),
+ ];
+ const sections = element._builder.getSectionsByLineRange(1, 2, 'left');
+ assert.equal(sections.length, 2);
+ assert.strictEqual(sections[0], section[0]);
+ assert.strictEqual(sections[1], section[1]);
+ });
+
+ test('render-start and render-content are fired', done => {
+ const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
+ element.render(keyLocations, {}).then(() => {
+ const firedEventTypes = dispatchEventStub.getCalls()
+ .map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-start');
+ assert.include(firedEventTypes, 'render-content');
+ done();
});
});
- suite('rendering', () => {
- let content;
- let outputEl;
- let keyLocations;
+ test('cancel', () => {
+ const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
+ element.cancel();
+ assert.isTrue(processorCancelStub.called);
+ });
+ });
- setup(done => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
- content = [
- {
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- },
- {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- },
- ];
- element = fixture('basic');
- outputEl = element.queryEffectiveChildren('#diffTable');
- keyLocations = {left: {}, right: {}};
- sandbox.stub(element, '_getDiffBuilder', () => {
- const builder = new GrDiffBuilder({content}, prefs, outputEl);
- sandbox.stub(builder, 'addColumns');
- builder.buildSectionElement = function(group) {
- const section = document.createElement('stub');
- section.textContent = group.lines
- .reduce((acc, line) => acc + line.text, '');
- return section;
- };
- return builder;
- });
- element.diff = {content};
- element.render(keyLocations, prefs).then(done);
- });
+ suite('mock-diff', () => {
+ let element;
+ let builder;
+ let diff;
+ let prefs;
+ let keyLocations;
- test('addColumns is called', done => {
- element.render(keyLocations, {}).then(done);
- assert.isTrue(element._builder.addColumns.called);
- });
+ setup(done => {
+ element = fixture('mock-diff');
+ diff = document.createElement('mock-diff-response').diffResponse;
+ element.diff = diff;
- test('getSectionsByLineRange one line', () => {
- const section = outputEl.querySelector('stub:nth-of-type(2)');
- const sections = element._builder.getSectionsByLineRange(1, 1, 'left');
- assert.equal(sections.length, 1);
- assert.strictEqual(sections[0], section);
- });
+ prefs = {
+ line_length: 80,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ keyLocations = {left: {}, right: {}};
- test('getSectionsByLineRange over diff', () => {
- const section = [
- outputEl.querySelector('stub:nth-of-type(2)'),
- outputEl.querySelector('stub:nth-of-type(3)'),
- ];
- const sections = element._builder.getSectionsByLineRange(1, 2, 'left');
- assert.equal(sections.length, 2);
- assert.strictEqual(sections[0], section[0]);
- assert.strictEqual(sections[1], section[1]);
- });
-
- test('render-start and render-content are fired', done => {
- const dispatchEventStub = sandbox.stub(element, 'dispatchEvent');
- element.render(keyLocations, {}).then(() => {
- const firedEventTypes = dispatchEventStub.getCalls()
- .map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-start');
- assert.include(firedEventTypes, 'render-content');
- done();
- });
- });
-
- test('cancel', () => {
- const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
- element.cancel();
- assert.isTrue(processorCancelStub.called);
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+ done();
});
});
- suite('mock-diff', () => {
- let element;
- let builder;
- let diff;
- let prefs;
- let keyLocations;
+ test('getContentByLine', () => {
+ let actual;
- setup(done => {
- element = fixture('mock-diff');
- diff = document.createElement('mock-diff-response').diffResponse;
- element.diff = diff;
+ actual = builder.getContentByLine(2, 'left');
+ assert.equal(actual.textContent, diff.content[0].ab[1]);
- prefs = {
- line_length: 80,
- show_tabs: true,
- tab_size: 4,
- };
- keyLocations = {left: {}, right: {}};
+ actual = builder.getContentByLine(2, 'right');
+ assert.equal(actual.textContent, diff.content[0].ab[1]);
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
- done();
- });
+ actual = builder.getContentByLine(5, 'left');
+ assert.equal(actual.textContent, diff.content[2].ab[0]);
+
+ actual = builder.getContentByLine(5, 'right');
+ assert.equal(actual.textContent, diff.content[1].b[0]);
+ });
+
+ test('findLinesByRange', () => {
+ const lines = [];
+ const elems = [];
+ const start = 6;
+ const end = 10;
+ const count = end - start + 1;
+
+ builder.findLinesByRange(start, end, 'right', lines, elems);
+
+ assert.equal(lines.length, count);
+ assert.equal(elems.length, count);
+
+ for (let i = 0; i < 5; i++) {
+ assert.instanceOf(lines[i], GrDiffLine);
+ assert.equal(lines[i].afterNumber, start + i);
+ assert.instanceOf(elems[i], HTMLElement);
+ assert.equal(lines[i].text, elems[i].textContent);
+ }
+ });
+
+ test('_renderContentByRange', () => {
+ const spy = sandbox.spy(builder, '_createTextEl');
+ const start = 9;
+ const end = 14;
+ const count = end - start + 1;
+
+ builder._renderContentByRange(start, end, 'left');
+
+ assert.equal(spy.callCount, count);
+ spy.getCalls().forEach((call, i) => {
+ assert.equal(call.args[1].beforeNumber, start + i);
});
+ });
- test('getContentByLine', () => {
- let actual;
+ test('_renderContentByRange notexistent elements', () => {
+ const spy = sandbox.spy(builder, '_createTextEl');
- actual = builder.getContentByLine(2, 'left');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
+ sandbox.stub(builder, 'findLinesByRange',
+ (s, e, d, lines, elements) => {
+ // Add a line and a corresponding element.
+ lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
+ const tr = document.createElement('tr');
+ const td = document.createElement('td');
+ const el = document.createElement('div');
+ tr.appendChild(td);
+ td.appendChild(el);
+ elements.push(el);
- actual = builder.getContentByLine(2, 'right');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
+ // Add 2 lines without corresponding elements.
+ lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
+ lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
+ });
- actual = builder.getContentByLine(5, 'left');
- assert.equal(actual.textContent, diff.content[2].ab[0]);
+ builder._renderContentByRange(1, 10, 'left');
+ // Should be called only once because only one line had a corresponding
+ // element.
+ assert.equal(spy.callCount, 1);
+ });
- actual = builder.getContentByLine(5, 'right');
- assert.equal(actual.textContent, diff.content[1].b[0]);
- });
+ test('_getLineNumberEl side-by-side left', () => {
+ const contentEl = builder.getContentByLine(5, 'left',
+ element.$.diffTable);
+ const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
+ assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl.classList.contains('left'));
+ });
- test('findLinesByRange', () => {
- const lines = [];
- const elems = [];
- const start = 6;
- const end = 10;
- const count = end - start + 1;
+ test('_getLineNumberEl side-by-side right', () => {
+ const contentEl = builder.getContentByLine(5, 'right',
+ element.$.diffTable);
+ const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
+ assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl.classList.contains('right'));
+ });
- builder.findLinesByRange(start, end, 'right', lines, elems);
+ test('_getLineNumberEl unified left', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
- assert.equal(lines.length, count);
- assert.equal(elems.length, count);
-
- for (let i = 0; i < 5; i++) {
- assert.instanceOf(lines[i], GrDiffLine);
- assert.equal(lines[i].afterNumber, start + i);
- assert.instanceOf(elems[i], HTMLElement);
- assert.equal(lines[i].text, elems[i].textContent);
- }
- });
-
- test('_renderContentByRange', () => {
- const spy = sandbox.spy(builder, '_createTextEl');
- const start = 9;
- const end = 14;
- const count = end - start + 1;
-
- builder._renderContentByRange(start, end, 'left');
-
- assert.equal(spy.callCount, count);
- spy.getCalls().forEach((call, i) => {
- assert.equal(call.args[1].beforeNumber, start + i);
- });
- });
-
- test('_renderContentByRange notexistent elements', () => {
- const spy = sandbox.spy(builder, '_createTextEl');
-
- sandbox.stub(builder, 'findLinesByRange',
- (s, e, d, lines, elements) => {
- // Add a line and a corresponding element.
- lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
- const tr = document.createElement('tr');
- const td = document.createElement('td');
- const el = document.createElement('div');
- tr.appendChild(td);
- td.appendChild(el);
- elements.push(el);
-
- // Add 2 lines without corresponding elements.
- lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
- lines.push(new GrDiffLine(GrDiffLine.Type.BOTH));
- });
-
- builder._renderContentByRange(1, 10, 'left');
- // Should be called only once because only one line had a corresponding
- // element.
- assert.equal(spy.callCount, 1);
- });
-
- test('_getLineNumberEl side-by-side left', () => {
const contentEl = builder.getContentByLine(5, 'left',
element.$.diffTable);
const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
assert.isTrue(lineNumberEl.classList.contains('lineNum'));
assert.isTrue(lineNumberEl.classList.contains('left'));
+ done();
});
+ });
- test('_getLineNumberEl side-by-side right', () => {
+ test('_getLineNumberEl unified right', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+
const contentEl = builder.getContentByLine(5, 'right',
element.$.diffTable);
const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
assert.isTrue(lineNumberEl.classList.contains('lineNum'));
assert.isTrue(lineNumberEl.classList.contains('right'));
+ done();
});
+ });
- test('_getLineNumberEl unified left', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
+ test('_getNextContentOnSide side-by-side left', () => {
+ const startElem = builder.getContentByLine(5, 'left',
+ element.$.diffTable);
+ const expectedStartString = diff.content[2].ab[0];
+ const expectedNextString = diff.content[2].ab[1];
+ assert.equal(startElem.textContent, expectedStartString);
- const contentEl = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('left'));
- done();
- });
- });
+ const nextElem = builder._getNextContentOnSide(startElem,
+ 'left');
+ assert.equal(nextElem.textContent, expectedNextString);
+ });
- test('_getLineNumberEl unified right', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
+ test('_getNextContentOnSide side-by-side right', () => {
+ const startElem = builder.getContentByLine(5, 'right',
+ element.$.diffTable);
+ const expectedStartString = diff.content[1].b[0];
+ const expectedNextString = diff.content[1].b[1];
+ assert.equal(startElem.textContent, expectedStartString);
- const contentEl = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('right'));
- done();
- });
- });
+ const nextElem = builder._getNextContentOnSide(startElem,
+ 'right');
+ assert.equal(nextElem.textContent, expectedNextString);
+ });
- test('_getNextContentOnSide side-by-side left', () => {
+ test('_getNextContentOnSide unified left', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+
const startElem = builder.getContentByLine(5, 'left',
element.$.diffTable);
const expectedStartString = diff.content[2].ab[0];
@@ -1047,9 +1098,17 @@
const nextElem = builder._getNextContentOnSide(startElem,
'left');
assert.equal(nextElem.textContent, expectedNextString);
- });
- test('_getNextContentOnSide side-by-side right', () => {
+ done();
+ });
+ });
+
+ test('_getNextContentOnSide unified right', done => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations, prefs).then(() => {
+ builder = element._builder;
+
const startElem = builder.getContentByLine(5, 'right',
element.$.diffTable);
const expectedStartString = diff.content[1].b[0];
@@ -1059,136 +1118,99 @@
const nextElem = builder._getNextContentOnSide(startElem,
'right');
assert.equal(nextElem.textContent, expectedNextString);
- });
- test('_getNextContentOnSide unified left', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const expectedStartString = diff.content[2].ab[0];
- const expectedNextString = diff.content[2].ab[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder._getNextContentOnSide(startElem,
- 'left');
- assert.equal(nextElem.textContent, expectedNextString);
-
- done();
- });
- });
-
- test('_getNextContentOnSide unified right', done => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- element.render(keyLocations, prefs).then(() => {
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const expectedStartString = diff.content[1].b[0];
- const expectedNextString = diff.content[1].b[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder._getNextContentOnSide(startElem,
- 'right');
- assert.equal(nextElem.textContent, expectedNextString);
-
- done();
- });
- });
-
- test('escaping HTML', () => {
- let input = '<script>alert("XSS");<' + '/script>';
- let expected = '<script>alert("XSS");</script>';
- let result = builder._formatText(input, 1, Infinity).innerHTML;
- assert.equal(result, expected);
-
- input = '& < > " \' / `';
- expected = '& < > " \' / `';
- result = builder._formatText(input, 1, Infinity).innerHTML;
- assert.equal(result, expected);
+ done();
});
});
- suite('blame', () => {
- let mockBlame;
+ test('escaping HTML', () => {
+ let input = '<script>alert("XSS");<' + '/script>';
+ let expected = '<script>alert("XSS");</script>';
+ let result = builder._formatText(input, 1, Infinity).innerHTML;
+ assert.equal(result, expected);
- setup(() => {
- mockBlame = [
- {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
- {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
- ];
- });
-
- test('setBlame attempts to render each blamed line', () => {
- const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum')
- .returns(null);
- builder.setBlame(mockBlame);
- assert.equal(getBlameStub.callCount, 32);
- });
-
- test('_getBlameCommitForBaseLine', () => {
- builder.setBlame(mockBlame);
- assert.isOk(builder._getBlameCommitForBaseLine(1));
- assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1');
-
- assert.isOk(builder._getBlameCommitForBaseLine(11));
- assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1');
-
- assert.isOk(builder._getBlameCommitForBaseLine(32));
- assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2');
-
- assert.isNull(builder._getBlameCommitForBaseLine(33));
- });
-
- test('_getBlameCommitForBaseLine w/o blame returns null', () => {
- assert.isNull(builder._getBlameCommitForBaseLine(1));
- assert.isNull(builder._getBlameCommitForBaseLine(11));
- assert.isNull(builder._getBlameCommitForBaseLine(31));
- });
-
- test('_createBlameCell', () => {
- const mocbBlameCell = document.createElement('span');
- const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine')
- .returns(mocbBlameCell);
- const line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.beforeNumber = 3;
- line.afterNumber = 5;
-
- const result = builder._createBlameCell(line);
-
- assert.isTrue(getBlameStub.calledWithExactly(3));
- assert.equal(result.getAttribute('data-line-number'), '3');
- assert.equal(result.firstChild, mocbBlameCell);
- });
-
- test('_getBlameForBaseLine', () => {
- const mockCommit = {
- time: 1576105200,
- id: 1234567890,
- author: 'Clark Kent',
- commit_msg: 'Testing Commit',
- ranges: [1],
- };
- const blameNode = builder._getBlameForBaseLine(1, mockCommit);
-
- const authors = blameNode.getElementsByClassName('blameAuthor');
- assert.equal(authors.length, 1);
- assert.equal(authors[0].innerText, ' Clark');
-
- const date = (new Date(mockCommit.time * 1000)).toLocaleDateString();
- Polymer.dom.flush();
- const cards = blameNode.getElementsByClassName('blameHoverCard');
- assert.equal(cards.length, 1);
- assert.equal(cards[0].innerHTML,
- `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}`
- + '<br><br>Testing Commit'
- );
- });
+ input = '& < > " \' / `';
+ expected = '& < > " \' / `';
+ result = builder._formatText(input, 1, Infinity).innerHTML;
+ assert.equal(result, expected);
});
});
+
+ suite('blame', () => {
+ let mockBlame;
+
+ setup(() => {
+ mockBlame = [
+ {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
+ {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
+ ];
+ });
+
+ test('setBlame attempts to render each blamed line', () => {
+ const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum')
+ .returns(null);
+ builder.setBlame(mockBlame);
+ assert.equal(getBlameStub.callCount, 32);
+ });
+
+ test('_getBlameCommitForBaseLine', () => {
+ builder.setBlame(mockBlame);
+ assert.isOk(builder._getBlameCommitForBaseLine(1));
+ assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1');
+
+ assert.isOk(builder._getBlameCommitForBaseLine(11));
+ assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1');
+
+ assert.isOk(builder._getBlameCommitForBaseLine(32));
+ assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2');
+
+ assert.isNull(builder._getBlameCommitForBaseLine(33));
+ });
+
+ test('_getBlameCommitForBaseLine w/o blame returns null', () => {
+ assert.isNull(builder._getBlameCommitForBaseLine(1));
+ assert.isNull(builder._getBlameCommitForBaseLine(11));
+ assert.isNull(builder._getBlameCommitForBaseLine(31));
+ });
+
+ test('_createBlameCell', () => {
+ const mocbBlameCell = document.createElement('span');
+ const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine')
+ .returns(mocbBlameCell);
+ const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = 3;
+ line.afterNumber = 5;
+
+ const result = builder._createBlameCell(line);
+
+ assert.isTrue(getBlameStub.calledWithExactly(3));
+ assert.equal(result.getAttribute('data-line-number'), '3');
+ assert.equal(result.firstChild, mocbBlameCell);
+ });
+
+ test('_getBlameForBaseLine', () => {
+ const mockCommit = {
+ time: 1576105200,
+ id: 1234567890,
+ author: 'Clark Kent',
+ commit_msg: 'Testing Commit',
+ ranges: [1],
+ };
+ const blameNode = builder._getBlameForBaseLine(1, mockCommit);
+
+ const authors = blameNode.getElementsByClassName('blameAuthor');
+ assert.equal(authors.length, 1);
+ assert.equal(authors[0].innerText, ' Clark');
+
+ const date = (new Date(mockCommit.time * 1000)).toLocaleDateString();
+ flush();
+ const cards = blameNode.getElementsByClassName('blameHoverCard');
+ assert.equal(cards.length, 1);
+ assert.equal(cards[0].innerHTML,
+ `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}`
+ + '<br><br>Testing Commit'
+ );
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
index 4f0a94f..0ce9a42 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
@@ -19,189 +19,206 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>GrDiffBuilderUnified</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="../../../scripts/util.js"></script>
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../gr-diff/gr-diff-group.js"></script>
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-<script src="gr-diff-builder.js"></script>
-<script src="gr-diff-builder-unified.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 type="module" src="../../../scripts/util.js"></script>
+<script type="module" src="../gr-diff/gr-diff-line.js"></script>
+<script type="module" src="../gr-diff/gr-diff-group.js"></script>
+<script type="module" src="../gr-diff-highlight/gr-annotation.js"></script>
+<script type="module" src="./gr-diff-builder.js"></script>
+<script type="module" src="./gr-diff-builder-unified.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import './gr-diff-builder-unified.js';
+void(0);
+</script>
-<script>
- suite('GrDiffBuilderUnified tests', async () => {
- await readyToTest();
- let prefs;
- let outputEl;
- let diffBuilder;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../gr-diff-highlight/gr-annotation.js';
+import './gr-diff-builder.js';
+import './gr-diff-builder-unified.js';
+suite('GrDiffBuilderUnified tests', () => {
+ let prefs;
+ let outputEl;
+ let diffBuilder;
- setup(()=> {
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- outputEl = document.createElement('div');
- diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
+ setup(()=> {
+ prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ outputEl = document.createElement('div');
+ diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
+ });
+
+ suite('buildSectionElement for BOTH group', () => {
+ let lines;
+ let group;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World";';
+ lines[2].text = ' return True';
+
+ group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines);
});
- suite('buildSectionElement for BOTH group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2),
- new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3),
- new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World";';
- lines[2].text = ' return True';
-
- group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines);
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('both'));
- });
-
- test('creates each unchanged row once', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 3);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[0].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[1].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.equal(
- rowEls[2].querySelector('.lineNum.left').textContent,
- lines[2].beforeNumber);
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
- });
+ test('creates the section', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('both'));
});
- suite('buildSectionElement for DELTA group', () => {
- let lines;
- let group;
+ test('creates each unchanged row once', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLine.Type.REMOVE, 1),
- new GrDiffLine(GrDiffLine.Type.REMOVE, 2),
- new GrDiffLine(GrDiffLine.Type.ADD, 2),
- new GrDiffLine(GrDiffLine.Type.ADD, 3),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- lines[2].text = 'def hello_universe()';
- lines[3].text = ' print "Hello Universe"';
+ assert.equal(rowEls.length, 3);
- group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
- });
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.left').textContent,
+ lines[0].beforeNumber);
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.right').textContent,
+ lines[0].afterNumber);
+ assert.equal(
+ rowEls[0].querySelector('.content').textContent, lines[0].text);
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('delta'));
- });
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.left').textContent,
+ lines[1].beforeNumber);
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.right').textContent,
+ lines[1].afterNumber);
+ assert.equal(
+ rowEls[1].querySelector('.content').textContent, lines[1].text);
- test('creates the section with class if ignoredWhitespaceOnly', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
- });
-
- test('creates the section with class if dueToRebase', () => {
- group.dueToRebase = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('dueToRebase'));
- });
-
- test('creates first the removed and then the added rows', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 4);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[3].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[3].querySelector('.content').textContent, lines[3].text);
- });
-
- test('creates only the added rows if only ignored whitespace', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 2);
-
- assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[3].text);
- });
+ assert.equal(
+ rowEls[2].querySelector('.lineNum.left').textContent,
+ lines[2].beforeNumber);
+ assert.equal(
+ rowEls[2].querySelector('.lineNum.right').textContent,
+ lines[2].afterNumber);
+ assert.equal(
+ rowEls[2].querySelector('.content').textContent, lines[2].text);
});
});
+
+ suite('buildSectionElement for DELTA group', () => {
+ let lines;
+ let group;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 1),
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 2),
+ new GrDiffLine(GrDiffLine.Type.ADD, 2),
+ new GrDiffLine(GrDiffLine.Type.ADD, 3),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ lines[2].text = 'def hello_universe()';
+ lines[3].text = ' print "Hello Universe"';
+
+ group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
+ });
+
+ test('creates the section', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('delta'));
+ });
+
+ test('creates the section with class if ignoredWhitespaceOnly', () => {
+ group.ignoredWhitespaceOnly = true;
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
+ });
+
+ test('creates the section with class if dueToRebase', () => {
+ group.dueToRebase = true;
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('dueToRebase'));
+ });
+
+ test('creates first the removed and then the added rows', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 4);
+
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.left').textContent,
+ lines[0].beforeNumber);
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
+ assert.equal(
+ rowEls[0].querySelector('.content').textContent, lines[0].text);
+
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.left').textContent,
+ lines[1].beforeNumber);
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
+ assert.equal(
+ rowEls[1].querySelector('.content').textContent, lines[1].text);
+
+ assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[2].querySelector('.lineNum.right').textContent,
+ lines[2].afterNumber);
+ assert.equal(
+ rowEls[2].querySelector('.content').textContent, lines[2].text);
+
+ assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[3].querySelector('.lineNum.right').textContent,
+ lines[3].afterNumber);
+ assert.equal(
+ rowEls[3].querySelector('.content').textContent, lines[3].text);
+ });
+
+ test('creates only the added rows if only ignored whitespace', () => {
+ group.ignoredWhitespaceOnly = true;
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 2);
+
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[0].querySelector('.lineNum.right').textContent,
+ lines[2].afterNumber);
+ assert.equal(
+ rowEls[0].querySelector('.content').textContent, lines[2].text);
+
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
+ assert.equal(
+ rowEls[1].querySelector('.lineNum.right').textContent,
+ lines[3].afterNumber);
+ assert.equal(
+ rowEls[1].querySelector('.content').textContent, lines[3].text);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 87152d8..92ea310 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -14,485 +14,494 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DiffSides = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.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-diff-cursor_html.js';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const DiffSides = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
- const ScrollBehavior = {
- KEEP_VISIBLE: 'keep-visible',
- NEVER: 'never',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- const LEFT_SIDE_CLASS = 'target-side-left';
- const RIGHT_SIDE_CLASS = 'target-side-right';
+const ScrollBehavior = {
+ KEEP_VISIBLE: 'keep-visible',
+ NEVER: 'never',
+};
- /** @extends Polymer.Element */
- class GrDiffCursor extends Polymer.mixinBehaviors([Gerrit.FireBehavior],
- Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(Polymer.Element))) {
- static get is() { return 'gr-diff-cursor'; }
+const LEFT_SIDE_CLASS = 'target-side-left';
+const RIGHT_SIDE_CLASS = 'target-side-right';
- static get properties() {
- return {
+/** @extends Polymer.Element */
+class GrDiffCursor extends mixinBehaviors([Gerrit.FireBehavior],
+ GestureEventListeners(
+ LegacyElementMixin(PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-cursor'; }
+
+ static get properties() {
+ return {
+ /**
+ * Either DiffSides.LEFT or DiffSides.RIGHT.
+ */
+ side: {
+ type: String,
+ value: DiffSides.RIGHT,
+ },
+ /** @type {!HTMLElement|undefined} */
+ diffRow: {
+ type: Object,
+ notify: true,
+ observer: '_rowChanged',
+ },
+
/**
- * Either DiffSides.LEFT or DiffSides.RIGHT.
+ * The diff views to cursor through and listen to.
*/
- side: {
- type: String,
- value: DiffSides.RIGHT,
- },
- /** @type {!HTMLElement|undefined} */
- diffRow: {
- type: Object,
- notify: true,
- observer: '_rowChanged',
- },
+ diffs: {
+ type: Array,
+ value() { return []; },
+ },
- /**
- * The diff views to cursor through and listen to.
- */
- diffs: {
- type: Array,
- value() { return []; },
- },
+ /**
+ * If set, the cursor will attempt to move to the line number (instead of
+ * the first chunk) the next time the diff renders. It is set back to null
+ * when used. It should be only used if you want the line to be focused
+ * after initialization of the component and page should scroll
+ * to that position. This parameter should be set at most for one gr-diff
+ * element in the page.
+ *
+ * @type {?number}
+ */
+ initialLineNumber: {
+ type: Number,
+ value: null,
+ },
- /**
- * If set, the cursor will attempt to move to the line number (instead of
- * the first chunk) the next time the diff renders. It is set back to null
- * when used. It should be only used if you want the line to be focused
- * after initialization of the component and page should scroll
- * to that position. This parameter should be set at most for one gr-diff
- * element in the page.
- *
- * @type {?number}
- */
- initialLineNumber: {
- type: Number,
- value: null,
- },
+ /**
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+ _scrollBehavior: {
+ type: String,
+ value: ScrollBehavior.KEEP_VISIBLE,
+ },
- /**
- * The scroll behavior for the cursor. Values are 'never' and
- * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
- * the viewport.
- */
- _scrollBehavior: {
- type: String,
- value: ScrollBehavior.KEEP_VISIBLE,
- },
+ _focusOnMove: {
+ type: Boolean,
+ value: true,
+ },
- _focusOnMove: {
- type: Boolean,
- value: true,
- },
+ _listeningForScroll: Boolean,
- _listeningForScroll: Boolean,
+ /**
+ * gr-diff-view has gr-fixed-panel on top. The panel can
+ * intersect a main element and partially hides a content of
+ * the main element. To correctly calculates visibility of an
+ * element, the cursor must know how much height occuped by a fixed
+ * panel.
+ * The scrollTopMargin defines margin occuped by fixed panel.
+ */
+ scrollTopMargin: {
+ type: Number,
+ value: 0,
+ },
+ };
+ }
- /**
- * gr-diff-view has gr-fixed-panel on top. The panel can
- * intersect a main element and partially hides a content of
- * the main element. To correctly calculates visibility of an
- * element, the cursor must know how much height occuped by a fixed
- * panel.
- * The scrollTopMargin defines margin occuped by fixed panel.
- */
- scrollTopMargin: {
- type: Number,
- value: 0,
- },
- };
+ static get observers() {
+ return [
+ '_updateSideClass(side)',
+ '_diffsChanged(diffs.splices)',
+ ];
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ afterNextRender(this, () => {
+ /*
+ This represents the diff cursor is ready for interaction coming from
+ client components. It is more then Polymer "ready" lifecycle, as no
+ "ready" events are automatically fired by Polymer, it means
+ the cursor is completely interactable - in this case attached and
+ painted on the page. We name it "ready" instead of "rendered" as the
+ long-term goal is to make gr-diff-cursor a javascript class - not a DOM
+ element with an actual lifecycle. This will be triggered only once
+ per element.
+ */
+ this.fire('ready', null, {bubbles: false});
+ });
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ // Catch when users are scrolling as the view loads.
+ this.listen(window, 'scroll', '_handleWindowScroll');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_handleWindowScroll');
+ }
+
+ moveLeft() {
+ this.side = DiffSides.LEFT;
+ if (this._isTargetBlank()) {
+ this.moveUp();
+ }
+ }
+
+ moveRight() {
+ this.side = DiffSides.RIGHT;
+ if (this._isTargetBlank()) {
+ this.moveUp();
+ }
+ }
+
+ moveDown() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.next(this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.next();
+ }
+ }
+
+ moveUp() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.previous(this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.previous();
+ }
+ }
+
+ moveToVisibleArea() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.cursorManager.moveToVisibleArea(
+ this._rowHasSide.bind(this));
+ } else {
+ this.$.cursorManager.moveToVisibleArea();
+ }
+ }
+
+ moveToNextChunk(opt_clipToTop) {
+ this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
+ target => target.parentNode.scrollHeight, opt_clipToTop);
+ this._fixSide();
+ }
+
+ moveToPreviousChunk() {
+ this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this));
+ this._fixSide();
+ }
+
+ moveToNextCommentThread() {
+ this.$.cursorManager.next(this._rowHasThread.bind(this));
+ this._fixSide();
+ }
+
+ moveToPreviousCommentThread() {
+ this.$.cursorManager.previous(this._rowHasThread.bind(this));
+ this._fixSide();
+ }
+
+ /**
+ * @param {number} number
+ * @param {string} side
+ * @param {string=} opt_path
+ */
+ moveToLineNumber(number, side, opt_path) {
+ const row = this._findRowByNumberAndFile(number, side, opt_path);
+ if (row) {
+ this.side = side;
+ this.$.cursorManager.setCursor(row);
+ }
+ }
+
+ /**
+ * Get the line number element targeted by the cursor row and side.
+ *
+ * @return {?Element|undefined}
+ */
+ getTargetLineElement() {
+ let lineElSelector = '.lineNum';
+
+ if (!this.diffRow) {
+ return;
}
- static get observers() {
- return [
- '_updateSideClass(side)',
- '_diffsChanged(diffs.splices)',
- ];
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
}
- /** @override */
- ready() {
- super.ready();
- Polymer.RenderStatus.afterNextRender(this, () => {
- /*
- This represents the diff cursor is ready for interaction coming from
- client components. It is more then Polymer "ready" lifecycle, as no
- "ready" events are automatically fired by Polymer, it means
- the cursor is completely interactable - in this case attached and
- painted on the page. We name it "ready" instead of "rendered" as the
- long-term goal is to make gr-diff-cursor a javascript class - not a DOM
- element with an actual lifecycle. This will be triggered only once
- per element.
- */
- this.fire('ready', null, {bubbles: false});
- });
+ return this.diffRow.querySelector(lineElSelector);
+ }
+
+ getTargetDiffElement() {
+ if (!this.diffRow) return null;
+
+ const hostOwner = dom( (this.diffRow))
+ .getOwnerRoot();
+ if (hostOwner && hostOwner.host &&
+ hostOwner.host.tagName === 'GR-DIFF') {
+ return hostOwner.host;
}
+ return null;
+ }
- /** @override */
- attached() {
- super.attached();
- // Catch when users are scrolling as the view loads.
- this.listen(window, 'scroll', '_handleWindowScroll');
+ moveToFirstChunk() {
+ this.$.cursorManager.moveToStart();
+ this.moveToNextChunk(true);
+ }
+
+ moveToLastChunk() {
+ this.$.cursorManager.moveToEnd();
+ this.moveToPreviousChunk();
+ }
+
+ reInitCursor() {
+ this._updateStops();
+ if (this.initialLineNumber) {
+ this.moveToLineNumber(this.initialLineNumber, this.side);
+ this.initialLineNumber = null;
+ } else {
+ this.moveToFirstChunk();
}
+ }
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_handleWindowScroll');
- }
-
- moveLeft() {
- this.side = DiffSides.LEFT;
- if (this._isTargetBlank()) {
- this.moveUp();
- }
- }
-
- moveRight() {
- this.side = DiffSides.RIGHT;
- if (this._isTargetBlank()) {
- this.moveUp();
- }
- }
-
- moveDown() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.cursorManager.next(this._rowHasSide.bind(this));
- } else {
- this.$.cursorManager.next();
- }
- }
-
- moveUp() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.cursorManager.previous(this._rowHasSide.bind(this));
- } else {
- this.$.cursorManager.previous();
- }
- }
-
- moveToVisibleArea() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.cursorManager.moveToVisibleArea(
- this._rowHasSide.bind(this));
- } else {
- this.$.cursorManager.moveToVisibleArea();
- }
- }
-
- moveToNextChunk(opt_clipToTop) {
- this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
- target => target.parentNode.scrollHeight, opt_clipToTop);
- this._fixSide();
- }
-
- moveToPreviousChunk() {
- this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this));
- this._fixSide();
- }
-
- moveToNextCommentThread() {
- this.$.cursorManager.next(this._rowHasThread.bind(this));
- this._fixSide();
- }
-
- moveToPreviousCommentThread() {
- this.$.cursorManager.previous(this._rowHasThread.bind(this));
- this._fixSide();
- }
-
- /**
- * @param {number} number
- * @param {string} side
- * @param {string=} opt_path
- */
- moveToLineNumber(number, side, opt_path) {
- const row = this._findRowByNumberAndFile(number, side, opt_path);
- if (row) {
- this.side = side;
- this.$.cursorManager.setCursor(row);
- }
- }
-
- /**
- * Get the line number element targeted by the cursor row and side.
- *
- * @return {?Element|undefined}
- */
- getTargetLineElement() {
- let lineElSelector = '.lineNum';
-
- if (!this.diffRow) {
- return;
- }
-
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
- }
-
- return this.diffRow.querySelector(lineElSelector);
- }
-
- getTargetDiffElement() {
- if (!this.diffRow) return null;
-
- const hostOwner = Polymer.dom(/** @type {Node} */ (this.diffRow))
- .getOwnerRoot();
- if (hostOwner && hostOwner.host &&
- hostOwner.host.tagName === 'GR-DIFF') {
- return hostOwner.host;
- }
- return null;
- }
-
- moveToFirstChunk() {
- this.$.cursorManager.moveToStart();
- this.moveToNextChunk(true);
- }
-
- moveToLastChunk() {
- this.$.cursorManager.moveToEnd();
- this.moveToPreviousChunk();
- }
-
- reInitCursor() {
- this._updateStops();
- if (this.initialLineNumber) {
- this.moveToLineNumber(this.initialLineNumber, this.side);
- this.initialLineNumber = null;
- } else {
- this.moveToFirstChunk();
- }
- }
-
- _handleWindowScroll() {
- if (this._listeningForScroll) {
- this._scrollBehavior = ScrollBehavior.NEVER;
- this._focusOnMove = false;
- this._listeningForScroll = false;
- }
- }
-
- handleDiffUpdate() {
- this._updateStops();
- if (!this.diffRow) {
- // does not scroll during init unless requested
- const scrollingBehaviorForInit = this.initialLineNumber ?
- ScrollBehavior.KEEP_VISIBLE :
- ScrollBehavior.NEVER;
- this._scrollBehavior = scrollingBehaviorForInit;
- this.reInitCursor();
- }
- this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
- this._focusOnMove = true;
+ _handleWindowScroll() {
+ if (this._listeningForScroll) {
+ this._scrollBehavior = ScrollBehavior.NEVER;
+ this._focusOnMove = false;
this._listeningForScroll = false;
}
+ }
- _handleDiffRenderStart() {
- this._listeningForScroll = true;
+ handleDiffUpdate() {
+ this._updateStops();
+ if (!this.diffRow) {
+ // does not scroll during init unless requested
+ const scrollingBehaviorForInit = this.initialLineNumber ?
+ ScrollBehavior.KEEP_VISIBLE :
+ ScrollBehavior.NEVER;
+ this._scrollBehavior = scrollingBehaviorForInit;
+ this.reInitCursor();
}
+ this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
+ this._focusOnMove = true;
+ this._listeningForScroll = false;
+ }
- createCommentInPlace() {
- const diffWithRangeSelected = this.diffs
- .find(diff => diff.isRangeSelected());
- if (diffWithRangeSelected) {
- diffWithRangeSelected.createRangeComment();
- } else {
- const line = this.getTargetLineElement();
- if (line) {
- this.getTargetDiffElement().addDraftAtLine(line);
- }
- }
- }
+ _handleDiffRenderStart() {
+ this._listeningForScroll = true;
+ }
- /**
- * Get an object describing the location of the cursor. Such as
- * {leftSide: false, number: 123} for line 123 of the revision, or
- * {leftSide: true, number: 321} for line 321 of the base patch.
- * Returns null if an address is not available.
- *
- * @return {?Object}
- */
- getAddress() {
- if (!this.diffRow) { return null; }
-
- // Get the line-number cell targeted by the cursor. If the mode is unified
- // then prefer the revision cell if available.
- let cell;
- if (this._getViewMode() === DiffViewMode.UNIFIED) {
- cell = this.diffRow.querySelector('.lineNum.right');
- if (!cell) {
- cell = this.diffRow.querySelector('.lineNum.left');
- }
- } else {
- cell = this.diffRow.querySelector('.lineNum.' + this.side);
- }
- if (!cell) { return null; }
-
- const number = cell.getAttribute('data-value');
- if (!number || number === 'FILE') { return null; }
-
- return {
- leftSide: cell.matches('.left'),
- number: parseInt(number, 10),
- };
- }
-
- _getViewMode() {
- if (!this.diffRow) {
- return null;
- }
-
- if (this.diffRow.classList.contains('side-by-side')) {
- return DiffViewMode.SIDE_BY_SIDE;
- } else {
- return DiffViewMode.UNIFIED;
- }
- }
-
- _rowHasSide(row) {
- const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
- ' + .content';
- return !!row.querySelector(selector);
- }
-
- _isFirstRowOfChunk(row) {
- const parentClassList = row.parentNode.classList;
- return parentClassList.contains('section') &&
- parentClassList.contains('delta') &&
- !row.previousSibling;
- }
-
- _rowHasThread(row) {
- return row.querySelector('.thread-group');
- }
-
- /**
- * If we jumped to a row where there is no content on the current side then
- * switch to the alternate side.
- */
- _fixSide() {
- if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
- this._isTargetBlank()) {
- this.side = this.side === DiffSides.LEFT ?
- DiffSides.RIGHT : DiffSides.LEFT;
- }
- }
-
- _isTargetBlank() {
- if (!this.diffRow) {
- return false;
- }
-
- const actions = this._getActionsForRow();
- return (this.side === DiffSides.LEFT && !actions.left) ||
- (this.side === DiffSides.RIGHT && !actions.right);
- }
-
- _rowChanged(newRow, oldRow) {
- if (oldRow) {
- oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
- }
- this._updateSideClass();
- }
-
- _updateSideClass() {
- if (!this.diffRow) {
- return;
- }
- this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
- this.diffRow);
- this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
- this.diffRow);
- }
-
- _isActionType(type) {
- return type !== 'blank' && type !== 'contextControl';
- }
-
- _getActionsForRow() {
- const actions = {left: false, right: false};
- if (this.diffRow) {
- actions.left = this._isActionType(
- this.diffRow.getAttribute('left-type'));
- actions.right = this._isActionType(
- this.diffRow.getAttribute('right-type'));
- }
- return actions;
- }
-
- _getStops() {
- return this.diffs.reduce(
- (stops, diff) => stops.concat(diff.getCursorStops()), []);
- }
-
- _updateStops() {
- this.$.cursorManager.stops = this._getStops();
- }
-
- /**
- * Setup and tear down on-render listeners for any diffs that are added or
- * removed from the cursor.
- *
- * @private
- */
- _diffsChanged(changeRecord) {
- if (!changeRecord) { return; }
-
- this._updateStops();
-
- let splice;
- let i;
- for (let spliceIdx = 0;
- changeRecord.indexSplices &&
- spliceIdx < changeRecord.indexSplices.length;
- spliceIdx++) {
- splice = changeRecord.indexSplices[spliceIdx];
-
- for (i = splice.index;
- i < splice.index + splice.addedCount;
- i++) {
- this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
- this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate');
- }
-
- for (i = 0;
- i < splice.removed && splice.removed.length;
- i++) {
- this.unlisten(splice.removed[i],
- 'render-start', '_handleDiffRenderStart');
- this.unlisten(splice.removed[i],
- 'render-content', 'handleDiffUpdate');
- }
- }
- }
-
- _findRowByNumberAndFile(targetNumber, side, opt_path) {
- let stops;
- if (opt_path) {
- const diff = this.diffs.filter(diff => diff.path === opt_path)[0];
- stops = diff.getCursorStops();
- } else {
- stops = this.$.cursorManager.stops;
- }
- let selector;
- for (let i = 0; i < stops.length; i++) {
- selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
- if (stops[i].querySelector(selector)) {
- return stops[i];
- }
+ createCommentInPlace() {
+ const diffWithRangeSelected = this.diffs
+ .find(diff => diff.isRangeSelected());
+ if (diffWithRangeSelected) {
+ diffWithRangeSelected.createRangeComment();
+ } else {
+ const line = this.getTargetLineElement();
+ if (line) {
+ this.getTargetDiffElement().addDraftAtLine(line);
}
}
}
- customElements.define(GrDiffCursor.is, GrDiffCursor);
-})();
+ /**
+ * Get an object describing the location of the cursor. Such as
+ * {leftSide: false, number: 123} for line 123 of the revision, or
+ * {leftSide: true, number: 321} for line 321 of the base patch.
+ * Returns null if an address is not available.
+ *
+ * @return {?Object}
+ */
+ getAddress() {
+ if (!this.diffRow) { return null; }
+
+ // Get the line-number cell targeted by the cursor. If the mode is unified
+ // then prefer the revision cell if available.
+ let cell;
+ if (this._getViewMode() === DiffViewMode.UNIFIED) {
+ cell = this.diffRow.querySelector('.lineNum.right');
+ if (!cell) {
+ cell = this.diffRow.querySelector('.lineNum.left');
+ }
+ } else {
+ cell = this.diffRow.querySelector('.lineNum.' + this.side);
+ }
+ if (!cell) { return null; }
+
+ const number = cell.getAttribute('data-value');
+ if (!number || number === 'FILE') { return null; }
+
+ return {
+ leftSide: cell.matches('.left'),
+ number: parseInt(number, 10),
+ };
+ }
+
+ _getViewMode() {
+ if (!this.diffRow) {
+ return null;
+ }
+
+ if (this.diffRow.classList.contains('side-by-side')) {
+ return DiffViewMode.SIDE_BY_SIDE;
+ } else {
+ return DiffViewMode.UNIFIED;
+ }
+ }
+
+ _rowHasSide(row) {
+ const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
+ ' + .content';
+ return !!row.querySelector(selector);
+ }
+
+ _isFirstRowOfChunk(row) {
+ const parentClassList = row.parentNode.classList;
+ return parentClassList.contains('section') &&
+ parentClassList.contains('delta') &&
+ !row.previousSibling;
+ }
+
+ _rowHasThread(row) {
+ return row.querySelector('.thread-group');
+ }
+
+ /**
+ * If we jumped to a row where there is no content on the current side then
+ * switch to the alternate side.
+ */
+ _fixSide() {
+ if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
+ this._isTargetBlank()) {
+ this.side = this.side === DiffSides.LEFT ?
+ DiffSides.RIGHT : DiffSides.LEFT;
+ }
+ }
+
+ _isTargetBlank() {
+ if (!this.diffRow) {
+ return false;
+ }
+
+ const actions = this._getActionsForRow();
+ return (this.side === DiffSides.LEFT && !actions.left) ||
+ (this.side === DiffSides.RIGHT && !actions.right);
+ }
+
+ _rowChanged(newRow, oldRow) {
+ if (oldRow) {
+ oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
+ }
+ this._updateSideClass();
+ }
+
+ _updateSideClass() {
+ if (!this.diffRow) {
+ return;
+ }
+ this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
+ this.diffRow);
+ this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
+ this.diffRow);
+ }
+
+ _isActionType(type) {
+ return type !== 'blank' && type !== 'contextControl';
+ }
+
+ _getActionsForRow() {
+ const actions = {left: false, right: false};
+ if (this.diffRow) {
+ actions.left = this._isActionType(
+ this.diffRow.getAttribute('left-type'));
+ actions.right = this._isActionType(
+ this.diffRow.getAttribute('right-type'));
+ }
+ return actions;
+ }
+
+ _getStops() {
+ return this.diffs.reduce(
+ (stops, diff) => stops.concat(diff.getCursorStops()), []);
+ }
+
+ _updateStops() {
+ this.$.cursorManager.stops = this._getStops();
+ }
+
+ /**
+ * Setup and tear down on-render listeners for any diffs that are added or
+ * removed from the cursor.
+ *
+ * @private
+ */
+ _diffsChanged(changeRecord) {
+ if (!changeRecord) { return; }
+
+ this._updateStops();
+
+ let splice;
+ let i;
+ for (let spliceIdx = 0;
+ changeRecord.indexSplices &&
+ spliceIdx < changeRecord.indexSplices.length;
+ spliceIdx++) {
+ splice = changeRecord.indexSplices[spliceIdx];
+
+ for (i = splice.index;
+ i < splice.index + splice.addedCount;
+ i++) {
+ this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
+ this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate');
+ }
+
+ for (i = 0;
+ i < splice.removed && splice.removed.length;
+ i++) {
+ this.unlisten(splice.removed[i],
+ 'render-start', '_handleDiffRenderStart');
+ this.unlisten(splice.removed[i],
+ 'render-content', 'handleDiffUpdate');
+ }
+ }
+ }
+
+ _findRowByNumberAndFile(targetNumber, side, opt_path) {
+ let stops;
+ if (opt_path) {
+ const diff = this.diffs.filter(diff => diff.path === opt_path)[0];
+ stops = diff.getCursorStops();
+ } else {
+ stops = this.$.cursorManager.stops;
+ }
+ let selector;
+ for (let i = 0; i < stops.length; i++) {
+ selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
+ if (stops[i].querySelector(selector)) {
+ return stops[i];
+ }
+ }
+ }
+}
+
+customElements.define(GrDiffCursor.is, GrDiffCursor);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
index 1e2d963..81e0c9b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
@@ -1,33 +1,21 @@
-<!--
-@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="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-
-<dom-module id="gr-diff-cursor">
- <template>
- <gr-cursor-manager
- id="cursorManager"
- scroll-behavior="[[_scrollBehavior]]"
- cursor-target-class="target-row"
- focus-on-move="[[_focusOnMove]]"
- target="{{diffRow}}"
- scroll-top-margin="[[scrollTopMargin]]"
- ></gr-cursor-manager>
- </template>
- <script src="gr-diff-cursor.js"></script>
-</dom-module>
+export const htmlTemplate = html`
+ <gr-cursor-manager id="cursorManager" scroll-behavior="[[_scrollBehavior]]" cursor-target-class="target-row" focus-on-move="[[_focusOnMove]]" target="{{diffRow}}" scroll-top-margin="[[scrollTopMargin]]"></gr-cursor-manager>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 02b1d572..40507db 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -19,20 +19,29 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-cursor</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="../gr-diff/gr-diff.html">
-<link rel="import" href="./gr-diff-cursor.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
+<script type="module" src="../gr-diff/gr-diff.js"></script>
+<script type="module" src="./gr-diff-cursor.js"></script>
+<script type="module" src="../../shared/gr-rest-api-interface/gr-rest-api-interface.js"></script>
+<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff.js';
+import './gr-diff-cursor.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -49,54 +58,122 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-cursor tests', async () => {
- await readyToTest();
- let sandbox;
- let cursorElement;
- let diffElement;
- let mockDiffResponse;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff.js';
+import './gr-diff-cursor.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff-cursor tests', () => {
+ let sandbox;
+ let cursorElement;
+ let diffElement;
+ let mockDiffResponse;
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+
+ const fixtureElems = fixture('basic');
+ mockDiffResponse = fixtureElems[0];
+ diffElement = fixtureElems[1];
+ cursorElement = fixtureElems[2];
+ const restAPI = fixtureElems[3];
+
+ // Register the diff with the cursor.
+ cursorElement.push('diffs', diffElement);
+
+ diffElement.loggedIn = false;
+ diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
+ diffElement.comments = {
+ left: [],
+ right: [],
+ meta: {patchRange: undefined},
+ };
+ const setupDone = () => {
+ cursorElement._updateStops();
+ cursorElement.moveToFirstChunk();
+ diffElement.removeEventListener('render', setupDone);
+ done();
+ };
+ diffElement.addEventListener('render', setupDone);
+
+ restAPI.getDiffPreferences().then(prefs => {
+ diffElement.prefs = prefs;
+ diffElement.diff = mockDiffResponse.diffResponse;
+ });
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('diff cursor functionality (side-by-side)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursorElement.diffRow);
+
+ const firstDeltaRow = diffElement.shadowRoot
+ .querySelector('.section.delta .diff-row');
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+
+ cursorElement.moveDown();
+
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+ assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
+
+ cursorElement.moveUp();
+
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+ });
+
+ test('moveToLastChunk', () => {
+ const chunks = Array.from(dom(diffElement.root).querySelectorAll(
+ '.section.delta'));
+ assert.isAbove(chunks.length, 1);
+ assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0);
+
+ cursorElement.moveToLastChunk();
+
+ assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement),
+ chunks.length - 1);
+ });
+
+ test('cursor scroll behavior', () => {
+ cursorElement._handleDiffRenderStart();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+ assert.isTrue(cursorElement._focusOnMove);
+
+ cursorElement._handleWindowScroll();
+ assert.equal(cursorElement._scrollBehavior, 'never');
+ assert.isFalse(cursorElement._focusOnMove);
+
+ cursorElement.handleDiffUpdate();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+ assert.isTrue(cursorElement._focusOnMove);
+ });
+
+ suite('unified diff', () => {
setup(done => {
- sandbox = sinon.sandbox.create();
-
- const fixtureElems = fixture('basic');
- mockDiffResponse = fixtureElems[0];
- diffElement = fixtureElems[1];
- cursorElement = fixtureElems[2];
- const restAPI = fixtureElems[3];
-
- // Register the diff with the cursor.
- cursorElement.push('diffs', diffElement);
-
- diffElement.loggedIn = false;
- diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
- diffElement.comments = {
- left: [],
- right: [],
- meta: {patchRange: undefined},
- };
- const setupDone = () => {
- cursorElement._updateStops();
- cursorElement.moveToFirstChunk();
- diffElement.removeEventListener('render', setupDone);
+ // We must allow the diff to re-render after setting the viewMode.
+ const renderHandler = function() {
+ diffElement.removeEventListener('render', renderHandler);
+ cursorElement.reInitCursor();
done();
};
- diffElement.addEventListener('render', setupDone);
-
- restAPI.getDiffPreferences().then(prefs => {
- diffElement.prefs = prefs;
- diffElement.diff = mockDiffResponse.diffResponse;
- });
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.viewMode = 'UNIFIED_DIFF';
});
- teardown(() => sandbox.restore());
-
- test('diff cursor functionality (side-by-side)', () => {
+ test('diff cursor functionality (unified)', () => {
// The cursor has been initialized to the first delta.
assert.isOk(cursorElement.diffRow);
- const firstDeltaRow = diffElement.shadowRoot
+ let firstDeltaRow = diffElement.shadowRoot
+ .querySelector('.section.delta .diff-row');
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+
+ firstDeltaRow = diffElement.shadowRoot
.querySelector('.section.delta .diff-row');
assert.equal(cursorElement.diffRow, firstDeltaRow);
@@ -110,309 +187,248 @@
assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
assert.equal(cursorElement.diffRow, firstDeltaRow);
});
+ });
- test('moveToLastChunk', () => {
- const chunks = Array.from(Polymer.dom(diffElement.root).querySelectorAll(
- '.section.delta'));
- assert.isAbove(chunks.length, 1);
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0);
+ test('cursor side functionality', () => {
+ // The side only applies to side-by-side mode, which should be the default
+ // mode.
+ assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
- cursorElement.moveToLastChunk();
+ const firstDeltaSection = diffElement.shadowRoot
+ .querySelector('.section.delta');
+ const firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
- assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement),
- chunks.length - 1);
- });
+ // Because the first delta in this diff is on the right, it should be set
+ // to the right side.
+ assert.equal(cursorElement.side, 'right');
+ assert.equal(cursorElement.diffRow, firstDeltaRow);
+ const firstIndex = cursorElement.$.cursorManager.index;
- test('cursor scroll behavior', () => {
- cursorElement._handleDiffRenderStart();
+ // Move the side to the left. Because this delta only has a right side, we
+ // should be moved up to the previous line where there is content on the
+ // right. The previous row is part of the previous section.
+ cursorElement.moveLeft();
+
+ assert.equal(cursorElement.side, 'left');
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+ assert.equal(cursorElement.$.cursorManager.index, firstIndex - 1);
+ assert.equal(cursorElement.diffRow.parentElement,
+ firstDeltaSection.previousSibling);
+
+ // If we move down, we should skip everything in the first delta because
+ // we are on the left side and the first delta has no content on the left.
+ cursorElement.moveDown();
+
+ assert.equal(cursorElement.side, 'left');
+ assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+ assert.isTrue(cursorElement.$.cursorManager.index > firstIndex);
+ assert.equal(cursorElement.diffRow.parentElement,
+ firstDeltaSection.nextSibling);
+ });
+
+ test('chunk skip functionality', () => {
+ const chunks = dom(diffElement.root).querySelectorAll(
+ '.section.delta');
+ const indexOfChunk = function(chunk) {
+ return Array.prototype.indexOf.call(chunks, chunk);
+ };
+
+ // We should be initialized to the first chunk. Since this chunk only has
+ // content on the right side, our side should be right.
+ let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+ assert.equal(currentIndex, 0);
+ assert.equal(cursorElement.side, 'right');
+
+ // Move to the next chunk.
+ cursorElement.moveToNextChunk();
+
+ // Since this chunk only has content on the left side. we should have been
+ // automatically mvoed over.
+ const previousIndex = currentIndex;
+ currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
+ assert.equal(currentIndex, previousIndex + 1);
+ assert.equal(cursorElement.side, 'left');
+ });
+
+ test('initialLineNumber not provided', done => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
+ const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
+ () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+
+ function renderHandler() {
+ diffElement.removeEventListener('render', renderHandler);
+ assert.isFalse(moveToNumStub.called);
+ assert.isTrue(moveToChunkStub.called);
+ assert.equal(scrollBehaviorDuringMove, 'never');
assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- assert.isTrue(cursorElement._focusOnMove);
+ done();
+ }
+ diffElement.addEventListener('render', renderHandler);
+ diffElement._diffChanged(mockDiffResponse.diffResponse);
+ });
- cursorElement._handleWindowScroll();
- assert.equal(cursorElement._scrollBehavior, 'never');
- assert.isFalse(cursorElement._focusOnMove);
-
- cursorElement.handleDiffUpdate();
+ test('initialLineNumber provided', done => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
+ () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+ const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
+ function renderHandler() {
+ diffElement.removeEventListener('render', renderHandler);
+ assert.isFalse(moveToChunkStub.called);
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 10);
+ assert.equal(moveToNumStub.lastCall.args[1], 'right');
+ assert.equal(scrollBehaviorDuringMove, 'keep-visible');
assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- assert.isTrue(cursorElement._focusOnMove);
+ done();
+ }
+ diffElement.addEventListener('render', renderHandler);
+ cursorElement.initialLineNumber = 10;
+ cursorElement.side = 'right';
+
+ diffElement._diffChanged(mockDiffResponse.diffResponse);
+ });
+
+ test('getTargetDiffElement', () => {
+ cursorElement.initialLineNumber = 1;
+ assert.isTrue(!!cursorElement.diffRow);
+ assert.equal(
+ cursorElement.getTargetDiffElement(),
+ diffElement
+ );
+ });
+
+ suite('createCommentInPlace', () => {
+ setup(() => {
+ diffElement.loggedIn = true;
});
- suite('unified diff', () => {
- setup(done => {
- // We must allow the diff to re-render after setting the viewMode.
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursorElement.reInitCursor();
- done();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.viewMode = 'UNIFIED_DIFF';
+ test('adds new draft for selected line on the left', done => {
+ cursorElement.moveToLineNumber(2, 'left');
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side, patchNum} = e.detail;
+ assert.equal(lineNum, 2);
+ assert.equal(range, undefined);
+ assert.equal(patchNum, 1);
+ assert.equal(side, 'left');
+ done();
});
+ cursorElement.createCommentInPlace();
+ });
- test('diff cursor functionality (unified)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursorElement.diffRow);
-
- let firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
-
- firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
-
- cursorElement.moveDown();
-
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
-
- cursorElement.moveUp();
-
- assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursorElement.diffRow, firstDeltaRow);
+ test('adds draft for selected line on the right', done => {
+ cursorElement.moveToLineNumber(4, 'right');
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side, patchNum} = e.detail;
+ assert.equal(lineNum, 4);
+ assert.equal(range, undefined);
+ assert.equal(patchNum, 2);
+ assert.equal(side, 'right');
+ done();
});
+ cursorElement.createCommentInPlace();
});
- test('cursor side functionality', () => {
- // The side only applies to side-by-side mode, which should be the default
- // mode.
- assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
-
- const firstDeltaSection = diffElement.shadowRoot
- .querySelector('.section.delta');
- const firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
-
- // Because the first delta in this diff is on the right, it should be set
- // to the right side.
- assert.equal(cursorElement.side, 'right');
- assert.equal(cursorElement.diffRow, firstDeltaRow);
- const firstIndex = cursorElement.$.cursorManager.index;
-
- // Move the side to the left. Because this delta only has a right side, we
- // should be moved up to the previous line where there is content on the
- // right. The previous row is part of the previous section.
- cursorElement.moveLeft();
-
- assert.equal(cursorElement.side, 'left');
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.equal(cursorElement.$.cursorManager.index, firstIndex - 1);
- assert.equal(cursorElement.diffRow.parentElement,
- firstDeltaSection.previousSibling);
-
- // If we move down, we should skip everything in the first delta because
- // we are on the left side and the first delta has no content on the left.
- cursorElement.moveDown();
-
- assert.equal(cursorElement.side, 'left');
- assert.notEqual(cursorElement.diffRow, firstDeltaRow);
- assert.isTrue(cursorElement.$.cursorManager.index > firstIndex);
- assert.equal(cursorElement.diffRow.parentElement,
- firstDeltaSection.nextSibling);
- });
-
- test('chunk skip functionality', () => {
- const chunks = Polymer.dom(diffElement.root).querySelectorAll(
- '.section.delta');
- const indexOfChunk = function(chunk) {
- return Array.prototype.indexOf.call(chunks, chunk);
+ test('createCommentInPlace creates comment for range if selected', done => {
+ const someRange = {
+ start_line: 2,
+ start_character: 3,
+ end_line: 6,
+ end_character: 1,
};
-
- // We should be initialized to the first chunk. Since this chunk only has
- // content on the right side, our side should be right.
- let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
- assert.equal(currentIndex, 0);
- assert.equal(cursorElement.side, 'right');
-
- // Move to the next chunk.
- cursorElement.moveToNextChunk();
-
- // Since this chunk only has content on the left side. we should have been
- // automatically mvoed over.
- const previousIndex = currentIndex;
- currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
- assert.equal(currentIndex, previousIndex + 1);
- assert.equal(cursorElement.side, 'left');
- });
-
- test('initialLineNumber not provided', done => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
- const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
- () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
-
- function renderHandler() {
- diffElement.removeEventListener('render', renderHandler);
- assert.isFalse(moveToNumStub.called);
- assert.isTrue(moveToChunkStub.called);
- assert.equal(scrollBehaviorDuringMove, 'never');
- assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- done();
- }
- diffElement.addEventListener('render', renderHandler);
- diffElement._diffChanged(mockDiffResponse.diffResponse);
- });
-
- test('initialLineNumber provided', done => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
- () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
- const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
- function renderHandler() {
- diffElement.removeEventListener('render', renderHandler);
- assert.isFalse(moveToChunkStub.called);
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], 10);
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(scrollBehaviorDuringMove, 'keep-visible');
- assert.equal(cursorElement._scrollBehavior, 'keep-visible');
- done();
- }
- diffElement.addEventListener('render', renderHandler);
- cursorElement.initialLineNumber = 10;
- cursorElement.side = 'right';
-
- diffElement._diffChanged(mockDiffResponse.diffResponse);
- });
-
- test('getTargetDiffElement', () => {
- cursorElement.initialLineNumber = 1;
- assert.isTrue(!!cursorElement.diffRow);
- assert.equal(
- cursorElement.getTargetDiffElement(),
- diffElement
- );
- });
-
- suite('createCommentInPlace', () => {
- setup(() => {
- diffElement.loggedIn = true;
- });
-
- test('adds new draft for selected line on the left', done => {
- cursorElement.moveToLineNumber(2, 'left');
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side, patchNum} = e.detail;
- assert.equal(lineNum, 2);
- assert.equal(range, undefined);
- assert.equal(patchNum, 1);
- assert.equal(side, 'left');
- done();
- });
- cursorElement.createCommentInPlace();
- });
-
- test('adds draft for selected line on the right', done => {
- cursorElement.moveToLineNumber(4, 'right');
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side, patchNum} = e.detail;
- assert.equal(lineNum, 4);
- assert.equal(range, undefined);
- assert.equal(patchNum, 2);
- assert.equal(side, 'right');
- done();
- });
- cursorElement.createCommentInPlace();
- });
-
- test('createCommentInPlace creates comment for range if selected', done => {
- const someRange = {
- start_line: 2,
- start_character: 3,
- end_line: 6,
- end_character: 1,
- };
- diffElement.$.highlights.selectedRange = {
- side: 'right',
- range: someRange,
- };
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side, patchNum} = e.detail;
- assert.equal(lineNum, 6);
- assert.equal(range, someRange);
- assert.equal(patchNum, 2);
- assert.equal(side, 'right');
- done();
- });
- cursorElement.createCommentInPlace();
- });
-
- test('createCommentInPlace ignores call if nothing is selected', () => {
- const createRangeCommentStub = sandbox.stub(diffElement,
- 'createRangeComment');
- const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine');
- cursorElement.diffRow = undefined;
- cursorElement.createCommentInPlace();
- assert.isFalse(createRangeCommentStub.called);
- assert.isFalse(addDraftAtLineStub.called);
- });
- });
-
- test('getAddress', () => {
- // It should initialize to the first chunk: line 5 of the revision.
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: false, number: 5});
-
- // Revision line 4 is up.
- cursorElement.moveUp();
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: false, number: 4});
-
- // Base line 4 is left.
- cursorElement.moveLeft();
- assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4});
-
- // Moving to the next chunk takes it back to the start.
- cursorElement.moveToNextChunk();
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: false, number: 5});
-
- // The following chunk is a removal starting on line 10 of the base.
- cursorElement.moveToNextChunk();
- assert.deepEqual(cursorElement.getAddress(),
- {leftSide: true, number: 10});
-
- // Should be null if there is no selection.
- cursorElement.$.cursorManager.unsetCursor();
- assert.isNotOk(cursorElement.getAddress());
- });
-
- test('_findRowByNumberAndFile', () => {
- // Get the first ab row after the first chunk.
- const row = Polymer.dom(diffElement.root).querySelectorAll('tr')[8];
-
- // It should be line 8 on the right, but line 5 on the left.
- assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row);
- assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row);
- });
-
- test('expand context updates stops', done => {
- sandbox.spy(cursorElement, 'handleDiffUpdate');
- MockInteractions.tap(diffElement.shadowRoot
- .querySelector('.showContext'));
- flush(() => {
- assert.isTrue(cursorElement.handleDiffUpdate.called);
+ diffElement.$.highlights.selectedRange = {
+ side: 'right',
+ range: someRange,
+ };
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side, patchNum} = e.detail;
+ assert.equal(lineNum, 6);
+ assert.equal(range, someRange);
+ assert.equal(patchNum, 2);
+ assert.equal(side, 'right');
done();
});
+ cursorElement.createCommentInPlace();
});
- suite('gr-diff-cursor event tests', () => {
- let sandbox;
- let someEmptyDiv;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- someEmptyDiv = fixture('empty');
- });
-
- teardown(() => sandbox.restore());
-
- test('ready is fired after component is rendered', done => {
- const cursorElement = document.createElement('gr-diff-cursor');
- cursorElement.addEventListener('ready', () => {
- done();
- });
- someEmptyDiv.appendChild(cursorElement);
- });
+ test('createCommentInPlace ignores call if nothing is selected', () => {
+ const createRangeCommentStub = sandbox.stub(diffElement,
+ 'createRangeComment');
+ const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine');
+ cursorElement.diffRow = undefined;
+ cursorElement.createCommentInPlace();
+ assert.isFalse(createRangeCommentStub.called);
+ assert.isFalse(addDraftAtLineStub.called);
});
});
+
+ test('getAddress', () => {
+ // It should initialize to the first chunk: line 5 of the revision.
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: false, number: 5});
+
+ // Revision line 4 is up.
+ cursorElement.moveUp();
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: false, number: 4});
+
+ // Base line 4 is left.
+ cursorElement.moveLeft();
+ assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4});
+
+ // Moving to the next chunk takes it back to the start.
+ cursorElement.moveToNextChunk();
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: false, number: 5});
+
+ // The following chunk is a removal starting on line 10 of the base.
+ cursorElement.moveToNextChunk();
+ assert.deepEqual(cursorElement.getAddress(),
+ {leftSide: true, number: 10});
+
+ // Should be null if there is no selection.
+ cursorElement.$.cursorManager.unsetCursor();
+ assert.isNotOk(cursorElement.getAddress());
+ });
+
+ test('_findRowByNumberAndFile', () => {
+ // Get the first ab row after the first chunk.
+ const row = dom(diffElement.root).querySelectorAll('tr')[8];
+
+ // It should be line 8 on the right, but line 5 on the left.
+ assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row);
+ assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row);
+ });
+
+ test('expand context updates stops', done => {
+ sandbox.spy(cursorElement, 'handleDiffUpdate');
+ MockInteractions.tap(diffElement.shadowRoot
+ .querySelector('.showContext'));
+ flush(() => {
+ assert.isTrue(cursorElement.handleDiffUpdate.called);
+ done();
+ });
+ });
+
+ suite('gr-diff-cursor event tests', () => {
+ let sandbox;
+ let someEmptyDiv;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ someEmptyDiv = fixture('empty');
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('ready is fired after component is rendered', done => {
+ const cursorElement = document.createElement('gr-diff-cursor');
+ cursorElement.addEventListener('ready', () => {
+ done();
+ });
+ someEmptyDiv.appendChild(cursorElement);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
index 79e4036..6db0836 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-annotation</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="gr-annotation.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 type="module" src="./gr-annotation.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-annotation.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,266 +41,269 @@
</template>
</test-fixture>
-<script>
- suite('annotation', async () => {
- await readyToTest();
- let str;
- let parent;
- let textNode;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-annotation.js';
+import {sanitizeDOMValue, setSanitizeDOMValue} from '@polymer/polymer/lib/utils/settings.js';
+suite('annotation', () => {
+ let str;
+ let parent;
+ let textNode;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ parent = fixture('basic');
+ textNode = parent.childNodes[0];
+ str = textNode.textContent;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_annotateText Case 1', () => {
+ GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 1);
+ assert.instanceOf(parent.childNodes[0], HTMLElement);
+ assert.equal(parent.childNodes[0].className, 'foobar');
+ assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+ assert.equal(parent.childNodes[0].childNodes[0].textContent, str);
+ });
+
+ test('_annotateText Case 2', () => {
+ const length = 12;
+ const substr = str.substr(0, length);
+ const remainder = str.substr(length);
+
+ GrAnnotation._annotateText(textNode, 0, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 2);
+
+ assert.instanceOf(parent.childNodes[0], HTMLElement);
+ assert.equal(parent.childNodes[0].className, 'foobar');
+ assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+ assert.equal(parent.childNodes[0].childNodes[0].textContent, substr);
+
+ assert.instanceOf(parent.childNodes[1], Text);
+ assert.equal(parent.childNodes[1].textContent, remainder);
+ });
+
+ test('_annotateText Case 3', () => {
+ const index = 12;
+ const length = str.length - index;
+ const remainder = str.substr(0, index);
+ const substr = str.substr(index);
+
+ GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 2);
+
+ assert.instanceOf(parent.childNodes[0], Text);
+ assert.equal(parent.childNodes[0].textContent, remainder);
+
+ assert.instanceOf(parent.childNodes[1], HTMLElement);
+ assert.equal(parent.childNodes[1].className, 'foobar');
+ assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+ assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+ });
+
+ test('_annotateText Case 4', () => {
+ const index = str.indexOf('dolor');
+ const length = 'dolor '.length;
+
+ const remainderPre = str.substr(0, index);
+ const substr = str.substr(index, length);
+ const remainderPost = str.substr(index + length);
+
+ GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 3);
+
+ assert.instanceOf(parent.childNodes[0], Text);
+ assert.equal(parent.childNodes[0].textContent, remainderPre);
+
+ assert.instanceOf(parent.childNodes[1], HTMLElement);
+ assert.equal(parent.childNodes[1].className, 'foobar');
+ assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+ assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+
+ assert.instanceOf(parent.childNodes[2], Text);
+ assert.equal(parent.childNodes[2].textContent, remainderPost);
+ });
+
+ test('_annotateElement design doc example', () => {
+ const layers = [
+ 'amet, ',
+ 'inceptos ',
+ 'amet, ',
+ 'et, suspendisse ince',
+ ];
+
+ // Apply the layers successively.
+ layers.forEach((layer, i) => {
+ GrAnnotation.annotateElement(
+ parent, str.indexOf(layer), layer.length, `layer-${i + 1}`);
+ });
+
+ assert.equal(parent.textContent, str);
+
+ // Layer 1:
+ const layer1 = parent.querySelectorAll('.layer-1');
+ assert.equal(layer1.length, 1);
+ assert.equal(layer1[0].textContent, layers[0]);
+ assert.equal(layer1[0].parentElement, parent);
+
+ // Layer 2:
+ const layer2 = parent.querySelectorAll('.layer-2');
+ assert.equal(layer2.length, 1);
+ assert.equal(layer2[0].textContent, layers[1]);
+ assert.equal(layer2[0].parentElement, parent);
+
+ // Layer 3:
+ const layer3 = parent.querySelectorAll('.layer-3');
+ assert.equal(layer3.length, 1);
+ assert.equal(layer3[0].textContent, layers[2]);
+ assert.equal(layer3[0].parentElement, layer1[0]);
+
+ // Layer 4:
+ const layer4 = parent.querySelectorAll('.layer-4');
+ assert.equal(layer4.length, 3);
+
+ assert.equal(layer4[0].textContent, 'et, ');
+ assert.equal(layer4[0].parentElement, layer3[0]);
+
+ assert.equal(layer4[1].textContent, 'suspendisse ');
+ assert.equal(layer4[1].parentElement, parent);
+
+ assert.equal(layer4[2].textContent, 'ince');
+ assert.equal(layer4[2].parentElement, layer2[0]);
+
+ assert.equal(layer4[0].textContent +
+ layer4[1].textContent +
+ layer4[2].textContent,
+ layers[3]);
+ });
+
+ test('splitTextNode', () => {
+ const helloString = 'hello';
+ const asciiString = 'ASCII';
+ const unicodeString = 'Unic💢de';
+
+ let node;
+ let tail;
+
+ // Non-unicode path:
+ node = document.createTextNode(helloString + asciiString);
+ tail = GrAnnotation.splitTextNode(node, helloString.length);
+ assert(node.textContent, helloString);
+ assert(tail.textContent, asciiString);
+
+ // Unicdoe path:
+ node = document.createTextNode(helloString + unicodeString);
+ tail = GrAnnotation.splitTextNode(node, helloString.length);
+ assert(node.textContent, helloString);
+ assert(tail.textContent, unicodeString);
+ });
+
+ suite('annotateWithElement', () => {
+ const fullText = '01234567890123456789';
+ let mockSanitize;
+ let originalSanitizeDOMValue;
setup(() => {
- sandbox = sinon.sandbox.create();
- parent = fixture('basic');
- textNode = parent.childNodes[0];
- str = textNode.textContent;
+ originalSanitizeDOMValue = sanitizeDOMValue;
+ assert.isDefined(originalSanitizeDOMValue);
+ mockSanitize = sandbox.spy(originalSanitizeDOMValue);
+ setSanitizeDOMValue(mockSanitize);
});
teardown(() => {
- sandbox.restore();
+ setSanitizeDOMValue(originalSanitizeDOMValue);
});
- test('_annotateText Case 1', () => {
- GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
+ test('annotates when fully contained', () => {
+ const length = 10;
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ GrAnnotation.annotateWithElement(
+ container, 1, length, {tagName: 'test-wrapper'});
- assert.equal(parent.childNodes.length, 1);
- assert.instanceOf(parent.childNodes[0], HTMLElement);
- assert.equal(parent.childNodes[0].className, 'foobar');
- assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
- assert.equal(parent.childNodes[0].childNodes[0].textContent, str);
+ assert.equal(
+ container.innerHTML,
+ '0<test-wrapper>1234567890</test-wrapper>123456789');
});
- test('_annotateText Case 2', () => {
- const length = 12;
- const substr = str.substr(0, length);
- const remainder = str.substr(length);
+ test('annotates when spanning multiple nodes', () => {
+ const length = 10;
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ GrAnnotation.annotateElement(container, 5, length, 'testclass');
+ GrAnnotation.annotateWithElement(
+ container, 1, length, {tagName: 'test-wrapper'});
- GrAnnotation._annotateText(textNode, 0, length, 'foobar');
-
- assert.equal(parent.childNodes.length, 2);
-
- assert.instanceOf(parent.childNodes[0], HTMLElement);
- assert.equal(parent.childNodes[0].className, 'foobar');
- assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
- assert.equal(parent.childNodes[0].childNodes[0].textContent, substr);
-
- assert.instanceOf(parent.childNodes[1], Text);
- assert.equal(parent.childNodes[1].textContent, remainder);
+ assert.equal(
+ container.innerHTML,
+ '0' +
+ '<test-wrapper>' +
+ '1234' +
+ '<hl class="testclass">567890</hl>' +
+ '</test-wrapper>' +
+ '<hl class="testclass">1234</hl>' +
+ '56789');
});
- test('_annotateText Case 3', () => {
- const index = 12;
- const length = str.length - index;
- const remainder = str.substr(0, index);
- const substr = str.substr(index);
+ test('annotates text node', () => {
+ const length = 10;
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ GrAnnotation.annotateWithElement(
+ container.childNodes[0], 1, length, {tagName: 'test-wrapper'});
- GrAnnotation._annotateText(textNode, index, length, 'foobar');
-
- assert.equal(parent.childNodes.length, 2);
-
- assert.instanceOf(parent.childNodes[0], Text);
- assert.equal(parent.childNodes[0].textContent, remainder);
-
- assert.instanceOf(parent.childNodes[1], HTMLElement);
- assert.equal(parent.childNodes[1].className, 'foobar');
- assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
- assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+ assert.equal(
+ container.innerHTML,
+ '0<test-wrapper>1234567890</test-wrapper>123456789');
});
- test('_annotateText Case 4', () => {
- const index = str.indexOf('dolor');
- const length = 'dolor '.length;
+ test('handles zero-length nodes', () => {
+ const container = document.createElement('div');
+ container.appendChild(document.createTextNode('0123456789'));
+ container.appendChild(document.createElement('span'));
+ container.appendChild(document.createTextNode('0123456789'));
+ GrAnnotation.annotateWithElement(
+ container, 1, 10, {tagName: 'test-wrapper'});
- const remainderPre = str.substr(0, index);
- const substr = str.substr(index, length);
- const remainderPost = str.substr(index + length);
-
- GrAnnotation._annotateText(textNode, index, length, 'foobar');
-
- assert.equal(parent.childNodes.length, 3);
-
- assert.instanceOf(parent.childNodes[0], Text);
- assert.equal(parent.childNodes[0].textContent, remainderPre);
-
- assert.instanceOf(parent.childNodes[1], HTMLElement);
- assert.equal(parent.childNodes[1].className, 'foobar');
- assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
- assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
-
- assert.instanceOf(parent.childNodes[2], Text);
- assert.equal(parent.childNodes[2].textContent, remainderPost);
+ assert.equal(
+ container.innerHTML,
+ '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789');
});
- test('_annotateElement design doc example', () => {
- const layers = [
- 'amet, ',
- 'inceptos ',
- 'amet, ',
- 'et, suspendisse ince',
- ];
-
- // Apply the layers successively.
- layers.forEach((layer, i) => {
- GrAnnotation.annotateElement(
- parent, str.indexOf(layer), layer.length, `layer-${i + 1}`);
- });
-
- assert.equal(parent.textContent, str);
-
- // Layer 1:
- const layer1 = parent.querySelectorAll('.layer-1');
- assert.equal(layer1.length, 1);
- assert.equal(layer1[0].textContent, layers[0]);
- assert.equal(layer1[0].parentElement, parent);
-
- // Layer 2:
- const layer2 = parent.querySelectorAll('.layer-2');
- assert.equal(layer2.length, 1);
- assert.equal(layer2[0].textContent, layers[1]);
- assert.equal(layer2[0].parentElement, parent);
-
- // Layer 3:
- const layer3 = parent.querySelectorAll('.layer-3');
- assert.equal(layer3.length, 1);
- assert.equal(layer3[0].textContent, layers[2]);
- assert.equal(layer3[0].parentElement, layer1[0]);
-
- // Layer 4:
- const layer4 = parent.querySelectorAll('.layer-4');
- assert.equal(layer4.length, 3);
-
- assert.equal(layer4[0].textContent, 'et, ');
- assert.equal(layer4[0].parentElement, layer3[0]);
-
- assert.equal(layer4[1].textContent, 'suspendisse ');
- assert.equal(layer4[1].parentElement, parent);
-
- assert.equal(layer4[2].textContent, 'ince');
- assert.equal(layer4[2].parentElement, layer2[0]);
-
- assert.equal(layer4[0].textContent +
- layer4[1].textContent +
- layer4[2].textContent,
- layers[3]);
- });
-
- test('splitTextNode', () => {
- const helloString = 'hello';
- const asciiString = 'ASCII';
- const unicodeString = 'Unic💢de';
-
- let node;
- let tail;
-
- // Non-unicode path:
- node = document.createTextNode(helloString + asciiString);
- tail = GrAnnotation.splitTextNode(node, helloString.length);
- assert(node.textContent, helloString);
- assert(tail.textContent, asciiString);
-
- // Unicdoe path:
- node = document.createTextNode(helloString + unicodeString);
- tail = GrAnnotation.splitTextNode(node, helloString.length);
- assert(node.textContent, helloString);
- assert(tail.textContent, unicodeString);
- });
-
- suite('annotateWithElement', () => {
- const fullText = '01234567890123456789';
- let mockSanitize;
- let originalSanitizeDOMValue;
-
- setup(() => {
- originalSanitizeDOMValue = window.Polymer.sanitizeDOMValue;
- assert.isDefined(originalSanitizeDOMValue);
- mockSanitize = sandbox.spy(originalSanitizeDOMValue);
- window.Polymer.sanitizeDOMValue = mockSanitize;
- });
-
- teardown(() => {
- window.Polymer.sanitizeDOMValue = originalSanitizeDOMValue;
- });
-
- test('annotates when fully contained', () => {
- const length = 10;
- const container = document.createElement('div');
- container.textContent = fullText;
- GrAnnotation.annotateWithElement(
- container, 1, length, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0<test-wrapper>1234567890</test-wrapper>123456789');
- });
-
- test('annotates when spanning multiple nodes', () => {
- const length = 10;
- const container = document.createElement('div');
- container.textContent = fullText;
- GrAnnotation.annotateElement(container, 5, length, 'testclass');
- GrAnnotation.annotateWithElement(
- container, 1, length, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0' +
- '<test-wrapper>' +
- '1234' +
- '<hl class="testclass">567890</hl>' +
- '</test-wrapper>' +
- '<hl class="testclass">1234</hl>' +
- '56789');
- });
-
- test('annotates text node', () => {
- const length = 10;
- const container = document.createElement('div');
- container.textContent = fullText;
- GrAnnotation.annotateWithElement(
- container.childNodes[0], 1, length, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0<test-wrapper>1234567890</test-wrapper>123456789');
- });
-
- test('handles zero-length nodes', () => {
- const container = document.createElement('div');
- container.appendChild(document.createTextNode('0123456789'));
- container.appendChild(document.createElement('span'));
- container.appendChild(document.createTextNode('0123456789'));
- GrAnnotation.annotateWithElement(
- container, 1, 10, {tagName: 'test-wrapper'});
-
- assert.equal(
- container.innerHTML,
- '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789');
- });
-
- test('sets sanitized attributes', () => {
- const container = document.createElement('div');
- container.textContent = fullText;
- const attributes = {
- 'href': 'foo',
- 'data-foo': 'bar',
- 'class': 'hello world',
- };
- GrAnnotation.annotateWithElement(
- container, 1, length, {tagName: 'test-wrapper', attributes});
- assert(mockSanitize.calledWith(
- 'foo', 'href', 'attribute', sinon.match.instanceOf(Element)));
- assert(mockSanitize.calledWith(
- 'bar', 'data-foo', 'attribute', sinon.match.instanceOf(Element)));
- assert(mockSanitize.calledWith(
- 'hello world',
- 'class',
- 'attribute',
- sinon.match.instanceOf(Element)));
- const el = container.querySelector('test-wrapper');
- assert.equal(el.getAttribute('href'), 'foo');
- assert.equal(el.getAttribute('data-foo'), 'bar');
- assert.equal(el.getAttribute('class'), 'hello world');
- });
+ test('sets sanitized attributes', () => {
+ const container = document.createElement('div');
+ container.textContent = fullText;
+ const attributes = {
+ 'href': 'foo',
+ 'data-foo': 'bar',
+ 'class': 'hello world',
+ };
+ GrAnnotation.annotateWithElement(
+ container, 1, length, {tagName: 'test-wrapper', attributes});
+ assert(mockSanitize.calledWith(
+ 'foo', 'href', 'attribute', sinon.match.instanceOf(Element)));
+ assert(mockSanitize.calledWith(
+ 'bar', 'data-foo', 'attribute', sinon.match.instanceOf(Element)));
+ assert(mockSanitize.calledWith(
+ 'hello world',
+ 'class',
+ 'attribute',
+ sinon.match.instanceOf(Element)));
+ const el = container.querySelector('test-wrapper');
+ assert.equal(el.getAttribute('href'), 'foo');
+ assert.equal(el.getAttribute('data-foo'), 'bar');
+ assert.equal(el.getAttribute('class'), 'hello world');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 99bf1c8..4567c9e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -14,484 +14,496 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-selection-action-box/gr-selection-action-box.js';
+import './gr-annotation.js';
+import './gr-range-normalizer.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-diff-highlight_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrDiffHighlight extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-highlight'; }
+
+ static get properties() {
+ return {
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ commentRanges: {
+ type: Array,
+ notify: true,
+ },
+ loggedIn: Boolean,
+ /**
+ * querySelector can return null, so needs to be nullable.
+ *
+ * @type {?HTMLElement}
+ * */
+ _cachedDiffBuilder: Object,
+
+ /**
+ * Which range is currently selected by the user.
+ * Stored in order to add a range-based comment
+ * later.
+ * undefined if no range is selected.
+ *
+ * @type {{side: string, range: Gerrit.Range}|undefined}
+ */
+ selectedRange: {
+ type: Object,
+ notify: true,
+ },
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('comment-thread-mouseleave',
+ e => this._handleCommentThreadMouseleave(e));
+ this.addEventListener('comment-thread-mouseenter',
+ e => this._handleCommentThreadMouseenter(e));
+ this.addEventListener('create-comment-requested',
+ e => this._handleRangeCommentRequest(e));
+ }
+
+ get diffBuilder() {
+ if (!this._cachedDiffBuilder) {
+ this._cachedDiffBuilder =
+ dom(this).querySelector('gr-diff-builder');
+ }
+ return this._cachedDiffBuilder;
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Determines side/line/range for a DOM selection and shows a tooltip.
+ *
+ * With native shadow DOM, gr-diff-highlight cannot access a selection that
+ * references the DOM elements making up the diff because they are in the
+ * shadow DOM the gr-diff element. For this reason, we listen to the
+ * selectionchange event and retrieve the selection in gr-diff, and then
+ * call this method to process the Selection.
+ *
+ * @param {Selection} selection A DOM Selection living in the shadow DOM of
+ * the diff element.
+ * @param {boolean} isMouseUp If true, this is called due to a mouseup
+ * event, in which case we might want to immediately create a comment,
+ * because isMouseUp === true combined with an existing selection must
+ * mean that this is the end of a double-click.
*/
- class GrDiffHighlight extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-highlight'; }
+ handleSelectionChange(selection, isMouseUp) {
+ // Debounce is not just nice for waiting until the selection has settled,
+ // it is also vital for being able to click on the action box before it is
+ // removed.
+ // If you wait longer than 50 ms, then you don't properly catch a very
+ // quick 'c' press after the selection change. If you wait less than 10
+ // ms, then you will have about 50 _handleSelection calls when doing a
+ // simple drag for select.
+ this.debounce(
+ 'selectionChange', () => this._handleSelection(selection, isMouseUp),
+ 10);
+ }
- static get properties() {
- return {
- /** @type {!Array<!Gerrit.HoveredRange>} */
- commentRanges: {
- type: Array,
- notify: true,
- },
- loggedIn: Boolean,
- /**
- * querySelector can return null, so needs to be nullable.
- *
- * @type {?HTMLElement}
- * */
- _cachedDiffBuilder: Object,
-
- /**
- * Which range is currently selected by the user.
- * Stored in order to add a range-based comment
- * later.
- * undefined if no range is selected.
- *
- * @type {{side: string, range: Gerrit.Range}|undefined}
- */
- selectedRange: {
- type: Object,
- notify: true,
- },
- };
+ _getThreadEl(e) {
+ const path = dom(e).path || [];
+ for (const pathEl of path) {
+ if (pathEl.classList.contains('comment-thread')) return pathEl;
}
+ return null;
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('comment-thread-mouseleave',
- e => this._handleCommentThreadMouseleave(e));
- this.addEventListener('comment-thread-mouseenter',
- e => this._handleCommentThreadMouseenter(e));
- this.addEventListener('create-comment-requested',
- e => this._handleRangeCommentRequest(e));
- }
+ _handleCommentThreadMouseenter(e) {
+ const threadEl = this._getThreadEl(e);
+ const index = this._indexForThreadEl(threadEl);
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder =
- Polymer.dom(this).querySelector('gr-diff-builder');
- }
- return this._cachedDiffBuilder;
- }
-
- /**
- * Determines side/line/range for a DOM selection and shows a tooltip.
- *
- * With native shadow DOM, gr-diff-highlight cannot access a selection that
- * references the DOM elements making up the diff because they are in the
- * shadow DOM the gr-diff element. For this reason, we listen to the
- * selectionchange event and retrieve the selection in gr-diff, and then
- * call this method to process the Selection.
- *
- * @param {Selection} selection A DOM Selection living in the shadow DOM of
- * the diff element.
- * @param {boolean} isMouseUp If true, this is called due to a mouseup
- * event, in which case we might want to immediately create a comment,
- * because isMouseUp === true combined with an existing selection must
- * mean that this is the end of a double-click.
- */
- handleSelectionChange(selection, isMouseUp) {
- // Debounce is not just nice for waiting until the selection has settled,
- // it is also vital for being able to click on the action box before it is
- // removed.
- // If you wait longer than 50 ms, then you don't properly catch a very
- // quick 'c' press after the selection change. If you wait less than 10
- // ms, then you will have about 50 _handleSelection calls when doing a
- // simple drag for select.
- this.debounce(
- 'selectionChange', () => this._handleSelection(selection, isMouseUp),
- 10);
- }
-
- _getThreadEl(e) {
- const path = Polymer.dom(e).path || [];
- for (const pathEl of path) {
- if (pathEl.classList.contains('comment-thread')) return pathEl;
- }
- return null;
- }
-
- _handleCommentThreadMouseenter(e) {
- const threadEl = this._getThreadEl(e);
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], true);
- }
- }
-
- _handleCommentThreadMouseleave(e) {
- const threadEl = this._getThreadEl(e);
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], false);
- }
- }
-
- _indexForThreadEl(threadEl) {
- const side = threadEl.getAttribute('comment-side');
- const range = JSON.parse(threadEl.getAttribute('range'));
-
- if (!range) return undefined;
-
- return this._indexOfCommentRange(side, range);
- }
-
- _indexOfCommentRange(side, range) {
- function rangesEqual(a, b) {
- if (!a && !b) {
- return true;
- }
- if (!a || !b) {
- return false;
- }
- return a.start_line === b.start_line &&
- a.start_character === b.start_character &&
- a.end_line === b.end_line &&
- a.end_character === b.end_character;
- }
-
- return this.commentRanges.findIndex(commentRange =>
- commentRange.side === side && rangesEqual(commentRange.range, range));
- }
-
- /**
- * Get current normalized selection.
- * Merges multiple ranges, accounts for triple click, accounts for
- * syntax highligh, convert native DOM Range objects to Gerrit concepts
- * (line, side, etc).
- *
- * @param {Selection} selection
- * @return {({
- * start: {
- * node: Node,
- * side: string,
- * line: Number,
- * column: Number
- * },
- * end: {
- * node: Node,
- * side: string,
- * line: Number,
- * column: Number
- * }
- * })|null|!Object}
- */
- _getNormalizedRange(selection) {
- const rangeCount = selection.rangeCount;
- if (rangeCount === 0) {
- return null;
- } else if (rangeCount === 1) {
- return this._normalizeRange(selection.getRangeAt(0));
- } else {
- const startRange = this._normalizeRange(selection.getRangeAt(0));
- const endRange = this._normalizeRange(
- selection.getRangeAt(rangeCount - 1));
- return {
- start: startRange.start,
- end: endRange.end,
- };
- }
- }
-
- /**
- * Normalize a specific DOM Range.
- *
- * @return {!Object} fixed normalized range
- */
- _normalizeRange(domRange) {
- const range = GrRangeNormalizer.normalize(domRange);
- return this._fixTripleClickSelection({
- start: this._normalizeSelectionSide(
- range.startContainer, range.startOffset),
- end: this._normalizeSelectionSide(
- range.endContainer, range.endOffset),
- }, domRange);
- }
-
- /**
- * Adjust triple click selection for the whole line.
- * A triple click always results in:
- * - start.column == end.column == 0
- * - end.line == start.line + 1
- *
- * @param {!Object} range Normalized range, ie column/line numbers
- * @param {!Range} domRange DOM Range object
- * @return {!Object} fixed normalized range
- */
- _fixTripleClickSelection(range, domRange) {
- if (!range.start) {
- // Selection outside of current diff.
- return range;
- }
- const start = range.start;
- const end = range.end;
- // Happens when triple click in side-by-side mode with other side empty.
- const endsAtOtherEmptySide = !end &&
- domRange.endOffset === 0 &&
- domRange.endContainer.nodeName === 'TD' &&
- (domRange.endContainer.classList.contains('left') ||
- domRange.endContainer.classList.contains('right'));
- const endsAtBeginningOfNextLine = end &&
- start.column === 0 &&
- end.column === 0 &&
- end.line === start.line + 1;
- const content = domRange.cloneContents().querySelector('.contentText');
- const lineLength = content && this._getLength(content) || 0;
- if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) {
- // Move the selection to the end of the previous line.
- range.end = {
- node: start.node,
- column: lineLength,
- side: start.side,
- line: start.line,
- };
- }
- return range;
- }
-
- /**
- * Convert DOM Range selection to concrete numbers (line, column, side).
- * Moves range end if it's not inside td.content.
- * Returns null if selection end is not valid (outside of diff).
- *
- * @param {Node} node td.content child
- * @param {number} offset offset within node
- * @return {({
- * node: Node,
- * side: string,
- * line: Number,
- * column: Number
- * }|undefined)}
- */
- _normalizeSelectionSide(node, offset) {
- let column;
- if (!this.contains(node)) {
- return;
- }
- const lineEl = this.diffBuilder.getLineElByChild(node);
- if (!lineEl) {
- return;
- }
- const side = this.diffBuilder.getSideByLineEl(lineEl);
- if (!side) {
- return;
- }
- const line = this.diffBuilder.getLineNumberByChild(lineEl);
- if (!line) {
- return;
- }
- const contentText = this.diffBuilder.getContentByLineEl(lineEl);
- if (!contentText) {
- return;
- }
- const contentTd = contentText.parentElement;
- if (!contentTd.contains(node)) {
- node = contentText;
- column = 0;
- } else {
- const thread = contentTd.querySelector('.comment-thread');
- if (thread && thread.contains(node)) {
- column = this._getLength(contentText);
- node = contentText;
- } else {
- column = this._convertOffsetToColumn(node, offset);
- }
- }
-
- return {
- node,
- side,
- line,
- column,
- };
- }
-
- /**
- * The only line in which add a comment tooltip is cut off is the first
- * line. Even if there is a collapsed section, The first visible line is
- * in the position where the second line would have been, if not for the
- * collapsed section, so don't need to worry about this case for
- * positioning the tooltip.
- */
- _positionActionBox(actionBox, startLine, range) {
- if (startLine > 1) {
- actionBox.placeAbove(range);
- return;
- }
- actionBox.positionBelow = true;
- actionBox.placeBelow(range);
- }
-
- _isRangeValid(range) {
- if (!range || !range.start || !range.end) {
- return false;
- }
- const start = range.start;
- const end = range.end;
- if (start.side !== end.side ||
- end.line < start.line ||
- (start.line === end.line && start.column === end.column)) {
- return false;
- }
- return true;
- }
-
- _handleSelection(selection, isMouseUp) {
- const normalizedRange = this._getNormalizedRange(selection);
- if (!this._isRangeValid(normalizedRange)) {
- this._removeActionBox();
- return;
- }
- const domRange = selection.getRangeAt(0);
- const start = normalizedRange.start;
- const end = normalizedRange.end;
-
- // TODO (viktard): Drop empty first and last lines from selection.
-
- // If the selection is from the end of one line to the start of the next
- // line, then this must have been a double-click, or you have started
- // dragging. Showing the action box is bad in the former case and not very
- // useful in the latter, so never do that.
- // If this was a mouse-up event, we create a comment immediately if
- // the selection is from the end of a line to the start of the next line.
- // In a perfect world we would only do this for double-click, but it is
- // extremely rare that a user would drag from the end of one line to the
- // start of the next and release the mouse, so we don't bother.
- // TODO(brohlfs): This does not work, if the double-click is before a new
- // diff chunk (start will be equal to end), and neither before an "expand
- // the diff context" block (end line will match the first line of the new
- // section and thus be greater than start line + 1).
- if (start.line === end.line - 1 && end.column === 0) {
- // Rather than trying to find the line contents (for comparing
- // start.column with the content length), we just check if the selection
- // is empty to see that it's at the end of a line.
- const content = domRange.cloneContents().querySelector('.contentText');
- if (isMouseUp && this._getLength(content) === 0) {
- this._fireCreateRangeComment(start.side, {
- start_line: start.line,
- start_character: 0,
- end_line: start.line,
- end_character: start.column,
- });
- }
- return;
- }
-
- let actionBox = this.shadowRoot.querySelector('gr-selection-action-box');
- if (!actionBox) {
- actionBox = document.createElement('gr-selection-action-box');
- const root = Polymer.dom(this.root);
- root.insertBefore(actionBox, root.firstElementChild);
- }
- this.selectedRange = {
- range: {
- start_line: start.line,
- start_character: start.column,
- end_line: end.line,
- end_character: end.column,
- },
- side: start.side,
- };
- if (start.line === end.line) {
- this._positionActionBox(actionBox, start.line, domRange);
- } else if (start.node instanceof Text) {
- if (start.column) {
- this._positionActionBox(actionBox, start.line,
- start.node.splitText(start.column));
- }
- start.node.parentElement.normalize(); // Undo splitText from above.
- } else if (start.node.classList.contains('content') &&
- start.node.firstChild) {
- this._positionActionBox(actionBox, start.line, start.node.firstChild);
- } else {
- this._positionActionBox(actionBox, start.line, start.node);
- }
- }
-
- _fireCreateRangeComment(side, range) {
- this.fire('create-range-comment', {side, range});
- this._removeActionBox();
- }
-
- _handleRangeCommentRequest(e) {
- e.stopPropagation();
- if (!this.selectedRange) {
- throw Error('Selected Range is needed for new range comment!');
- }
- const {side, range} = this.selectedRange;
- this._fireCreateRangeComment(side, range);
- }
-
- _removeActionBox() {
- this.selectedRange = undefined;
- const actionBox = this.shadowRoot
- .querySelector('gr-selection-action-box');
- if (actionBox) {
- Polymer.dom(this.root).removeChild(actionBox);
- }
- }
-
- _convertOffsetToColumn(el, offset) {
- if (el instanceof Element && el.classList.contains('content')) {
- return offset;
- }
- while (el.previousSibling ||
- !el.parentElement.classList.contains('content')) {
- if (el.previousSibling) {
- el = el.previousSibling;
- offset += this._getLength(el);
- } else {
- el = el.parentElement;
- }
- }
- return offset;
- }
-
- /**
- * Traverse Element from right to left, call callback for each node.
- * Stops if callback returns true.
- *
- * @param {!Element} startNode
- * @param {function(Node):boolean} callback
- * @param {Object=} opt_flags If flags.left is true, traverse left.
- */
- _traverseContentSiblings(startNode, callback, opt_flags) {
- const travelLeft = opt_flags && opt_flags.left;
- let node = startNode;
- while (node) {
- if (node instanceof Element &&
- node.tagName !== 'HL' &&
- node.tagName !== 'SPAN') {
- break;
- }
- const nextNode = travelLeft ? node.previousSibling : node.nextSibling;
- if (callback(node)) {
- break;
- }
- node = nextNode;
- }
- }
-
- /**
- * Get length of a node. If the node is a content node, then only give the
- * length of its .contentText child.
- *
- * @param {?Element} node this is sometimes passed as null.
- * @return {number}
- */
- _getLength(node) {
- if (node instanceof Element && node.classList.contains('content')) {
- return this._getLength(node.querySelector('.contentText'));
- } else {
- return GrAnnotation.getLength(node);
- }
+ if (index !== undefined) {
+ this.set(['commentRanges', index, 'hovering'], true);
}
}
- customElements.define(GrDiffHighlight.is, GrDiffHighlight);
-})();
+ _handleCommentThreadMouseleave(e) {
+ const threadEl = this._getThreadEl(e);
+ const index = this._indexForThreadEl(threadEl);
+
+ if (index !== undefined) {
+ this.set(['commentRanges', index, 'hovering'], false);
+ }
+ }
+
+ _indexForThreadEl(threadEl) {
+ const side = threadEl.getAttribute('comment-side');
+ const range = JSON.parse(threadEl.getAttribute('range'));
+
+ if (!range) return undefined;
+
+ return this._indexOfCommentRange(side, range);
+ }
+
+ _indexOfCommentRange(side, range) {
+ function rangesEqual(a, b) {
+ if (!a && !b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ return a.start_line === b.start_line &&
+ a.start_character === b.start_character &&
+ a.end_line === b.end_line &&
+ a.end_character === b.end_character;
+ }
+
+ return this.commentRanges.findIndex(commentRange =>
+ commentRange.side === side && rangesEqual(commentRange.range, range));
+ }
+
+ /**
+ * Get current normalized selection.
+ * Merges multiple ranges, accounts for triple click, accounts for
+ * syntax highligh, convert native DOM Range objects to Gerrit concepts
+ * (line, side, etc).
+ *
+ * @param {Selection} selection
+ * @return {({
+ * start: {
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * },
+ * end: {
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * }
+ * })|null|!Object}
+ */
+ _getNormalizedRange(selection) {
+ const rangeCount = selection.rangeCount;
+ if (rangeCount === 0) {
+ return null;
+ } else if (rangeCount === 1) {
+ return this._normalizeRange(selection.getRangeAt(0));
+ } else {
+ const startRange = this._normalizeRange(selection.getRangeAt(0));
+ const endRange = this._normalizeRange(
+ selection.getRangeAt(rangeCount - 1));
+ return {
+ start: startRange.start,
+ end: endRange.end,
+ };
+ }
+ }
+
+ /**
+ * Normalize a specific DOM Range.
+ *
+ * @return {!Object} fixed normalized range
+ */
+ _normalizeRange(domRange) {
+ const range = GrRangeNormalizer.normalize(domRange);
+ return this._fixTripleClickSelection({
+ start: this._normalizeSelectionSide(
+ range.startContainer, range.startOffset),
+ end: this._normalizeSelectionSide(
+ range.endContainer, range.endOffset),
+ }, domRange);
+ }
+
+ /**
+ * Adjust triple click selection for the whole line.
+ * A triple click always results in:
+ * - start.column == end.column == 0
+ * - end.line == start.line + 1
+ *
+ * @param {!Object} range Normalized range, ie column/line numbers
+ * @param {!Range} domRange DOM Range object
+ * @return {!Object} fixed normalized range
+ */
+ _fixTripleClickSelection(range, domRange) {
+ if (!range.start) {
+ // Selection outside of current diff.
+ return range;
+ }
+ const start = range.start;
+ const end = range.end;
+ // Happens when triple click in side-by-side mode with other side empty.
+ const endsAtOtherEmptySide = !end &&
+ domRange.endOffset === 0 &&
+ domRange.endContainer.nodeName === 'TD' &&
+ (domRange.endContainer.classList.contains('left') ||
+ domRange.endContainer.classList.contains('right'));
+ const endsAtBeginningOfNextLine = end &&
+ start.column === 0 &&
+ end.column === 0 &&
+ end.line === start.line + 1;
+ const content = domRange.cloneContents().querySelector('.contentText');
+ const lineLength = content && this._getLength(content) || 0;
+ if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) {
+ // Move the selection to the end of the previous line.
+ range.end = {
+ node: start.node,
+ column: lineLength,
+ side: start.side,
+ line: start.line,
+ };
+ }
+ return range;
+ }
+
+ /**
+ * Convert DOM Range selection to concrete numbers (line, column, side).
+ * Moves range end if it's not inside td.content.
+ * Returns null if selection end is not valid (outside of diff).
+ *
+ * @param {Node} node td.content child
+ * @param {number} offset offset within node
+ * @return {({
+ * node: Node,
+ * side: string,
+ * line: Number,
+ * column: Number
+ * }|undefined)}
+ */
+ _normalizeSelectionSide(node, offset) {
+ let column;
+ if (!this.contains(node)) {
+ return;
+ }
+ const lineEl = this.diffBuilder.getLineElByChild(node);
+ if (!lineEl) {
+ return;
+ }
+ const side = this.diffBuilder.getSideByLineEl(lineEl);
+ if (!side) {
+ return;
+ }
+ const line = this.diffBuilder.getLineNumberByChild(lineEl);
+ if (!line) {
+ return;
+ }
+ const contentText = this.diffBuilder.getContentByLineEl(lineEl);
+ if (!contentText) {
+ return;
+ }
+ const contentTd = contentText.parentElement;
+ if (!contentTd.contains(node)) {
+ node = contentText;
+ column = 0;
+ } else {
+ const thread = contentTd.querySelector('.comment-thread');
+ if (thread && thread.contains(node)) {
+ column = this._getLength(contentText);
+ node = contentText;
+ } else {
+ column = this._convertOffsetToColumn(node, offset);
+ }
+ }
+
+ return {
+ node,
+ side,
+ line,
+ column,
+ };
+ }
+
+ /**
+ * The only line in which add a comment tooltip is cut off is the first
+ * line. Even if there is a collapsed section, The first visible line is
+ * in the position where the second line would have been, if not for the
+ * collapsed section, so don't need to worry about this case for
+ * positioning the tooltip.
+ */
+ _positionActionBox(actionBox, startLine, range) {
+ if (startLine > 1) {
+ actionBox.placeAbove(range);
+ return;
+ }
+ actionBox.positionBelow = true;
+ actionBox.placeBelow(range);
+ }
+
+ _isRangeValid(range) {
+ if (!range || !range.start || !range.end) {
+ return false;
+ }
+ const start = range.start;
+ const end = range.end;
+ if (start.side !== end.side ||
+ end.line < start.line ||
+ (start.line === end.line && start.column === end.column)) {
+ return false;
+ }
+ return true;
+ }
+
+ _handleSelection(selection, isMouseUp) {
+ const normalizedRange = this._getNormalizedRange(selection);
+ if (!this._isRangeValid(normalizedRange)) {
+ this._removeActionBox();
+ return;
+ }
+ const domRange = selection.getRangeAt(0);
+ const start = normalizedRange.start;
+ const end = normalizedRange.end;
+
+ // TODO (viktard): Drop empty first and last lines from selection.
+
+ // If the selection is from the end of one line to the start of the next
+ // line, then this must have been a double-click, or you have started
+ // dragging. Showing the action box is bad in the former case and not very
+ // useful in the latter, so never do that.
+ // If this was a mouse-up event, we create a comment immediately if
+ // the selection is from the end of a line to the start of the next line.
+ // In a perfect world we would only do this for double-click, but it is
+ // extremely rare that a user would drag from the end of one line to the
+ // start of the next and release the mouse, so we don't bother.
+ // TODO(brohlfs): This does not work, if the double-click is before a new
+ // diff chunk (start will be equal to end), and neither before an "expand
+ // the diff context" block (end line will match the first line of the new
+ // section and thus be greater than start line + 1).
+ if (start.line === end.line - 1 && end.column === 0) {
+ // Rather than trying to find the line contents (for comparing
+ // start.column with the content length), we just check if the selection
+ // is empty to see that it's at the end of a line.
+ const content = domRange.cloneContents().querySelector('.contentText');
+ if (isMouseUp && this._getLength(content) === 0) {
+ this._fireCreateRangeComment(start.side, {
+ start_line: start.line,
+ start_character: 0,
+ end_line: start.line,
+ end_character: start.column,
+ });
+ }
+ return;
+ }
+
+ let actionBox = this.shadowRoot.querySelector('gr-selection-action-box');
+ if (!actionBox) {
+ actionBox = document.createElement('gr-selection-action-box');
+ const root = dom(this.root);
+ root.insertBefore(actionBox, root.firstElementChild);
+ }
+ this.selectedRange = {
+ range: {
+ start_line: start.line,
+ start_character: start.column,
+ end_line: end.line,
+ end_character: end.column,
+ },
+ side: start.side,
+ };
+ if (start.line === end.line) {
+ this._positionActionBox(actionBox, start.line, domRange);
+ } else if (start.node instanceof Text) {
+ if (start.column) {
+ this._positionActionBox(actionBox, start.line,
+ start.node.splitText(start.column));
+ }
+ start.node.parentElement.normalize(); // Undo splitText from above.
+ } else if (start.node.classList.contains('content') &&
+ start.node.firstChild) {
+ this._positionActionBox(actionBox, start.line, start.node.firstChild);
+ } else {
+ this._positionActionBox(actionBox, start.line, start.node);
+ }
+ }
+
+ _fireCreateRangeComment(side, range) {
+ this.fire('create-range-comment', {side, range});
+ this._removeActionBox();
+ }
+
+ _handleRangeCommentRequest(e) {
+ e.stopPropagation();
+ if (!this.selectedRange) {
+ throw Error('Selected Range is needed for new range comment!');
+ }
+ const {side, range} = this.selectedRange;
+ this._fireCreateRangeComment(side, range);
+ }
+
+ _removeActionBox() {
+ this.selectedRange = undefined;
+ const actionBox = this.shadowRoot
+ .querySelector('gr-selection-action-box');
+ if (actionBox) {
+ dom(this.root).removeChild(actionBox);
+ }
+ }
+
+ _convertOffsetToColumn(el, offset) {
+ if (el instanceof Element && el.classList.contains('content')) {
+ return offset;
+ }
+ while (el.previousSibling ||
+ !el.parentElement.classList.contains('content')) {
+ if (el.previousSibling) {
+ el = el.previousSibling;
+ offset += this._getLength(el);
+ } else {
+ el = el.parentElement;
+ }
+ }
+ return offset;
+ }
+
+ /**
+ * Traverse Element from right to left, call callback for each node.
+ * Stops if callback returns true.
+ *
+ * @param {!Element} startNode
+ * @param {function(Node):boolean} callback
+ * @param {Object=} opt_flags If flags.left is true, traverse left.
+ */
+ _traverseContentSiblings(startNode, callback, opt_flags) {
+ const travelLeft = opt_flags && opt_flags.left;
+ let node = startNode;
+ while (node) {
+ if (node instanceof Element &&
+ node.tagName !== 'HL' &&
+ node.tagName !== 'SPAN') {
+ break;
+ }
+ const nextNode = travelLeft ? node.previousSibling : node.nextSibling;
+ if (callback(node)) {
+ break;
+ }
+ node = nextNode;
+ }
+ }
+
+ /**
+ * Get length of a node. If the node is a content node, then only give the
+ * length of its .contentText child.
+ *
+ * @param {?Element} node this is sometimes passed as null.
+ * @return {number}
+ */
+ _getLength(node) {
+ if (node instanceof Element && node.classList.contains('content')) {
+ return this._getLength(node.querySelector('.contentText'));
+ } else {
+ return GrAnnotation.getLength(node);
+ }
+ }
+}
+
+customElements.define(GrDiffHighlight.is, GrDiffHighlight);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
index be72b05..10b4f2d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_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="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-selection-action-box/gr-selection-action-box.html">
-<script src="gr-annotation.js"></script>
-<script src="gr-range-normalizer.js"></script>
-
-<dom-module id="gr-diff-highlight">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
position: relative;
@@ -39,6 +32,4 @@
<div class="contentWrapper">
<slot></slot>
</div>
- </template>
- <script src="gr-diff-highlight.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 02c2033..ca1e2e2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_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-diff-highlight</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-diff-highlight.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-diff-highlight.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-highlight.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -147,486 +152,488 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-highlight', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-highlight.js';
+suite('gr-diff-highlight', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic')[1];
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('comment events', () => {
+ let builder;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic')[1];
+ builder = {
+ getContentsByLineRange: sandbox.stub().returns([]),
+ getLineElByChild: sandbox.stub().returns({}),
+ getSideByLineEl: sandbox.stub().returns('other-side'),
+ };
+ element._cachedDiffBuilder = builder;
+ });
+
+ test('comment-thread-mouseenter from line comments is ignored', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ element.appendChild(threadEl);
+ element.commentRanges = [{side: 'right'}];
+
+ sandbox.stub(element, 'set');
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseenter', {bubbles: true, composed: true}));
+ assert.isFalse(element.set.called);
+ });
+
+ test('comment-thread-mouseenter from ranged comment causes set', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ threadEl.setAttribute('range', JSON.stringify({
+ start_line: 3,
+ start_character: 4,
+ end_line: 5,
+ end_character: 6,
+ }));
+ element.appendChild(threadEl);
+ element.commentRanges = [{side: 'right', range: {
+ start_line: 3,
+ start_character: 4,
+ end_line: 5,
+ end_character: 6,
+ }}];
+
+ sandbox.stub(element, 'set');
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseenter', {bubbles: true, composed: true}));
+ assert.isTrue(element.set.called);
+ const args = element.set.lastCall.args;
+ assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
+ assert.deepEqual(args[1], true);
+ });
+
+ test('comment-thread-mouseleave from line comments is ignored', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ element.appendChild(threadEl);
+ element.commentRanges = [{side: 'right'}];
+
+ sandbox.stub(element, 'set');
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseleave', {bubbles: true, composed: true}));
+ assert.isFalse(element.set.called);
+ });
+
+ test(`create-range-comment for range when create-comment-requested
+ is fired`, () => {
+ sandbox.stub(element, '_removeActionBox');
+ element.selectedRange = {
+ side: 'left',
+ range: {
+ start_line: 7,
+ start_character: 11,
+ end_line: 24,
+ end_character: 42,
+ },
+ };
+ const requestEvent = new CustomEvent('create-comment-requested');
+ let createRangeEvent;
+ element.addEventListener('create-range-comment', e => {
+ createRangeEvent = e;
+ });
+ element.dispatchEvent(requestEvent);
+ assert.deepEqual(element.selectedRange, createRangeEvent.detail);
+ assert.isTrue(element._removeActionBox.called);
+ });
+ });
+
+ suite('selection', () => {
+ let diff;
+ let builder;
+ let contentStubs;
+
+ const stubContent = (line, side, opt_child) => {
+ const contentTd = diff.querySelector(
+ `.${side}.lineNum[data-value="${line}"] ~ .content`);
+ const contentText = contentTd.querySelector('.contentText');
+ const lineEl = diff.querySelector(
+ `.${side}.lineNum[data-value="${line}"]`);
+ contentStubs.push({
+ lineEl,
+ contentTd,
+ contentText,
+ });
+ builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
+ builder.getLineNumberByChild.withArgs(lineEl).returns(line);
+ builder.getContentByLine.withArgs(line, side).returns(contentText);
+ builder.getSideByLineEl.withArgs(lineEl).returns(side);
+ return contentText;
+ };
+
+ const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
+ const selection = window.getSelection();
+ const range = document.createRange();
+ range.setStart(startNode, startOffset);
+ range.setEnd(endNode, endOffset);
+ selection.addRange(range);
+ element._handleSelection(selection);
+ };
+
+ const getLineElByChild = node => {
+ const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
+ return stubs && stubs.lineEl;
+ };
+
+ setup(() => {
+ contentStubs = [];
+ stub('gr-selection-action-box', {
+ placeAbove: sandbox.stub(),
+ placeBelow: sandbox.stub(),
+ });
+ diff = element.querySelector('#diffTable');
+ builder = {
+ getContentByLine: sandbox.stub(),
+ getContentByLineEl: sandbox.stub(),
+ getLineElByChild,
+ getLineNumberByChild: sandbox.stub(),
+ getSideByLineEl: sandbox.stub(),
+ };
+ element._cachedDiffBuilder = builder;
});
teardown(() => {
- sandbox.restore();
+ contentStubs = null;
+ window.getSelection().removeAllRanges();
});
- suite('comment events', () => {
- let builder;
+ test('single first line', () => {
+ const content = stubContent(1, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
- setup(() => {
- builder = {
- getContentsByLineRange: sandbox.stub().returns([]),
- getLineElByChild: sandbox.stub().returns({}),
- getSideByLineEl: sandbox.stub().returns('other-side'),
- };
- element._cachedDiffBuilder = builder;
+ test('multiline starting on first line', () => {
+ const startContent = stubContent(1, 'right');
+ const endContent = stubContent(2, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(
+ startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('single line', () => {
+ const content = stubContent(138, 'left');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 138,
+ start_character: 5,
+ end_line: 138,
+ end_character: 12,
});
+ assert.equal(side, 'left');
+ assert.notOk(actionBox.positionBelow);
+ });
- test('comment-thread-mouseenter from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
-
- sandbox.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
+ test('multiline', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(
+ startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = element.shadowRoot
+ .querySelector('gr-selection-action-box');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
});
+ assert.equal(side, 'right');
+ assert.notOk(actionBox.positionBelow);
+ });
- test('comment-thread-mouseenter from ranged comment causes set', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- threadEl.setAttribute('range', JSON.stringify({
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }));
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right', range: {
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }}];
+ test('multiple ranges aka firefox implementation', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
- sandbox.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isTrue(element.set.called);
- const args = element.set.lastCall.args;
- assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
- assert.deepEqual(args[1], true);
- });
+ const startRange = document.createRange();
+ startRange.setStart(startContent.firstChild, 10);
+ startRange.setEnd(startContent.firstChild, 11);
- test('comment-thread-mouseleave from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
+ const endRange = document.createRange();
+ endRange.setStart(endContent.lastChild, 6);
+ endRange.setEnd(endContent.lastChild, 7);
- sandbox.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseleave', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
- });
-
- test(`create-range-comment for range when create-comment-requested
- is fired`, () => {
- sandbox.stub(element, '_removeActionBox');
- element.selectedRange = {
- side: 'left',
- range: {
- start_line: 7,
- start_character: 11,
- end_line: 24,
- end_character: 42,
- },
- };
- const requestEvent = new CustomEvent('create-comment-requested');
- let createRangeEvent;
- element.addEventListener('create-range-comment', e => {
- createRangeEvent = e;
- });
- element.dispatchEvent(requestEvent);
- assert.deepEqual(element.selectedRange, createRangeEvent.detail);
- assert.isTrue(element._removeActionBox.called);
+ const getRangeAtStub = sandbox.stub();
+ getRangeAtStub
+ .onFirstCall().returns(startRange)
+ .onSecondCall()
+ .returns(endRange);
+ const selection = {
+ rangeCount: 2,
+ getRangeAt: getRangeAtStub,
+ removeAllRanges: sandbox.stub(),
+ };
+ element._handleSelection(selection);
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
});
});
- suite('selection', () => {
- let diff;
- let builder;
- let contentStubs;
-
- const stubContent = (line, side, opt_child) => {
- const contentTd = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"] ~ .content`);
- const contentText = contentTd.querySelector('.contentText');
- const lineEl = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"]`);
- contentStubs.push({
- lineEl,
- contentTd,
- contentText,
- });
- builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
- builder.getLineNumberByChild.withArgs(lineEl).returns(line);
- builder.getContentByLine.withArgs(line, side).returns(contentText);
- builder.getSideByLineEl.withArgs(lineEl).returns(side);
- return contentText;
- };
-
- const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
- const selection = window.getSelection();
- const range = document.createRange();
- range.setStart(startNode, startOffset);
- range.setEnd(endNode, endOffset);
- selection.addRange(range);
- element._handleSelection(selection);
- };
-
- const getLineElByChild = node => {
- const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
- return stubs && stubs.lineEl;
- };
-
- setup(() => {
- contentStubs = [];
- stub('gr-selection-action-box', {
- placeAbove: sandbox.stub(),
- placeBelow: sandbox.stub(),
- });
- diff = element.querySelector('#diffTable');
- builder = {
- getContentByLine: sandbox.stub(),
- getContentByLineEl: sandbox.stub(),
- getLineElByChild,
- getLineNumberByChild: sandbox.stub(),
- getSideByLineEl: sandbox.stub(),
- };
- element._cachedDiffBuilder = builder;
+ test('multiline grow end highlight over tabs', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
+ emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 2,
});
+ assert.equal(side, 'right');
+ });
- teardown(() => {
- contentStubs = null;
- window.getSelection().removeAllRanges();
+ test('collapsed', () => {
+ const content = stubContent(138, 'left');
+ emulateSelection(content.firstChild, 5, content.firstChild, 5);
+ assert.isOk(window.getSelection().getRangeAt(0).startContainer);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts inside hl', () => {
+ const content = stubContent(140, 'left');
+ const hl = content.querySelector('.foo');
+ emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 8,
+ end_line: 140,
+ end_character: 23,
});
+ assert.equal(side, 'left');
+ });
- test('single first line', () => {
- const content = stubContent(1, 'right');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
+ test('ends inside hl', () => {
+ const content = stubContent(140, 'left');
+ const hl = content.querySelector('.bar');
+ emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 18,
+ end_line: 140,
+ end_character: 27,
});
+ });
- test('multiline starting on first line', () => {
- const startContent = stubContent(1, 'right');
- const endContent = stubContent(2, 'right');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
+ test('multiple hl', () => {
+ const content = stubContent(140, 'left');
+ const hl = content.querySelectorAll('hl')[4];
+ emulateSelection(content.firstChild, 2, hl.firstChild, 2);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 2,
+ end_line: 140,
+ end_character: 61,
});
+ assert.equal(side, 'left');
+ });
- test('single line', () => {
- const content = stubContent(138, 'left');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 138,
- start_character: 5,
- end_line: 138,
- end_character: 12,
- });
- assert.equal(side, 'left');
- assert.notOk(actionBox.positionBelow);
+ test('starts outside of diff', () => {
+ const contentText = stubContent(140, 'left');
+ const contentTd = contentText.parentElement;
+
+ emulateSelection(contentTd.previousElementSibling, 0,
+ contentText.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('ends outside of diff', () => {
+ const content = stubContent(140, 'left');
+ emulateSelection(content.nextElementSibling.firstChild, 2,
+ content.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts and ends on different sides', () => {
+ const startContent = stubContent(140, 'left');
+ const endContent = stubContent(130, 'right');
+ emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts in comment thread element', () => {
+ const startContent = stubContent(140, 'left');
+ const comment = startContent.parentElement.querySelector(
+ '.comment-thread');
+ const endContent = stubContent(141, 'left');
+ emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 83,
+ end_line: 141,
+ end_character: 4,
});
+ assert.equal(side, 'left');
+ });
- test('multiline', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- sandbox.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
- assert.equal(side, 'right');
- assert.notOk(actionBox.positionBelow);
+ test('ends in comment thread element', () => {
+ const content = stubContent(140, 'left');
+ const comment = content.parentElement.querySelector(
+ '.comment-thread');
+ emulateSelection(content.firstChild, 4, comment.firstChild, 1);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 4,
+ end_line: 140,
+ end_character: 83,
});
+ assert.equal(side, 'left');
+ });
- test('multiple ranges aka firefox implementation', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
+ test('starts in context element', () => {
+ const contextControl =
+ diff.querySelector('.contextControl').querySelector('gr-button');
+ const content = stubContent(146, 'right');
+ emulateSelection(contextControl, 0, content.firstChild, 7);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
- const startRange = document.createRange();
- startRange.setStart(startContent.firstChild, 10);
- startRange.setEnd(startContent.firstChild, 11);
+ test('ends in context element', () => {
+ const contextControl =
+ diff.querySelector('.contextControl').querySelector('gr-button');
+ const content = stubContent(141, 'left');
+ emulateSelection(content.firstChild, 2, contextControl, 1);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
- const endRange = document.createRange();
- endRange.setStart(endContent.lastChild, 6);
- endRange.setEnd(endContent.lastChild, 7);
-
- const getRangeAtStub = sandbox.stub();
- getRangeAtStub
- .onFirstCall().returns(startRange)
- .onSecondCall()
- .returns(endRange);
- const selection = {
- rangeCount: 2,
- getRangeAt: getRangeAtStub,
- removeAllRanges: sandbox.stub(),
- };
- element._handleSelection(selection);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
+ test('selection containing context element', () => {
+ const startContent = stubContent(130, 'right');
+ const endContent = stubContent(146, 'right');
+ emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 130,
+ start_character: 3,
+ end_line: 146,
+ end_character: 14,
});
+ assert.equal(side, 'right');
+ });
- test('multiline grow end highlight over tabs', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 2,
- });
- assert.equal(side, 'right');
+ test('ends at a tab', () => {
+ const content = stubContent(140, 'left');
+ emulateSelection(
+ content.firstChild, 1, content.querySelector('span'), 0);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 1,
+ end_line: 140,
+ end_character: 51,
});
+ assert.equal(side, 'left');
+ });
- test('collapsed', () => {
- const content = stubContent(138, 'left');
- emulateSelection(content.firstChild, 5, content.firstChild, 5);
- assert.isOk(window.getSelection().getRangeAt(0).startContainer);
- assert.isFalse(!!element.selectedRange);
+ test('starts at a tab', () => {
+ const content = stubContent(140, 'left');
+ emulateSelection(
+ content.querySelectorAll('hl')[3], 0,
+ content.querySelectorAll('span')[1].nextSibling, 1);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 51,
+ end_line: 140,
+ end_character: 71,
});
+ assert.equal(side, 'left');
+ });
- test('starts inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.foo');
- emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 8,
- end_line: 140,
- end_character: 23,
- });
- assert.equal(side, 'left');
+ test('properly accounts for syntax highlighting', () => {
+ const content = stubContent(140, 'left');
+ const spy = sinon.spy(element, '_normalizeRange');
+ emulateSelection(
+ content.querySelectorAll('hl')[3], 0,
+ content.querySelectorAll('span')[1], 0);
+ const spyCall = spy.getCall(0);
+ const range = window.getSelection().getRangeAt(0);
+ assert.notDeepEqual(spyCall.returnValue, range);
+ });
+
+ test('GrRangeNormalizer._getTextOffset computes text offset', () => {
+ let content = stubContent(140, 'left');
+ let child = content.lastChild.lastChild;
+ let result = GrRangeNormalizer._getTextOffset(content, child);
+ assert.equal(result, 75);
+ content = stubContent(146, 'right');
+ child = content.lastChild;
+ result = GrRangeNormalizer._getTextOffset(content, child);
+ assert.equal(result, 0);
+ });
+
+ test('_fixTripleClickSelection', () => {
+ const startContent = stubContent(119, 'right');
+ const endContent = stubContent(120, 'right');
+ emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 0,
+ end_line: 119,
+ end_character: element._getLength(startContent),
});
+ assert.equal(side, 'right');
+ });
- test('ends inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.bar');
- emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 18,
- end_line: 140,
- end_character: 27,
- });
+ test('_fixTripleClickSelection empty line', () => {
+ const startContent = stubContent(146, 'right');
+ const endContent = stubContent(165, 'left');
+ emulateSelection(startContent.firstChild, 0,
+ endContent.parentElement.previousElementSibling, 0);
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 146,
+ start_character: 0,
+ end_line: 146,
+ end_character: 84,
});
-
- test('multiple hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelectorAll('hl')[4];
- emulateSelection(content.firstChild, 2, hl.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 2,
- end_line: 140,
- end_character: 61,
- });
- assert.equal(side, 'left');
- });
-
- test('starts outside of diff', () => {
- const contentText = stubContent(140, 'left');
- const contentTd = contentText.parentElement;
-
- emulateSelection(contentTd.previousElementSibling, 0,
- contentText.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends outside of diff', () => {
- const content = stubContent(140, 'left');
- emulateSelection(content.nextElementSibling.firstChild, 2,
- content.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts and ends on different sides', () => {
- const startContent = stubContent(140, 'left');
- const endContent = stubContent(130, 'right');
- emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts in comment thread element', () => {
- const startContent = stubContent(140, 'left');
- const comment = startContent.parentElement.querySelector(
- '.comment-thread');
- const endContent = stubContent(141, 'left');
- emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 83,
- end_line: 141,
- end_character: 4,
- });
- assert.equal(side, 'left');
- });
-
- test('ends in comment thread element', () => {
- const content = stubContent(140, 'left');
- const comment = content.parentElement.querySelector(
- '.comment-thread');
- emulateSelection(content.firstChild, 4, comment.firstChild, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 4,
- end_line: 140,
- end_character: 83,
- });
- assert.equal(side, 'left');
- });
-
- test('starts in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(146, 'right');
- emulateSelection(contextControl, 0, content.firstChild, 7);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(141, 'left');
- emulateSelection(content.firstChild, 2, contextControl, 1);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('selection containing context element', () => {
- const startContent = stubContent(130, 'right');
- const endContent = stubContent(146, 'right');
- emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 130,
- start_character: 3,
- end_line: 146,
- end_character: 14,
- });
- assert.equal(side, 'right');
- });
-
- test('ends at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.firstChild, 1, content.querySelector('span'), 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 1,
- end_line: 140,
- end_character: 51,
- });
- assert.equal(side, 'left');
- });
-
- test('starts at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1].nextSibling, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 51,
- end_line: 140,
- end_character: 71,
- });
- assert.equal(side, 'left');
- });
-
- test('properly accounts for syntax highlighting', () => {
- const content = stubContent(140, 'left');
- const spy = sinon.spy(element, '_normalizeRange');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1], 0);
- const spyCall = spy.getCall(0);
- const range = window.getSelection().getRangeAt(0);
- assert.notDeepEqual(spyCall.returnValue, range);
- });
-
- test('GrRangeNormalizer._getTextOffset computes text offset', () => {
- let content = stubContent(140, 'left');
- let child = content.lastChild.lastChild;
- let result = GrRangeNormalizer._getTextOffset(content, child);
- assert.equal(result, 75);
- content = stubContent(146, 'right');
- child = content.lastChild;
- result = GrRangeNormalizer._getTextOffset(content, child);
- assert.equal(result, 0);
- });
-
- test('_fixTripleClickSelection', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 0,
- end_line: 119,
- end_character: element._getLength(startContent),
- });
- assert.equal(side, 'right');
- });
-
- test('_fixTripleClickSelection empty line', () => {
- const startContent = stubContent(146, 'right');
- const endContent = stubContent(165, 'left');
- emulateSelection(startContent.firstChild, 0,
- endContent.parentElement.previousElementSibling, 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 146,
- start_character: 0,
- end_line: 146,
- end_character: 84,
- });
- assert.equal(side, 'right');
- });
+ assert.equal(side, 'right');
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index a44e366..26a3a40 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -14,1097 +14,1112 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MSG_EMPTY_BLAME = 'No blame information for this diff.';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-comment-thread/gr-comment-thread.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import '../gr-diff/gr-diff.js';
+import '../gr-syntax-layer/gr-syntax-layer.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-diff-host_html.js';
- const EVENT_AGAINST_PARENT = 'diff-against-parent';
- const EVENT_ZERO_REBASE = 'rebase-percent-zero';
- const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero';
+const MSG_EMPTY_BLAME = 'No blame information for this diff.';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const EVENT_AGAINST_PARENT = 'diff-against-parent';
+const EVENT_ZERO_REBASE = 'rebase-percent-zero';
+const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero';
- /** @enum {string} */
- const TimingLabel = {
- TOTAL: 'Diff Total Render',
- CONTENT: 'Diff Content Render',
- SYNTAX: 'Diff Syntax Render',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- // Disable syntax highlighting if the overall diff is too large.
- const SYNTAX_MAX_DIFF_LENGTH = 20000;
+/** @enum {string} */
+const TimingLabel = {
+ TOTAL: 'Diff Total Render',
+ CONTENT: 'Diff Content Render',
+ SYNTAX: 'Diff Syntax Render',
+};
- // If any line of the diff is more than the character limit, then disable
- // syntax highlighting for the entire file.
- const SYNTAX_MAX_LINE_LENGTH = 500;
+// Disable syntax highlighting if the overall diff is too large.
+const SYNTAX_MAX_DIFF_LENGTH = 20000;
- // 120 lines is good enough threshold for full-sized window viewport
- const NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT = 120;
+// If any line of the diff is more than the character limit, then disable
+// syntax highlighting for the entire file.
+const SYNTAX_MAX_LINE_LENGTH = 500;
- const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
+// 120 lines is good enough threshold for full-sized window viewport
+const NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT = 120;
+
+const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
+
+/**
+ * @param {Object} diff
+ * @return {boolean}
+ */
+function isImageDiff(diff) {
+ if (!diff) { return false; }
+
+ const isA = diff.meta_a &&
+ diff.meta_a.content_type.startsWith('image/');
+ const isB = diff.meta_b &&
+ diff.meta_b.content_type.startsWith('image/');
+
+ return !!(diff.binary && (isA || isB));
+}
+
+/** @enum {string} */
+Gerrit.DiffSide = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ */
+/**
+ * Wrapper around gr-diff.
+ *
+ * Webcomponent fetching diffs and related data from restAPI and passing them
+ * to the presentational gr-diff for rendering.
+ *
+ * @extends Polymer.Element
+ */
+class GrDiffHost extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-host'; }
+ /**
+ * Fired when the user selects a line.
+ *
+ * @event line-selected
+ */
+
+ /**
+ * Fired if being logged in is required.
+ *
+ * @event show-auth-required
+ */
+
+ /**
+ * Fired when a comment is saved or discarded
+ *
+ * @event diff-comments-modified
+ */
+
+ static get properties() {
+ return {
+ changeNum: String,
+ noAutoRender: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ patchRange: Object,
+ path: String,
+ prefs: {
+ type: Object,
+ },
+ projectName: String,
+ displayLine: {
+ type: Boolean,
+ value: false,
+ },
+ isImageDiff: {
+ type: Boolean,
+ computed: '_computeIsImageDiff(diff)',
+ notify: true,
+ },
+ commitRange: Object,
+ filesWeblinks: {
+ type: Object,
+ value() {
+ return {};
+ },
+ notify: true,
+ },
+ hidden: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ noRenderOnPrefsChange: {
+ type: Boolean,
+ value: false,
+ },
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ lineWrapping: {
+ type: Boolean,
+ value: false,
+ },
+ viewMode: {
+ type: String,
+ value: DiffViewMode.SIDE_BY_SIDE,
+ },
+
+ /**
+ * Special line number which should not be collapsed into a shared region.
+ *
+ * @type {{
+ * number: number,
+ * leftSide: {boolean}
+ * }|null}
+ */
+ lineOfInterest: Object,
+
+ /**
+ * If the diff fails to load, show the failure message in the diff rather
+ * than bubbling the error up to the whole page. This is useful for when
+ * loading inline diffs because one diff failing need not mark the whole
+ * page with a failure.
+ */
+ showLoadFailure: Boolean,
+
+ isBlameLoaded: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeIsBlameLoaded(_blame)',
+ },
+
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+
+ _loading: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {?string} */
+ _errorMessage: {
+ type: String,
+ value: null,
+ },
+
+ /** @type {?Object} */
+ _baseImage: Object,
+ /** @type {?Object} */
+ _revisionImage: Object,
+ /**
+ * This is a DiffInfo object.
+ */
+ diff: {
+ type: Object,
+ notify: true,
+ },
+
+ /** @type {?Object} */
+ _blame: {
+ type: Object,
+ value: null,
+ },
+
+ /**
+ * @type {!Array<!Gerrit.CoverageRange>}
+ */
+ _coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
+
+ _loadedWhitespaceLevel: String,
+
+ _parentIndex: {
+ type: Number,
+ computed: '_computeParentIndex(patchRange.*)',
+ },
+
+ _syntaxHighlightingEnabled: {
+ type: Boolean,
+ computed:
+ '_isSyntaxHighlightingEnabled(prefs.*, diff)',
+ },
+
+ _layers: {
+ type: Array,
+ value: [],
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' +
+ ' noRenderOnPrefsChange)',
+ '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)',
+ ];
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener(
+ // These are named inconsistently for a reason:
+ // The create-comment event is fired to indicate that we should
+ // create a comment.
+ // The comment-* events are just notifying that the comments did already
+ // change in some way, and that we should update any models we may want
+ // to keep in sync.
+ 'create-comment',
+ e => this._handleCreateComment(e));
+ this.addEventListener('comment-discard',
+ e => this._handleCommentDiscard(e));
+ this.addEventListener('comment-update',
+ e => this._handleCommentUpdate(e));
+ this.addEventListener('comment-save',
+ e => this._handleCommentSave(e));
+ this.addEventListener('render-start',
+ () => this._handleRenderStart());
+ this.addEventListener('render-content',
+ () => this._handleRenderContent());
+ this.addEventListener('normalize-range',
+ event => this._handleNormalizeRange(event));
+ this.addEventListener('diff-context-expanded',
+ event => this._handleDiffContextExpanded(event));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ if (this._canReload()) {
+ this.reload();
+ }
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ }
+
+ /**
+ * @param {boolean=} shouldReportMetric indicate a new Diff Page. This is a
+ * signal to report metrics event that started on location change.
+ * @return {!Promise}
+ **/
+ reload(shouldReportMetric) {
+ this._loading = true;
+ this._errorMessage = null;
+ const whitespaceLevel = this._getIgnoreWhitespace();
+
+ const layers = [this.$.syntaxLayer];
+ // Get layers from plugins (if any).
+ for (const pluginLayer of this.$.jsAPI.getDiffLayers(
+ this.path, this.changeNum, this.patchNum)) {
+ layers.push(pluginLayer);
+ }
+ this._layers = layers;
+
+ if (shouldReportMetric) {
+ // We listen on render viewport only on DiffPage (on paramsChanged)
+ this._listenToViewportRender();
+ }
+
+ this._coverageRanges = [];
+ this._getCoverageData();
+ const diffRequest = this._getDiff()
+ .then(diff => {
+ this._loadedWhitespaceLevel = whitespaceLevel;
+ this._reportDiff(diff);
+ return diff;
+ })
+ .catch(e => {
+ this._handleGetDiffError(e);
+ return null;
+ });
+
+ const assetRequest = diffRequest.then(diff => {
+ // If the diff is null, then it's failed to load.
+ if (!diff) { return null; }
+
+ return this._loadDiffAssets(diff);
+ });
+
+ // Not waiting for coverage ranges intentionally as
+ // plugin loading should not block the content rendering
+ return Promise.all([diffRequest, assetRequest])
+ .then(results => {
+ const diff = results[0];
+ if (!diff) {
+ return Promise.resolve();
+ }
+ this.filesWeblinks = this._getFilesWeblinks(diff);
+ return new Promise(resolve => {
+ const callback = event => {
+ const needsSyntaxHighlighting = event.detail &&
+ event.detail.contentRendered;
+ if (needsSyntaxHighlighting) {
+ this.$.reporting.time(TimingLabel.SYNTAX);
+ this.$.syntaxLayer.process().then(() => {
+ this.$.reporting.timeEnd(TimingLabel.SYNTAX);
+ this.$.reporting.timeEnd(TimingLabel.TOTAL);
+ resolve();
+ });
+ } else {
+ this.$.reporting.timeEnd(TimingLabel.TOTAL);
+ resolve();
+ }
+ this.removeEventListener('render', callback);
+ if (shouldReportMetric) {
+ // We report diffViewContentDisplayed only on reload caused
+ // by params changed - expected only on Diff Page.
+ this.$.reporting.diffViewContentDisplayed();
+ }
+ };
+ this.addEventListener('render', callback);
+ this.diff = diff;
+ });
+ })
+ .catch(err => {
+ console.warn('Error encountered loading diff:', err);
+ })
+ .then(() => { this._loading = false; });
+ }
+
+ _getCoverageData() {
+ const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this;
+ this.$.jsAPI.getCoverageAnnotationApi().
+ then(coverageAnnotationApi => {
+ if (!coverageAnnotationApi) return;
+ const provider = coverageAnnotationApi.getCoverageProvider();
+ return provider(changeNum, path, basePatchNum, patchNum)
+ .then(coverageRanges => {
+ if (!coverageRanges ||
+ changeNum !== this.changeNum ||
+ path !== this.path ||
+ basePatchNum !== this.patchRange.basePatchNum ||
+ patchNum !== this.patchRange.patchNum) {
+ return;
+ }
+
+ const existingCoverageRanges = this._coverageRanges;
+ this._coverageRanges = coverageRanges;
+
+ // Notify with existing coverage ranges
+ // in case there is some existing coverage data that needs to be removed
+ existingCoverageRanges.forEach(range => {
+ coverageAnnotationApi.notify(
+ path,
+ range.code_range.start_line,
+ range.code_range.end_line,
+ range.side);
+ });
+
+ // Notify with new coverage data
+ coverageRanges.forEach(range => {
+ coverageAnnotationApi.notify(
+ path,
+ range.code_range.start_line,
+ range.code_range.end_line,
+ range.side);
+ });
+ });
+ })
+ .catch(err => {
+ console.warn('Loading coverage ranges failed: ', err);
+ });
+ }
+
+ _getFilesWeblinks(diff) {
+ if (!this.commitRange) {
+ return {};
+ }
+ return {
+ meta_a: Gerrit.Nav.getFileWebLinks(
+ this.projectName, this.commitRange.baseCommit, this.path,
+ {weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
+ meta_b: Gerrit.Nav.getFileWebLinks(
+ this.projectName, this.commitRange.commit, this.path,
+ {weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
+ };
+ }
+
+ /** Cancel any remaining diff builder rendering work. */
+ cancel() {
+ this.$.diff.cancel();
+ }
+
+ /** @return {!Array<!HTMLElement>} */
+ getCursorStops() {
+ return this.$.diff.getCursorStops();
+ }
+
+ /** @return {boolean} */
+ isRangeSelected() {
+ return this.$.diff.isRangeSelected();
+ }
+
+ createRangeComment() {
+ return this.$.diff.createRangeComment();
+ }
+
+ toggleLeftDiff() {
+ this.$.diff.toggleLeftDiff();
+ }
+
+ /**
+ * Load and display blame information for the base of the diff.
+ *
+ * @return {Promise} A promise that resolves when blame finishes rendering.
+ */
+ loadBlame() {
+ return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
+ this.path, true)
+ .then(blame => {
+ if (!blame.length) {
+ this.fire('show-alert', {message: MSG_EMPTY_BLAME});
+ return Promise.reject(MSG_EMPTY_BLAME);
+ }
+
+ this._blame = blame;
+ });
+ }
+
+ /** Unload blame information for the diff. */
+ clearBlame() {
+ this._blame = null;
+ }
+
+ /**
+ * The thread elements in this diff, in no particular order.
+ *
+ * @return {!Array<!HTMLElement>}
+ */
+ getThreadEls() {
+ return Array.from(
+ dom(this.$.diff).querySelectorAll('.comment-thread'));
+ }
+
+ /** @param {HTMLElement} el */
+ addDraftAtLine(el) {
+ this.$.diff.addDraftAtLine(el);
+ }
+
+ clearDiffContent() {
+ this.$.diff.clearDiffContent();
+ }
+
+ expandAllContext() {
+ this.$.diff.expandAllContext();
+ }
+
+ /** @return {!Promise} */
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ /** @return {boolean}} */
+ _canReload() {
+ return !!this.changeNum && !!this.patchRange && !!this.path &&
+ !this.noAutoRender;
+ }
+
+ /** @return {!Promise<!Object>} */
+ _getDiff() {
+ // Wrap the diff request in a new promise so that the error handler
+ // rejects the promise, allowing the error to be handled in the .catch.
+ return new Promise((resolve, reject) => {
+ this.$.restAPI.getDiff(
+ this.changeNum,
+ this.patchRange.basePatchNum,
+ this.patchRange.patchNum,
+ this.path,
+ this._getIgnoreWhitespace(),
+ reject)
+ .then(resolve);
+ });
+ }
+
+ _handleGetDiffError(response) {
+ // Loading the diff may respond with 409 if the file is too large. In this
+ // case, use a toast error..
+ if (response.status === 409) {
+ this.fire('server-error', {response});
+ return;
+ }
+
+ if (this.showLoadFailure) {
+ this._errorMessage = [
+ 'Encountered error when loading the diff:',
+ response.status,
+ response.statusText,
+ ].join(' ');
+ return;
+ }
+
+ this.fire('page-error', {response});
+ }
+
+ /**
+ * Report info about the diff response.
+ */
+ _reportDiff(diff) {
+ if (!diff || !diff.content) {
+ return;
+ }
+
+ // Count the delta lines stemming from normal deltas, and from
+ // due_to_rebase deltas.
+ let nonRebaseDelta = 0;
+ let rebaseDelta = 0;
+ diff.content.forEach(chunk => {
+ if (chunk.ab) { return; }
+ const deltaSize = Math.max(
+ chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
+ if (chunk.due_to_rebase) {
+ rebaseDelta += deltaSize;
+ } else {
+ nonRebaseDelta += deltaSize;
+ }
+ });
+
+ // Find the percent of the delta from due_to_rebase chunks rounded to two
+ // digits. Diffs with no delta are considered 0%.
+ const totalDelta = rebaseDelta + nonRebaseDelta;
+ const percentRebaseDelta = !totalDelta ? 0 :
+ Math.round(100 * rebaseDelta / totalDelta);
+
+ // Report the due_to_rebase percentage in the "diff" category when
+ // applicable.
+ if (this.patchRange.basePatchNum === 'PARENT') {
+ this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
+ } else if (percentRebaseDelta === 0) {
+ this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
+ } else {
+ this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
+ {percentRebaseDelta});
+ }
+ }
+
+ /**
+ * @param {Object} diff
+ * @return {!Promise}
+ */
+ _loadDiffAssets(diff) {
+ if (isImageDiff(diff)) {
+ return this._getImages(diff).then(images => {
+ this._baseImage = images.baseImage;
+ this._revisionImage = images.revisionImage;
+ });
+ } else {
+ this._baseImage = null;
+ this._revisionImage = null;
+ return Promise.resolve();
+ }
+ }
/**
* @param {Object} diff
* @return {boolean}
*/
- function isImageDiff(diff) {
- if (!diff) { return false; }
-
- const isA = diff.meta_a &&
- diff.meta_a.content_type.startsWith('image/');
- const isB = diff.meta_b &&
- diff.meta_b.content_type.startsWith('image/');
-
- return !!(diff.binary && (isA || isB));
+ _computeIsImageDiff(diff) {
+ return isImageDiff(diff);
}
- /** @enum {string} */
- Gerrit.DiffSide = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+ _commentsChanged(newComments) {
+ const allComments = [];
+ for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) {
+ // This is needed by the threading.
+ for (const comment of newComments[side]) {
+ comment.__commentSide = side;
+ }
+ allComments.push(...newComments[side]);
+ }
+ // Currently, the only way this is ever changed here is when the initial
+ // comments are loaded, so it's okay performance wise to clear the threads
+ // and recreate them. If this changes in future, we might want to reuse
+ // some DOM nodes here.
+ this._clearThreads();
+ const threads = this._createThreads(allComments);
+ for (const thread of threads) {
+ const threadEl = this._createThreadElement(thread);
+ this._attachThreadElement(threadEl);
+ }
+ }
+
+ _sortComments(comments) {
+ return comments.slice(0).sort((a, b) => {
+ if (b.__draft && !a.__draft ) { return -1; }
+ if (a.__draft && !b.__draft ) { return 1; }
+ return util.parseDate(a.updated) - util.parseDate(b.updated);
+ });
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
+ * @param {!Array<!Object>} comments
+ * @return {!Array<!Object>} Threads for the given comments.
*/
+ _createThreads(comments) {
+ const sortedComments = this._sortComments(comments);
+ const threads = [];
+ for (const comment of sortedComments) {
+ // If the comment is in reply to another comment, find that comment's
+ // thread and append to it.
+ if (comment.in_reply_to) {
+ const thread = threads.find(thread =>
+ thread.comments.some(c => c.id === comment.in_reply_to));
+ if (thread) {
+ thread.comments.push(comment);
+ continue;
+ }
+ }
+
+ // Otherwise, this comment starts its own thread.
+ const newThread = {
+ start_datetime: comment.updated,
+ comments: [comment],
+ commentSide: comment.__commentSide,
+ patchNum: comment.patch_set,
+ rootId: comment.id || comment.__draftID,
+ lineNum: comment.line,
+ isOnParent: comment.side === 'PARENT',
+ };
+ if (comment.range) {
+ newThread.range = Object.assign({}, comment.range);
+ }
+ threads.push(newThread);
+ }
+ return threads;
+ }
+
/**
- * Wrapper around gr-diff.
- *
- * Webcomponent fetching diffs and related data from restAPI and passing them
- * to the presentational gr-diff for rendering.
- *
- * @extends Polymer.Element
+ * @param {Object} blame
+ * @return {boolean}
*/
- class GrDiffHost extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-host'; }
- /**
- * Fired when the user selects a line.
- *
- * @event line-selected
- */
+ _computeIsBlameLoaded(blame) {
+ return !!blame;
+ }
- /**
- * Fired if being logged in is required.
- *
- * @event show-auth-required
- */
+ /**
+ * @param {Object} diff
+ * @return {!Promise}
+ */
+ _getImages(diff) {
+ return this.$.restAPI.getImagesForDiff(this.changeNum, diff,
+ this.patchRange);
+ }
- /**
- * Fired when a comment is saved or discarded
- *
- * @event diff-comments-modified
- */
+ /** @param {CustomEvent} e */
+ _handleCreateComment(e) {
+ const {lineNum, side, patchNum, isOnParent, range} = e.detail;
+ const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range,
+ isOnParent);
+ threadEl.addOrEditDraft(lineNum, range);
- static get properties() {
- return {
- changeNum: String,
- noAutoRender: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- patchRange: Object,
- path: String,
- prefs: {
- type: Object,
- },
- projectName: String,
- displayLine: {
- type: Boolean,
- value: false,
- },
- isImageDiff: {
- type: Boolean,
- computed: '_computeIsImageDiff(diff)',
- notify: true,
- },
- commitRange: Object,
- filesWeblinks: {
- type: Object,
- value() {
- return {};
- },
- notify: true,
- },
- hidden: {
- type: Boolean,
- reflectToAttribute: true,
- },
- noRenderOnPrefsChange: {
- type: Boolean,
- value: false,
- },
- comments: {
- type: Object,
- observer: '_commentsChanged',
- },
- lineWrapping: {
- type: Boolean,
- value: false,
- },
- viewMode: {
- type: String,
- value: DiffViewMode.SIDE_BY_SIDE,
- },
+ this.$.reporting.recordDraftInteraction();
+ }
- /**
- * Special line number which should not be collapsed into a shared region.
- *
- * @type {{
- * number: number,
- * leftSide: {boolean}
- * }|null}
- */
- lineOfInterest: Object,
-
- /**
- * If the diff fails to load, show the failure message in the diff rather
- * than bubbling the error up to the whole page. This is useful for when
- * loading inline diffs because one diff failing need not mark the whole
- * page with a failure.
- */
- showLoadFailure: Boolean,
-
- isBlameLoaded: {
- type: Boolean,
- notify: true,
- computed: '_computeIsBlameLoaded(_blame)',
- },
-
- _loggedIn: {
- type: Boolean,
- value: false,
- },
-
- _loading: {
- type: Boolean,
- value: false,
- },
-
- /** @type {?string} */
- _errorMessage: {
- type: String,
- value: null,
- },
-
- /** @type {?Object} */
- _baseImage: Object,
- /** @type {?Object} */
- _revisionImage: Object,
- /**
- * This is a DiffInfo object.
- */
- diff: {
- type: Object,
- notify: true,
- },
-
- /** @type {?Object} */
- _blame: {
- type: Object,
- value: null,
- },
-
- /**
- * @type {!Array<!Gerrit.CoverageRange>}
- */
- _coverageRanges: {
- type: Array,
- value: () => [],
- },
-
- _loadedWhitespaceLevel: String,
-
- _parentIndex: {
- type: Number,
- computed: '_computeParentIndex(patchRange.*)',
- },
-
- _syntaxHighlightingEnabled: {
- type: Boolean,
- computed:
- '_isSyntaxHighlightingEnabled(prefs.*, diff)',
- },
-
- _layers: {
- type: Array,
- value: [],
- },
- };
- }
-
- static get observers() {
- return [
- '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' +
- ' noRenderOnPrefsChange)',
- '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)',
- ];
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener(
- // These are named inconsistently for a reason:
- // The create-comment event is fired to indicate that we should
- // create a comment.
- // The comment-* events are just notifying that the comments did already
- // change in some way, and that we should update any models we may want
- // to keep in sync.
- 'create-comment',
- e => this._handleCreateComment(e));
- this.addEventListener('comment-discard',
- e => this._handleCommentDiscard(e));
- this.addEventListener('comment-update',
- e => this._handleCommentUpdate(e));
- this.addEventListener('comment-save',
- e => this._handleCommentSave(e));
- this.addEventListener('render-start',
- () => this._handleRenderStart());
- this.addEventListener('render-content',
- () => this._handleRenderContent());
- this.addEventListener('normalize-range',
- event => this._handleNormalizeRange(event));
- this.addEventListener('diff-context-expanded',
- event => this._handleDiffContextExpanded(event));
- }
-
- /** @override */
- ready() {
- super.ready();
- if (this._canReload()) {
- this.reload();
- }
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
+ /**
+ * Gets or creates a comment thread at a given location.
+ * May provide a range, to get/create a range comment.
+ *
+ * @param {string} patchNum
+ * @param {?number} lineNum
+ * @param {string} commentSide
+ * @param {Gerrit.Range|undefined} range
+ * @param {boolean} isOnParent
+ * @return {!Object}
+ */
+ _getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) {
+ let threadEl = this._getThreadEl(lineNum, commentSide, range);
+ if (!threadEl) {
+ threadEl = this._createThreadElement({
+ comments: [],
+ commentSide,
+ patchNum,
+ lineNum,
+ range,
+ isOnParent,
});
+ this._attachThreadElement(threadEl);
+ }
+ return threadEl;
+ }
+
+ _attachThreadElement(threadEl) {
+ dom(this.$.diff).appendChild(threadEl);
+ }
+
+ _clearThreads() {
+ for (const threadEl of this.getThreadEls()) {
+ const parent = dom(threadEl).parentNode;
+ dom(parent).removeChild(threadEl);
+ }
+ }
+
+ _createThreadElement(thread) {
+ const threadEl = document.createElement('gr-comment-thread');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`);
+ threadEl.comments = thread.comments;
+ threadEl.commentSide = thread.commentSide;
+ threadEl.isOnParent = !!thread.isOnParent;
+ threadEl.parentIndex = this._parentIndex;
+ threadEl.changeNum = this.changeNum;
+ threadEl.patchNum = thread.patchNum;
+ threadEl.lineNum = thread.lineNum;
+ const rootIdChangedListener = changeEvent => {
+ thread.rootId = changeEvent.detail.value;
+ };
+ threadEl.addEventListener('root-id-changed', rootIdChangedListener);
+ threadEl.path = this.path;
+ threadEl.projectName = this.projectName;
+ threadEl.range = thread.range;
+ const threadDiscardListener = e => {
+ const threadEl = /** @type {!Node} */ (e.currentTarget);
+
+ const parent = dom(threadEl).parentNode;
+ dom(parent).removeChild(threadEl);
+
+ threadEl.removeEventListener('root-id-changed', rootIdChangedListener);
+ threadEl.removeEventListener('thread-discard', threadDiscardListener);
+ };
+ threadEl.addEventListener('thread-discard', threadDiscardListener);
+ return threadEl;
+ }
+
+ /**
+ * Gets a comment thread element at a given location.
+ * May provide a range, to get a range comment.
+ *
+ * @param {?number} lineNum
+ * @param {string} commentSide
+ * @param {!Gerrit.Range=} range
+ * @return {?Node}
+ */
+ _getThreadEl(lineNum, commentSide, range = undefined) {
+ let line;
+ if (commentSide === GrDiffBuilder.Side.LEFT) {
+ line = {beforeNumber: lineNum};
+ } else if (commentSide === GrDiffBuilder.Side.RIGHT) {
+ line = {afterNumber: lineNum};
+ } else {
+ throw new Error(`Unknown side: ${commentSide}`);
+ }
+ function matchesRange(threadEl) {
+ const threadRange = /** @type {!Gerrit.Range} */(
+ JSON.parse(threadEl.getAttribute('range')));
+ return Gerrit.rangesEqual(threadRange, range);
}
- /**
- * @param {boolean=} shouldReportMetric indicate a new Diff Page. This is a
- * signal to report metrics event that started on location change.
- * @return {!Promise}
- **/
- reload(shouldReportMetric) {
- this._loading = true;
- this._errorMessage = null;
- const whitespaceLevel = this._getIgnoreWhitespace();
+ const filteredThreadEls = this._filterThreadElsForLocation(
+ this.getThreadEls(), line, commentSide).filter(matchesRange);
+ return filteredThreadEls.length ? filteredThreadEls[0] : null;
+ }
- const layers = [this.$.syntaxLayer];
- // Get layers from plugins (if any).
- for (const pluginLayer of this.$.jsAPI.getDiffLayers(
- this.path, this.changeNum, this.patchNum)) {
- layers.push(pluginLayer);
- }
- this._layers = layers;
-
- if (shouldReportMetric) {
- // We listen on render viewport only on DiffPage (on paramsChanged)
- this._listenToViewportRender();
- }
-
- this._coverageRanges = [];
- this._getCoverageData();
- const diffRequest = this._getDiff()
- .then(diff => {
- this._loadedWhitespaceLevel = whitespaceLevel;
- this._reportDiff(diff);
- return diff;
- })
- .catch(e => {
- this._handleGetDiffError(e);
- return null;
- });
-
- const assetRequest = diffRequest.then(diff => {
- // If the diff is null, then it's failed to load.
- if (!diff) { return null; }
-
- return this._loadDiffAssets(diff);
- });
-
- // Not waiting for coverage ranges intentionally as
- // plugin loading should not block the content rendering
- return Promise.all([diffRequest, assetRequest])
- .then(results => {
- const diff = results[0];
- if (!diff) {
- return Promise.resolve();
- }
- this.filesWeblinks = this._getFilesWeblinks(diff);
- return new Promise(resolve => {
- const callback = event => {
- const needsSyntaxHighlighting = event.detail &&
- event.detail.contentRendered;
- if (needsSyntaxHighlighting) {
- this.$.reporting.time(TimingLabel.SYNTAX);
- this.$.syntaxLayer.process().then(() => {
- this.$.reporting.timeEnd(TimingLabel.SYNTAX);
- this.$.reporting.timeEnd(TimingLabel.TOTAL);
- resolve();
- });
- } else {
- this.$.reporting.timeEnd(TimingLabel.TOTAL);
- resolve();
- }
- this.removeEventListener('render', callback);
- if (shouldReportMetric) {
- // We report diffViewContentDisplayed only on reload caused
- // by params changed - expected only on Diff Page.
- this.$.reporting.diffViewContentDisplayed();
- }
- };
- this.addEventListener('render', callback);
- this.diff = diff;
- });
- })
- .catch(err => {
- console.warn('Error encountered loading diff:', err);
- })
- .then(() => { this._loading = false; });
+ /**
+ * @param {!Array<!HTMLElement>} threadEls
+ * @param {!{beforeNumber: (number|string|undefined|null),
+ * afterNumber: (number|string|undefined|null)}}
+ * lineInfo
+ * @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for
+ * which to return the threads.
+ * @return {!Array<!HTMLElement>} The thread elements matching the given
+ * location.
+ */
+ _filterThreadElsForLocation(threadEls, lineInfo, side) {
+ function matchesLeftLine(threadEl) {
+ return threadEl.getAttribute('comment-side') ==
+ Gerrit.DiffSide.LEFT &&
+ threadEl.getAttribute('line-num') == lineInfo.beforeNumber;
+ }
+ function matchesRightLine(threadEl) {
+ return threadEl.getAttribute('comment-side') ==
+ Gerrit.DiffSide.RIGHT &&
+ threadEl.getAttribute('line-num') == lineInfo.afterNumber;
+ }
+ function matchesFileComment(threadEl) {
+ return threadEl.getAttribute('comment-side') == side &&
+ // line/range comments have 1-based line set, if line is falsy it's
+ // a file comment
+ !threadEl.getAttribute('line-num');
}
- _getCoverageData() {
- const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this;
- this.$.jsAPI.getCoverageAnnotationApi().
- then(coverageAnnotationApi => {
- if (!coverageAnnotationApi) return;
- const provider = coverageAnnotationApi.getCoverageProvider();
- return provider(changeNum, path, basePatchNum, patchNum)
- .then(coverageRanges => {
- if (!coverageRanges ||
- changeNum !== this.changeNum ||
- path !== this.path ||
- basePatchNum !== this.patchRange.basePatchNum ||
- patchNum !== this.patchRange.patchNum) {
- return;
- }
+ // Select the appropriate matchers for the desired side and line
+ // If side is BOTH, we want both the left and right matcher.
+ const matchers = [];
+ if (side !== Gerrit.DiffSide.RIGHT) {
+ matchers.push(matchesLeftLine);
+ }
+ if (side !== Gerrit.DiffSide.LEFT) {
+ matchers.push(matchesRightLine);
+ }
+ if (lineInfo.afterNumber === 'FILE' ||
+ lineInfo.beforeNumber === 'FILE') {
+ matchers.push(matchesFileComment);
+ }
+ return threadEls.filter(threadEl =>
+ matchers.some(matcher => matcher(threadEl)));
+ }
- const existingCoverageRanges = this._coverageRanges;
- this._coverageRanges = coverageRanges;
+ _getIgnoreWhitespace() {
+ if (!this.prefs || !this.prefs.ignore_whitespace) {
+ return WHITESPACE_IGNORE_NONE;
+ }
+ return this.prefs.ignore_whitespace;
+ }
- // Notify with existing coverage ranges
- // in case there is some existing coverage data that needs to be removed
- existingCoverageRanges.forEach(range => {
- coverageAnnotationApi.notify(
- path,
- range.code_range.start_line,
- range.code_range.end_line,
- range.side);
- });
-
- // Notify with new coverage data
- coverageRanges.forEach(range => {
- coverageAnnotationApi.notify(
- path,
- range.code_range.start_line,
- range.code_range.end_line,
- range.side);
- });
- });
- })
- .catch(err => {
- console.warn('Loading coverage ranges failed: ', err);
- });
+ _whitespaceChanged(
+ preferredWhitespaceLevel, loadedWhitespaceLevel,
+ noRenderOnPrefsChange) {
+ // Polymer 2: check for undefined
+ if ([
+ preferredWhitespaceLevel,
+ loadedWhitespaceLevel,
+ noRenderOnPrefsChange,
+ ].some(arg => arg === undefined)) {
+ return;
}
- _getFilesWeblinks(diff) {
- if (!this.commitRange) {
- return {};
- }
- return {
- meta_a: Gerrit.Nav.getFileWebLinks(
- this.projectName, this.commitRange.baseCommit, this.path,
- {weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
- meta_b: Gerrit.Nav.getFileWebLinks(
- this.projectName, this.commitRange.commit, this.path,
- {weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
- };
+ if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
+ !noRenderOnPrefsChange) {
+ this.reload();
+ }
+ }
+
+ _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) {
+ // Polymer 2: check for undefined
+ if ([
+ noRenderOnPrefsChange,
+ prefsChangeRecord,
+ ].some(arg => arg === undefined)) {
+ return;
}
- /** Cancel any remaining diff builder rendering work. */
- cancel() {
- this.$.diff.cancel();
+ if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') {
+ return;
}
- /** @return {!Array<!HTMLElement>} */
- getCursorStops() {
- return this.$.diff.getCursorStops();
+ if (!noRenderOnPrefsChange) {
+ this.reload();
}
+ }
- /** @return {boolean} */
- isRangeSelected() {
- return this.$.diff.isRangeSelected();
+ /**
+ * @param {Object} patchRangeRecord
+ * @return {number|null}
+ */
+ _computeParentIndex(patchRangeRecord) {
+ return this.isMergeParent(patchRangeRecord.base.basePatchNum) ?
+ this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
+ }
+
+ _handleCommentSave(e) {
+ const comment = e.detail.comment;
+ const side = e.detail.comment.__commentSide;
+ const idx = this._findDraftIndex(comment, side);
+ this.set(['comments', side, idx], comment);
+ this._handleCommentSaveOrDiscard();
+ }
+
+ _handleCommentDiscard(e) {
+ const comment = e.detail.comment;
+ this._removeComment(comment);
+ this._handleCommentSaveOrDiscard();
+ }
+
+ /**
+ * Closure annotation for Polymer.prototype.push is off. Submitted PR:
+ * https://github.com/Polymer/polymer/pull/4776
+ * but for not supressing annotations.
+ *
+ * @suppress {checkTypes}
+ */
+ _handleCommentUpdate(e) {
+ const comment = e.detail.comment;
+ const side = e.detail.comment.__commentSide;
+ let idx = this._findCommentIndex(comment, side);
+ if (idx === -1) {
+ idx = this._findDraftIndex(comment, side);
}
-
- createRangeComment() {
- return this.$.diff.createRangeComment();
- }
-
- toggleLeftDiff() {
- this.$.diff.toggleLeftDiff();
- }
-
- /**
- * Load and display blame information for the base of the diff.
- *
- * @return {Promise} A promise that resolves when blame finishes rendering.
- */
- loadBlame() {
- return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
- this.path, true)
- .then(blame => {
- if (!blame.length) {
- this.fire('show-alert', {message: MSG_EMPTY_BLAME});
- return Promise.reject(MSG_EMPTY_BLAME);
- }
-
- this._blame = blame;
- });
- }
-
- /** Unload blame information for the diff. */
- clearBlame() {
- this._blame = null;
- }
-
- /**
- * The thread elements in this diff, in no particular order.
- *
- * @return {!Array<!HTMLElement>}
- */
- getThreadEls() {
- return Array.from(
- Polymer.dom(this.$.diff).querySelectorAll('.comment-thread'));
- }
-
- /** @param {HTMLElement} el */
- addDraftAtLine(el) {
- this.$.diff.addDraftAtLine(el);
- }
-
- clearDiffContent() {
- this.$.diff.clearDiffContent();
- }
-
- expandAllContext() {
- this.$.diff.expandAllContext();
- }
-
- /** @return {!Promise} */
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- /** @return {boolean}} */
- _canReload() {
- return !!this.changeNum && !!this.patchRange && !!this.path &&
- !this.noAutoRender;
- }
-
- /** @return {!Promise<!Object>} */
- _getDiff() {
- // Wrap the diff request in a new promise so that the error handler
- // rejects the promise, allowing the error to be handled in the .catch.
- return new Promise((resolve, reject) => {
- this.$.restAPI.getDiff(
- this.changeNum,
- this.patchRange.basePatchNum,
- this.patchRange.patchNum,
- this.path,
- this._getIgnoreWhitespace(),
- reject)
- .then(resolve);
- });
- }
-
- _handleGetDiffError(response) {
- // Loading the diff may respond with 409 if the file is too large. In this
- // case, use a toast error..
- if (response.status === 409) {
- this.fire('server-error', {response});
- return;
- }
-
- if (this.showLoadFailure) {
- this._errorMessage = [
- 'Encountered error when loading the diff:',
- response.status,
- response.statusText,
- ].join(' ');
- return;
- }
-
- this.fire('page-error', {response});
- }
-
- /**
- * Report info about the diff response.
- */
- _reportDiff(diff) {
- if (!diff || !diff.content) {
- return;
- }
-
- // Count the delta lines stemming from normal deltas, and from
- // due_to_rebase deltas.
- let nonRebaseDelta = 0;
- let rebaseDelta = 0;
- diff.content.forEach(chunk => {
- if (chunk.ab) { return; }
- const deltaSize = Math.max(
- chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
- if (chunk.due_to_rebase) {
- rebaseDelta += deltaSize;
- } else {
- nonRebaseDelta += deltaSize;
- }
- });
-
- // Find the percent of the delta from due_to_rebase chunks rounded to two
- // digits. Diffs with no delta are considered 0%.
- const totalDelta = rebaseDelta + nonRebaseDelta;
- const percentRebaseDelta = !totalDelta ? 0 :
- Math.round(100 * rebaseDelta / totalDelta);
-
- // Report the due_to_rebase percentage in the "diff" category when
- // applicable.
- if (this.patchRange.basePatchNum === 'PARENT') {
- this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
- } else if (percentRebaseDelta === 0) {
- this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
- } else {
- this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
- {percentRebaseDelta});
- }
- }
-
- /**
- * @param {Object} diff
- * @return {!Promise}
- */
- _loadDiffAssets(diff) {
- if (isImageDiff(diff)) {
- return this._getImages(diff).then(images => {
- this._baseImage = images.baseImage;
- this._revisionImage = images.revisionImage;
- });
- } else {
- this._baseImage = null;
- this._revisionImage = null;
- return Promise.resolve();
- }
- }
-
- /**
- * @param {Object} diff
- * @return {boolean}
- */
- _computeIsImageDiff(diff) {
- return isImageDiff(diff);
- }
-
- _commentsChanged(newComments) {
- const allComments = [];
- for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) {
- // This is needed by the threading.
- for (const comment of newComments[side]) {
- comment.__commentSide = side;
- }
- allComments.push(...newComments[side]);
- }
- // Currently, the only way this is ever changed here is when the initial
- // comments are loaded, so it's okay performance wise to clear the threads
- // and recreate them. If this changes in future, we might want to reuse
- // some DOM nodes here.
- this._clearThreads();
- const threads = this._createThreads(allComments);
- for (const thread of threads) {
- const threadEl = this._createThreadElement(thread);
- this._attachThreadElement(threadEl);
- }
- }
-
- _sortComments(comments) {
- return comments.slice(0).sort((a, b) => {
- if (b.__draft && !a.__draft ) { return -1; }
- if (a.__draft && !b.__draft ) { return 1; }
- return util.parseDate(a.updated) - util.parseDate(b.updated);
- });
- }
-
- /**
- * @param {!Array<!Object>} comments
- * @return {!Array<!Object>} Threads for the given comments.
- */
- _createThreads(comments) {
- const sortedComments = this._sortComments(comments);
- const threads = [];
- for (const comment of sortedComments) {
- // If the comment is in reply to another comment, find that comment's
- // thread and append to it.
- if (comment.in_reply_to) {
- const thread = threads.find(thread =>
- thread.comments.some(c => c.id === comment.in_reply_to));
- if (thread) {
- thread.comments.push(comment);
- continue;
- }
- }
-
- // Otherwise, this comment starts its own thread.
- const newThread = {
- start_datetime: comment.updated,
- comments: [comment],
- commentSide: comment.__commentSide,
- patchNum: comment.patch_set,
- rootId: comment.id || comment.__draftID,
- lineNum: comment.line,
- isOnParent: comment.side === 'PARENT',
- };
- if (comment.range) {
- newThread.range = Object.assign({}, comment.range);
- }
- threads.push(newThread);
- }
- return threads;
- }
-
- /**
- * @param {Object} blame
- * @return {boolean}
- */
- _computeIsBlameLoaded(blame) {
- return !!blame;
- }
-
- /**
- * @param {Object} diff
- * @return {!Promise}
- */
- _getImages(diff) {
- return this.$.restAPI.getImagesForDiff(this.changeNum, diff,
- this.patchRange);
- }
-
- /** @param {CustomEvent} e */
- _handleCreateComment(e) {
- const {lineNum, side, patchNum, isOnParent, range} = e.detail;
- const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range,
- isOnParent);
- threadEl.addOrEditDraft(lineNum, range);
-
- this.$.reporting.recordDraftInteraction();
- }
-
- /**
- * Gets or creates a comment thread at a given location.
- * May provide a range, to get/create a range comment.
- *
- * @param {string} patchNum
- * @param {?number} lineNum
- * @param {string} commentSide
- * @param {Gerrit.Range|undefined} range
- * @param {boolean} isOnParent
- * @return {!Object}
- */
- _getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) {
- let threadEl = this._getThreadEl(lineNum, commentSide, range);
- if (!threadEl) {
- threadEl = this._createThreadElement({
- comments: [],
- commentSide,
- patchNum,
- lineNum,
- range,
- isOnParent,
- });
- this._attachThreadElement(threadEl);
- }
- return threadEl;
- }
-
- _attachThreadElement(threadEl) {
- Polymer.dom(this.$.diff).appendChild(threadEl);
- }
-
- _clearThreads() {
- for (const threadEl of this.getThreadEls()) {
- const parent = Polymer.dom(threadEl).parentNode;
- Polymer.dom(parent).removeChild(threadEl);
- }
- }
-
- _createThreadElement(thread) {
- const threadEl = document.createElement('gr-comment-thread');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`);
- threadEl.comments = thread.comments;
- threadEl.commentSide = thread.commentSide;
- threadEl.isOnParent = !!thread.isOnParent;
- threadEl.parentIndex = this._parentIndex;
- threadEl.changeNum = this.changeNum;
- threadEl.patchNum = thread.patchNum;
- threadEl.lineNum = thread.lineNum;
- const rootIdChangedListener = changeEvent => {
- thread.rootId = changeEvent.detail.value;
- };
- threadEl.addEventListener('root-id-changed', rootIdChangedListener);
- threadEl.path = this.path;
- threadEl.projectName = this.projectName;
- threadEl.range = thread.range;
- const threadDiscardListener = e => {
- const threadEl = /** @type {!Node} */ (e.currentTarget);
-
- const parent = Polymer.dom(threadEl).parentNode;
- Polymer.dom(parent).removeChild(threadEl);
-
- threadEl.removeEventListener('root-id-changed', rootIdChangedListener);
- threadEl.removeEventListener('thread-discard', threadDiscardListener);
- };
- threadEl.addEventListener('thread-discard', threadDiscardListener);
- return threadEl;
- }
-
- /**
- * Gets a comment thread element at a given location.
- * May provide a range, to get a range comment.
- *
- * @param {?number} lineNum
- * @param {string} commentSide
- * @param {!Gerrit.Range=} range
- * @return {?Node}
- */
- _getThreadEl(lineNum, commentSide, range = undefined) {
- let line;
- if (commentSide === GrDiffBuilder.Side.LEFT) {
- line = {beforeNumber: lineNum};
- } else if (commentSide === GrDiffBuilder.Side.RIGHT) {
- line = {afterNumber: lineNum};
- } else {
- throw new Error(`Unknown side: ${commentSide}`);
- }
- function matchesRange(threadEl) {
- const threadRange = /** @type {!Gerrit.Range} */(
- JSON.parse(threadEl.getAttribute('range')));
- return Gerrit.rangesEqual(threadRange, range);
- }
-
- const filteredThreadEls = this._filterThreadElsForLocation(
- this.getThreadEls(), line, commentSide).filter(matchesRange);
- return filteredThreadEls.length ? filteredThreadEls[0] : null;
- }
-
- /**
- * @param {!Array<!HTMLElement>} threadEls
- * @param {!{beforeNumber: (number|string|undefined|null),
- * afterNumber: (number|string|undefined|null)}}
- * lineInfo
- * @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for
- * which to return the threads.
- * @return {!Array<!HTMLElement>} The thread elements matching the given
- * location.
- */
- _filterThreadElsForLocation(threadEls, lineInfo, side) {
- function matchesLeftLine(threadEl) {
- return threadEl.getAttribute('comment-side') ==
- Gerrit.DiffSide.LEFT &&
- threadEl.getAttribute('line-num') == lineInfo.beforeNumber;
- }
- function matchesRightLine(threadEl) {
- return threadEl.getAttribute('comment-side') ==
- Gerrit.DiffSide.RIGHT &&
- threadEl.getAttribute('line-num') == lineInfo.afterNumber;
- }
- function matchesFileComment(threadEl) {
- return threadEl.getAttribute('comment-side') == side &&
- // line/range comments have 1-based line set, if line is falsy it's
- // a file comment
- !threadEl.getAttribute('line-num');
- }
-
- // Select the appropriate matchers for the desired side and line
- // If side is BOTH, we want both the left and right matcher.
- const matchers = [];
- if (side !== Gerrit.DiffSide.RIGHT) {
- matchers.push(matchesLeftLine);
- }
- if (side !== Gerrit.DiffSide.LEFT) {
- matchers.push(matchesRightLine);
- }
- if (lineInfo.afterNumber === 'FILE' ||
- lineInfo.beforeNumber === 'FILE') {
- matchers.push(matchesFileComment);
- }
- return threadEls.filter(threadEl =>
- matchers.some(matcher => matcher(threadEl)));
- }
-
- _getIgnoreWhitespace() {
- if (!this.prefs || !this.prefs.ignore_whitespace) {
- return WHITESPACE_IGNORE_NONE;
- }
- return this.prefs.ignore_whitespace;
- }
-
- _whitespaceChanged(
- preferredWhitespaceLevel, loadedWhitespaceLevel,
- noRenderOnPrefsChange) {
- // Polymer 2: check for undefined
- if ([
- preferredWhitespaceLevel,
- loadedWhitespaceLevel,
- noRenderOnPrefsChange,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
- !noRenderOnPrefsChange) {
- this.reload();
- }
- }
-
- _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) {
- // Polymer 2: check for undefined
- if ([
- noRenderOnPrefsChange,
- prefsChangeRecord,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') {
- return;
- }
-
- if (!noRenderOnPrefsChange) {
- this.reload();
- }
- }
-
- /**
- * @param {Object} patchRangeRecord
- * @return {number|null}
- */
- _computeParentIndex(patchRangeRecord) {
- return this.isMergeParent(patchRangeRecord.base.basePatchNum) ?
- this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
- }
-
- _handleCommentSave(e) {
- const comment = e.detail.comment;
- const side = e.detail.comment.__commentSide;
- const idx = this._findDraftIndex(comment, side);
+ if (idx !== -1) { // Update draft or comment.
this.set(['comments', side, idx], comment);
- this._handleCommentSaveOrDiscard();
- }
-
- _handleCommentDiscard(e) {
- const comment = e.detail.comment;
- this._removeComment(comment);
- this._handleCommentSaveOrDiscard();
- }
-
- /**
- * Closure annotation for Polymer.prototype.push is off. Submitted PR:
- * https://github.com/Polymer/polymer/pull/4776
- * but for not supressing annotations.
- *
- * @suppress {checkTypes}
- */
- _handleCommentUpdate(e) {
- const comment = e.detail.comment;
- const side = e.detail.comment.__commentSide;
- let idx = this._findCommentIndex(comment, side);
- if (idx === -1) {
- idx = this._findDraftIndex(comment, side);
- }
- if (idx !== -1) { // Update draft or comment.
- this.set(['comments', side, idx], comment);
- } else { // Create new draft.
- this.push(['comments', side], comment);
- }
- }
-
- _handleCommentSaveOrDiscard() {
- this.dispatchEvent(new CustomEvent(
- 'diff-comments-modified', {bubbles: true, composed: true}));
- }
-
- _removeComment(comment) {
- const side = comment.__commentSide;
- this._removeCommentFromSide(comment, side);
- }
-
- _removeCommentFromSide(comment, side) {
- let idx = this._findCommentIndex(comment, side);
- if (idx === -1) {
- idx = this._findDraftIndex(comment, side);
- }
- if (idx !== -1) {
- this.splice('comments.' + side, idx, 1);
- }
- }
-
- /** @return {number} */
- _findCommentIndex(comment, side) {
- if (!comment.id || !this.comments[side]) {
- return -1;
- }
- return this.comments[side].findIndex(item => item.id === comment.id);
- }
-
- /** @return {number} */
- _findDraftIndex(comment, side) {
- if (!comment.__draftID || !this.comments[side]) {
- return -1;
- }
- return this.comments[side].findIndex(
- item => item.__draftID === comment.__draftID);
- }
-
- _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) {
- if (!preferenceChangeRecord ||
- !preferenceChangeRecord.base ||
- !preferenceChangeRecord.base.syntax_highlighting ||
- !diff) {
- return false;
- }
- return !this._anyLineTooLong(diff) &&
- this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH;
- }
-
- /**
- * @return {boolean} whether any of the lines in diff are longer
- * than SYNTAX_MAX_LINE_LENGTH.
- */
- _anyLineTooLong(diff) {
- if (!diff) return false;
- return diff.content.some(section => {
- const lines = section.ab ?
- section.ab :
- (section.a || []).concat(section.b || []);
- return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH);
- });
- }
-
- _listenToViewportRender() {
- const renderUpdateListener = start => {
- if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) {
- this.$.reporting.diffViewDisplayed();
- this.$.syntaxLayer.removeListener(renderUpdateListener);
- }
- };
-
- this.$.syntaxLayer.addListener(renderUpdateListener);
- }
-
- _handleRenderStart() {
- this.$.reporting.time(TimingLabel.TOTAL);
- this.$.reporting.time(TimingLabel.CONTENT);
- }
-
- _handleRenderContent() {
- this.$.reporting.timeEnd(TimingLabel.CONTENT);
- }
-
- _handleNormalizeRange(event) {
- this.$.reporting.reportInteraction('normalize-range',
- {
- side: event.detail.side,
- lineNum: event.detail.lineNum,
- });
- }
-
- _handleDiffContextExpanded(event) {
- this.$.reporting.reportInteraction(
- 'diff-context-expanded', {numLines: event.detail.numLines}
- );
- }
-
- /**
- * Find the last chunk for the given side.
- *
- * @param {!Object} diff
- * @param {boolean} leftSide true if checking the base of the diff,
- * false if testing the revision.
- * @return {Object|null} returns the chunk object or null if there was
- * no chunk for that side.
- */
- _lastChunkForSide(diff, leftSide) {
- if (!diff.content.length) { return null; }
-
- let chunkIndex = diff.content.length;
- let chunk;
-
- // Walk backwards until we find a chunk for the given side.
- do {
- chunkIndex--;
- chunk = diff.content[chunkIndex];
- } while (
- // We haven't reached the beginning.
- chunkIndex >= 0 &&
-
- // The chunk doesn't have both sides.
- !chunk.ab &&
-
- // The chunk doesn't have the given side.
- ((leftSide && (!chunk.a || !chunk.a.length)) ||
- (!leftSide && (!chunk.b || !chunk.b.length))));
-
- // If we reached the beginning of the diff and failed to find a chunk
- // with the given side, return null.
- if (chunkIndex === -1) { return null; }
-
- return chunk;
- }
-
- /**
- * Check whether the specified side of the diff has a trailing newline.
- *
- * @param {!Object} diff
- * @param {boolean} leftSide true if checking the base of the diff,
- * false if testing the revision.
- * @return {boolean|null} Return true if the side has a trailing newline.
- * Return false if it doesn't. Return null if not applicable (for
- * example, if the diff has no content on the specified side).
- */
- _hasTrailingNewlines(diff, leftSide) {
- const chunk = this._lastChunkForSide(diff, leftSide);
- if (!chunk) { return null; }
- let lines;
- if (chunk.ab) {
- lines = chunk.ab;
- } else {
- lines = leftSide ? chunk.a : chunk.b;
- }
- return lines[lines.length - 1] === '';
- }
-
- _showNewlineWarningLeft(diff) {
- return this._hasTrailingNewlines(diff, true) === false;
- }
-
- _showNewlineWarningRight(diff) {
- return this._hasTrailingNewlines(diff, false) === false;
+ } else { // Create new draft.
+ this.push(['comments', side], comment);
}
}
- customElements.define(GrDiffHost.is, GrDiffHost);
-})();
+ _handleCommentSaveOrDiscard() {
+ this.dispatchEvent(new CustomEvent(
+ 'diff-comments-modified', {bubbles: true, composed: true}));
+ }
+
+ _removeComment(comment) {
+ const side = comment.__commentSide;
+ this._removeCommentFromSide(comment, side);
+ }
+
+ _removeCommentFromSide(comment, side) {
+ let idx = this._findCommentIndex(comment, side);
+ if (idx === -1) {
+ idx = this._findDraftIndex(comment, side);
+ }
+ if (idx !== -1) {
+ this.splice('comments.' + side, idx, 1);
+ }
+ }
+
+ /** @return {number} */
+ _findCommentIndex(comment, side) {
+ if (!comment.id || !this.comments[side]) {
+ return -1;
+ }
+ return this.comments[side].findIndex(item => item.id === comment.id);
+ }
+
+ /** @return {number} */
+ _findDraftIndex(comment, side) {
+ if (!comment.__draftID || !this.comments[side]) {
+ return -1;
+ }
+ return this.comments[side].findIndex(
+ item => item.__draftID === comment.__draftID);
+ }
+
+ _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) {
+ if (!preferenceChangeRecord ||
+ !preferenceChangeRecord.base ||
+ !preferenceChangeRecord.base.syntax_highlighting ||
+ !diff) {
+ return false;
+ }
+ return !this._anyLineTooLong(diff) &&
+ this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH;
+ }
+
+ /**
+ * @return {boolean} whether any of the lines in diff are longer
+ * than SYNTAX_MAX_LINE_LENGTH.
+ */
+ _anyLineTooLong(diff) {
+ if (!diff) return false;
+ return diff.content.some(section => {
+ const lines = section.ab ?
+ section.ab :
+ (section.a || []).concat(section.b || []);
+ return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH);
+ });
+ }
+
+ _listenToViewportRender() {
+ const renderUpdateListener = start => {
+ if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) {
+ this.$.reporting.diffViewDisplayed();
+ this.$.syntaxLayer.removeListener(renderUpdateListener);
+ }
+ };
+
+ this.$.syntaxLayer.addListener(renderUpdateListener);
+ }
+
+ _handleRenderStart() {
+ this.$.reporting.time(TimingLabel.TOTAL);
+ this.$.reporting.time(TimingLabel.CONTENT);
+ }
+
+ _handleRenderContent() {
+ this.$.reporting.timeEnd(TimingLabel.CONTENT);
+ }
+
+ _handleNormalizeRange(event) {
+ this.$.reporting.reportInteraction('normalize-range',
+ {
+ side: event.detail.side,
+ lineNum: event.detail.lineNum,
+ });
+ }
+
+ _handleDiffContextExpanded(event) {
+ this.$.reporting.reportInteraction(
+ 'diff-context-expanded', {numLines: event.detail.numLines}
+ );
+ }
+
+ /**
+ * Find the last chunk for the given side.
+ *
+ * @param {!Object} diff
+ * @param {boolean} leftSide true if checking the base of the diff,
+ * false if testing the revision.
+ * @return {Object|null} returns the chunk object or null if there was
+ * no chunk for that side.
+ */
+ _lastChunkForSide(diff, leftSide) {
+ if (!diff.content.length) { return null; }
+
+ let chunkIndex = diff.content.length;
+ let chunk;
+
+ // Walk backwards until we find a chunk for the given side.
+ do {
+ chunkIndex--;
+ chunk = diff.content[chunkIndex];
+ } while (
+ // We haven't reached the beginning.
+ chunkIndex >= 0 &&
+
+ // The chunk doesn't have both sides.
+ !chunk.ab &&
+
+ // The chunk doesn't have the given side.
+ ((leftSide && (!chunk.a || !chunk.a.length)) ||
+ (!leftSide && (!chunk.b || !chunk.b.length))));
+
+ // If we reached the beginning of the diff and failed to find a chunk
+ // with the given side, return null.
+ if (chunkIndex === -1) { return null; }
+
+ return chunk;
+ }
+
+ /**
+ * Check whether the specified side of the diff has a trailing newline.
+ *
+ * @param {!Object} diff
+ * @param {boolean} leftSide true if checking the base of the diff,
+ * false if testing the revision.
+ * @return {boolean|null} Return true if the side has a trailing newline.
+ * Return false if it doesn't. Return null if not applicable (for
+ * example, if the diff has no content on the specified side).
+ */
+ _hasTrailingNewlines(diff, leftSide) {
+ const chunk = this._lastChunkForSide(diff, leftSide);
+ if (!chunk) { return null; }
+ let lines;
+ if (chunk.ab) {
+ lines = chunk.ab;
+ } else {
+ lines = leftSide ? chunk.a : chunk.b;
+ }
+ return lines[lines.length - 1] === '';
+ }
+
+ _showNewlineWarningLeft(diff) {
+ return this._hasTrailingNewlines(diff, true) === false;
+ }
+
+ _showNewlineWarningRight(diff) {
+ return this._hasTrailingNewlines(diff, false) === false;
+ }
+}
+
+customElements.define(GrDiffHost.is, GrDiffHost);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
index 2d9369f..d48531b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
@@ -1,67 +1,26 @@
-<!--
-@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="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../gr-diff/gr-diff.html">
-<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
-
-<dom-module id="gr-diff-host">
- <template>
- <gr-diff
- id="diff"
- change-num="[[changeNum]]"
- no-auto-render=[[noAutoRender]]
- patch-range="[[patchRange]]"
- path="[[path]]"
- prefs="[[prefs]]"
- project-name="[[projectName]]"
- display-line="[[displayLine]]"
- is-image-diff="[[isImageDiff]]"
- commit-range="[[commitRange]]"
- hidden$="[[hidden]]"
- no-render-on-prefs-change="[[noRenderOnPrefsChange]]"
- line-wrapping="[[lineWrapping]]"
- view-mode="[[viewMode]]"
- line-of-interest="[[lineOfInterest]]"
- logged-in="[[_loggedIn]]"
- loading="[[_loading]]"
- error-message="[[_errorMessage]]"
- base-image="[[_baseImage]]"
- revision-image=[[_revisionImage]]
- coverage-ranges="[[_coverageRanges]]"
- blame="[[_blame]]"
- layers="[[_layers]]"
- diff="[[diff]]"
- show-newline-warning-left="[[_showNewlineWarningLeft(diff)]]"
- show-newline-warning-right="[[_showNewlineWarningRight(diff)]]">
+export const htmlTemplate = html`
+ <gr-diff id="diff" change-num="[[changeNum]]" no-auto-render="[[noAutoRender]]" patch-range="[[patchRange]]" path="[[path]]" prefs="[[prefs]]" project-name="[[projectName]]" display-line="[[displayLine]]" is-image-diff="[[isImageDiff]]" commit-range="[[commitRange]]" hidden\$="[[hidden]]" no-render-on-prefs-change="[[noRenderOnPrefsChange]]" line-wrapping="[[lineWrapping]]" view-mode="[[viewMode]]" line-of-interest="[[lineOfInterest]]" logged-in="[[_loggedIn]]" loading="[[_loading]]" error-message="[[_errorMessage]]" base-image="[[_baseImage]]" revision-image="[[_revisionImage]]" coverage-ranges="[[_coverageRanges]]" blame="[[_blame]]" layers="[[_layers]]" diff="[[diff]]" show-newline-warning-left="[[_showNewlineWarningLeft(diff)]]" show-newline-warning-right="[[_showNewlineWarningRight(diff)]]">
</gr-diff>
- <gr-syntax-layer
- id="syntaxLayer"
- enabled="[[_syntaxHighlightingEnabled]]"
- diff="[[diff]]"></gr-syntax-layer>
+ <gr-syntax-layer id="syntaxLayer" enabled="[[_syntaxHighlightingEnabled]]" diff="[[diff]]"></gr-syntax-layer>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting" category="diff"></gr-reporting>
- </template>
- <script src="gr-diff-host.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index be2101c..d2097d3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</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>
-<link rel="import" href="gr-diff-host.html">
+<script type="module" src="./gr-diff-host.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-host.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,125 +41,50 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-host tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let getLoggedIn;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-host.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff-host tests', () => {
+ let element;
+ let sandbox;
+ let getLoggedIn;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ getLoggedIn = false;
+ stub('gr-rest-api-interface', {
+ async getLoggedIn() { return getLoggedIn; },
+ });
+ stub('gr-reporting', {
+ time: sandbox.stub(),
+ timeEnd: sandbox.stub(),
+ });
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('plugin layers', () => {
+ const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
setup(() => {
- sandbox = sinon.sandbox.create();
- getLoggedIn = false;
- stub('gr-rest-api-interface', {
- async getLoggedIn() { return getLoggedIn; },
- });
- stub('gr-reporting', {
- time: sandbox.stub(),
- timeEnd: sandbox.stub(),
+ stub('gr-js-api-interface', {
+ getDiffLayers() { return pluginLayers; },
});
element = fixture('basic');
});
-
- teardown(() => {
- sandbox.restore();
+ test('plugin layers requested', () => {
+ element.patchRange = {};
+ element.reload();
+ assert(element.$.jsAPI.getDiffLayers.called);
});
+ });
- suite('plugin layers', () => {
- const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
- setup(() => {
- stub('gr-js-api-interface', {
- getDiffLayers() { return pluginLayers; },
- });
- element = fixture('basic');
- });
- test('plugin layers requested', () => {
- element.patchRange = {};
- element.reload();
- assert(element.$.jsAPI.getDiffLayers.called);
- });
- });
-
- suite('handle comment-update', () => {
- setup(() => {
- sandbox.stub(element, '_commentsChanged');
- element.comments = {
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- {id: 'd2', __draft: true, __commentSide: 'right'},
- ],
- };
- });
-
- test('creating a draft', () => {
- const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
- __commentSide: 'left'};
- element.fire('comment-update', {comment});
- assert.include(element.comments.left, comment);
- });
-
- test('discarding a draft', () => {
- const draftID = 'tempID';
- const id = 'savedID';
- const comment = {
- __draft: true,
- __draftID: draftID,
- side: 'PARENT',
- __commentSide: 'left',
- };
- const diffCommentsModifiedStub = sandbox.stub();
- element.addEventListener('diff-comments-modified',
- diffCommentsModifiedStub);
- element.comments.left.push(comment);
- comment.id = id;
- element.fire('comment-discard', {comment});
- const drafts = element.comments.left
- .filter(item => item.__draftID === draftID);
- assert.equal(drafts.length, 0);
- assert.isTrue(diffCommentsModifiedStub.called);
- });
-
- test('saving a draft', () => {
- const draftID = 'tempID';
- const id = 'savedID';
- const comment = {
- __draft: true,
- __draftID: draftID,
- side: 'PARENT',
- __commentSide: 'left',
- };
- const diffCommentsModifiedStub = sandbox.stub();
- element.addEventListener('diff-comments-modified',
- diffCommentsModifiedStub);
- element.comments.left.push(comment);
- comment.id = id;
- element.fire('comment-save', {comment});
- const drafts = element.comments.left
- .filter(item => item.__draftID === draftID);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].id, id);
- assert.isTrue(diffCommentsModifiedStub.called);
- });
- });
-
- test('remove comment', () => {
+ suite('handle comment-update', () => {
+ setup(() => {
sandbox.stub(element, '_commentsChanged');
element.comments = {
meta: {
@@ -179,1453 +109,1531 @@
{id: 'd2', __draft: true, __commentSide: 'right'},
],
};
-
- element._removeComment({});
- // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
- // to believe that one object deepEquals another even when they do :-/.
- assert.equal(JSON.stringify(element.comments), JSON.stringify({
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- {id: 'd2', __draft: true, __commentSide: 'right'},
- ],
- }));
-
- element._removeComment({id: 'bc2', side: 'PARENT',
- __commentSide: 'left'});
- assert.deepEqual(element.comments, {
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- {id: 'd2', __draft: true, __commentSide: 'right'},
- ],
- });
-
- element._removeComment({id: 'd2', __commentSide: 'right'});
- assert.deepEqual(element.comments, {
- meta: {
- changeNum: '42',
- patchRange: {
- basePatchNum: 'PARENT',
- patchNum: 3,
- },
- path: '/path/to/foo',
- projectConfig: {foo: 'bar'},
- },
- left: [
- {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
- {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
- {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
- ],
- right: [
- {id: 'c1', __commentSide: 'right'},
- {id: 'c2', __commentSide: 'right'},
- {id: 'd1', __draft: true, __commentSide: 'right'},
- ],
- });
});
- test('thread-discard handling', () => {
- const threads = [
- {comments: [{id: 4711}]},
- {comments: [{id: 42}]},
- ];
- element._parentIndex = 1;
- element.changeNum = '2';
- element.path = 'some/path';
- element.projectName = 'Some project';
- const threadEls = threads.map(
- thread => {
- const threadEl = element._createThreadElement(thread);
- // Polymer 2 doesn't fire ready events and doesn't execute
- // observers if element is not added to the Dom.
- // See https://github.com/Polymer/old-docs-site/issues/2322
- // and https://github.com/Polymer/polymer/issues/4526
- element._attachThreadElement(threadEl);
- return threadEl;
- });
- assert.equal(threadEls.length, 2);
- assert.equal(threadEls[0].rootId, 4711);
- assert.equal(threadEls[1].rootId, 42);
- for (const threadEl of threadEls) {
- Polymer.dom(element).appendChild(threadEl);
- }
-
- threadEls[0].dispatchEvent(
- new CustomEvent('thread-discard', {detail: {rootId: 4711}}));
- const attachedThreads = element.queryAllEffectiveChildren(
- 'gr-comment-thread');
- assert.equal(attachedThreads.length, 1);
- assert.equal(attachedThreads[0].rootId, 42);
+ test('creating a draft', () => {
+ const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT',
+ __commentSide: 'left'};
+ element.fire('comment-update', {comment});
+ assert.include(element.comments.left, comment);
});
- suite('render reporting', () => {
- test('starts total and content timer on render-start', done => {
- element.dispatchEvent(
- new CustomEvent('render-start', {bubbles: true, composed: true}));
- assert.isTrue(element.$.reporting.time.calledWithExactly(
- 'Diff Total Render'));
- assert.isTrue(element.$.reporting.time.calledWithExactly(
- 'Diff Content Render'));
- done();
- });
+ test('discarding a draft', () => {
+ const draftID = 'tempID';
+ const id = 'savedID';
+ const comment = {
+ __draft: true,
+ __draftID: draftID,
+ side: 'PARENT',
+ __commentSide: 'left',
+ };
+ const diffCommentsModifiedStub = sandbox.stub();
+ element.addEventListener('diff-comments-modified',
+ diffCommentsModifiedStub);
+ element.comments.left.push(comment);
+ comment.id = id;
+ element.fire('comment-discard', {comment});
+ const drafts = element.comments.left
+ .filter(item => item.__draftID === draftID);
+ assert.equal(drafts.length, 0);
+ assert.isTrue(diffCommentsModifiedStub.called);
+ });
- test('ends content timer on render-content', () => {
- element.dispatchEvent(
- new CustomEvent('render-content', {bubbles: true, composed: true}));
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'Diff Content Render'));
- });
+ test('saving a draft', () => {
+ const draftID = 'tempID';
+ const id = 'savedID';
+ const comment = {
+ __draft: true,
+ __draftID: draftID,
+ side: 'PARENT',
+ __commentSide: 'left',
+ };
+ const diffCommentsModifiedStub = sandbox.stub();
+ element.addEventListener('diff-comments-modified',
+ diffCommentsModifiedStub);
+ element.comments.left.push(comment);
+ comment.id = id;
+ element.fire('comment-save', {comment});
+ const drafts = element.comments.left
+ .filter(item => item.__draftID === draftID);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].id, id);
+ assert.isTrue(diffCommentsModifiedStub.called);
+ });
+ });
- test('ends total and syntax timer after syntax layer processing', done => {
- let notifySyntaxProcessed;
- sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
- resolve => {
- notifySyntaxProcessed = resolve;
- }));
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.patchRange = {};
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- return element.reload(true);
+ test('remove comment', () => {
+ sandbox.stub(element, '_commentsChanged');
+ element.comments = {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ {id: 'd2', __draft: true, __commentSide: 'right'},
+ ],
+ };
+
+ element._removeComment({});
+ // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem
+ // to believe that one object deepEquals another even when they do :-/.
+ assert.equal(JSON.stringify(element.comments), JSON.stringify({
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ {id: 'd2', __draft: true, __commentSide: 'right'},
+ ],
+ }));
+
+ element._removeComment({id: 'bc2', side: 'PARENT',
+ __commentSide: 'left'});
+ assert.deepEqual(element.comments, {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ {id: 'd2', __draft: true, __commentSide: 'right'},
+ ],
+ });
+
+ element._removeComment({id: 'd2', __commentSide: 'right'});
+ assert.deepEqual(element.comments, {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ ],
+ });
+ });
+
+ test('thread-discard handling', () => {
+ const threads = [
+ {comments: [{id: 4711}]},
+ {comments: [{id: 42}]},
+ ];
+ element._parentIndex = 1;
+ element.changeNum = '2';
+ element.path = 'some/path';
+ element.projectName = 'Some project';
+ const threadEls = threads.map(
+ thread => {
+ const threadEl = element._createThreadElement(thread);
+ // Polymer 2 doesn't fire ready events and doesn't execute
+ // observers if element is not added to the Dom.
+ // See https://github.com/Polymer/old-docs-site/issues/2322
+ // and https://github.com/Polymer/polymer/issues/4526
+ element._attachThreadElement(threadEl);
+ return threadEl;
});
- // Multiple cascading microtasks are scheduled.
- setTimeout(() => {
- notifySyntaxProcessed();
- // Assert after the notification task is processed.
- Promise.resolve().then(() => {
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'Diff Total Render'));
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'Diff Syntax Render'));
- assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
- 'StartupDiffViewOnlyContent'));
- done();
- });
- });
- });
+ assert.equal(threadEls.length, 2);
+ assert.equal(threadEls[0].rootId, 4711);
+ assert.equal(threadEls[1].rootId, 42);
+ for (const threadEl of threadEls) {
+ dom(element).appendChild(threadEl);
+ }
- test('ends total timer w/ no syntax layer processing', done => {
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.patchRange = {};
- element.reload();
- // Multiple cascading microtasks are scheduled.
- setTimeout(() => {
- assert.isTrue(element.$.reporting.timeEnd.calledOnce);
+ threadEls[0].dispatchEvent(
+ new CustomEvent('thread-discard', {detail: {rootId: 4711}}));
+ const attachedThreads = element.queryAllEffectiveChildren(
+ 'gr-comment-thread');
+ assert.equal(attachedThreads.length, 1);
+ assert.equal(attachedThreads[0].rootId, 42);
+ });
+
+ suite('render reporting', () => {
+ test('starts total and content timer on render-start', done => {
+ element.dispatchEvent(
+ new CustomEvent('render-start', {bubbles: true, composed: true}));
+ assert.isTrue(element.$.reporting.time.calledWithExactly(
+ 'Diff Total Render'));
+ assert.isTrue(element.$.reporting.time.calledWithExactly(
+ 'Diff Content Render'));
+ done();
+ });
+
+ test('ends content timer on render-content', () => {
+ element.dispatchEvent(
+ new CustomEvent('render-content', {bubbles: true, composed: true}));
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'Diff Content Render'));
+ });
+
+ test('ends total and syntax timer after syntax layer processing', done => {
+ let notifySyntaxProcessed;
+ sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+ resolve => {
+ notifySyntaxProcessed = resolve;
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.patchRange = {};
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ return element.reload(true);
+ });
+ // Multiple cascading microtasks are scheduled.
+ setTimeout(() => {
+ notifySyntaxProcessed();
+ // Assert after the notification task is processed.
+ Promise.resolve().then(() => {
assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
'Diff Total Render'));
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'Diff Syntax Render'));
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'StartupDiffViewOnlyContent'));
done();
});
});
+ });
- test('completes reload promise after syntax layer processing', done => {
- let notifySyntaxProcessed;
- sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
- resolve => {
- notifySyntaxProcessed = resolve;
- }));
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.patchRange = {};
- let reloadComplete = false;
- element.$.restAPI.getDiffPreferences()
- .then(prefs => {
- element.prefs = prefs;
- return element.reload();
- })
- .then(() => {
- reloadComplete = true;
- });
- // Multiple cascading microtasks are scheduled.
+ test('ends total timer w/ no syntax layer processing', done => {
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.patchRange = {};
+ element.reload();
+ // Multiple cascading microtasks are scheduled.
+ setTimeout(() => {
+ assert.isTrue(element.$.reporting.timeEnd.calledOnce);
+ assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+ 'Diff Total Render'));
+ done();
+ });
+ });
+
+ test('completes reload promise after syntax layer processing', done => {
+ let notifySyntaxProcessed;
+ sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
+ resolve => {
+ notifySyntaxProcessed = resolve;
+ }));
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.patchRange = {};
+ let reloadComplete = false;
+ element.$.restAPI.getDiffPreferences()
+ .then(prefs => {
+ element.prefs = prefs;
+ return element.reload();
+ })
+ .then(() => {
+ reloadComplete = true;
+ });
+ // Multiple cascading microtasks are scheduled.
+ setTimeout(() => {
+ assert.isFalse(reloadComplete);
+ notifySyntaxProcessed();
+ // Assert after the notification task is processed.
setTimeout(() => {
- assert.isFalse(reloadComplete);
- notifySyntaxProcessed();
- // Assert after the notification task is processed.
- setTimeout(() => {
- assert.isTrue(reloadComplete);
- done();
- });
+ assert.isTrue(reloadComplete);
+ done();
});
});
});
+ });
- test('reload() cancels before network resolves', () => {
- const cancelStub = sandbox.stub(element.$.diff, 'cancel');
+ test('reload() cancels before network resolves', () => {
+ const cancelStub = sandbox.stub(element.$.diff, 'cancel');
- // Stub the network calls into requests that never resolve.
- sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
+ // Stub the network calls into requests that never resolve.
+ sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
+ element.patchRange = {};
+
+ element.reload();
+ assert.isTrue(cancelStub.called);
+ });
+
+ suite('not logged in', () => {
+ setup(() => {
+ getLoggedIn = false;
+ element = fixture('basic');
+ });
+
+ test('reload() loads files weblinks', () => {
+ const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
+ .returns({name: 'stubb', url: '#s'});
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
+ content: [],
+ }));
+ element.projectName = 'test-project';
+ element.path = 'test-path';
+ element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
element.patchRange = {};
-
- element.reload();
- assert.isTrue(cancelStub.called);
+ return element.reload().then(() => {
+ assert.isTrue(weblinksStub.calledTwice);
+ assert.isTrue(weblinksStub.firstCall.calledWith({
+ commit: 'test-base',
+ file: 'test-path',
+ options: {
+ weblinks: undefined,
+ },
+ repo: 'test-project',
+ type: Gerrit.Nav.WeblinkType.FILE}));
+ assert.isTrue(weblinksStub.secondCall.calledWith({
+ commit: 'test-commit',
+ file: 'test-path',
+ options: {
+ weblinks: undefined,
+ },
+ repo: 'test-project',
+ type: Gerrit.Nav.WeblinkType.FILE}));
+ assert.deepEqual(element.filesWeblinks, {
+ meta_a: [{name: 'stubb', url: '#s'}],
+ meta_b: [{name: 'stubb', url: '#s'}],
+ });
+ });
});
- suite('not logged in', () => {
+ test('_getDiff handles null diff responses', done => {
+ stub('gr-rest-api-interface', {
+ getDiff() { return Promise.resolve(null); },
+ });
+ element.changeNum = 123;
+ element.patchRange = {basePatchNum: 1, patchNum: 2};
+ element.path = 'file.txt';
+ element._getDiff().then(done);
+ });
+
+ test('reload resolves on error', () => {
+ const onErrStub = sandbox.stub(element, '_handleGetDiffError');
+ const error = {ok: false, status: 500};
+ sandbox.stub(element.$.restAPI, 'getDiff',
+ (changeNum, basePatchNum, patchNum, path, onErr) => {
+ onErr(error);
+ });
+ element.patchRange = {};
+ return element.reload().then(() => {
+ assert.isTrue(onErrStub.calledOnce);
+ });
+ });
+
+ suite('_handleGetDiffError', () => {
+ let serverErrorStub;
+ let pageErrorStub;
+
setup(() => {
- getLoggedIn = false;
- element = fixture('basic');
+ serverErrorStub = sinon.stub();
+ element.addEventListener('server-error', serverErrorStub);
+ pageErrorStub = sinon.stub();
+ element.addEventListener('page-error', pageErrorStub);
});
- test('reload() loads files weblinks', () => {
- const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
- .returns({name: 'stubb', url: '#s'});
- sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({
- content: [],
- }));
- element.projectName = 'test-project';
- element.path = 'test-path';
- element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
- element.patchRange = {};
- return element.reload().then(() => {
- assert.isTrue(weblinksStub.calledTwice);
- assert.isTrue(weblinksStub.firstCall.calledWith({
- commit: 'test-base',
- file: 'test-path',
- options: {
- weblinks: undefined,
- },
- repo: 'test-project',
- type: Gerrit.Nav.WeblinkType.FILE}));
- assert.isTrue(weblinksStub.secondCall.calledWith({
- commit: 'test-commit',
- file: 'test-path',
- options: {
- weblinks: undefined,
- },
- repo: 'test-project',
- type: Gerrit.Nav.WeblinkType.FILE}));
- assert.deepEqual(element.filesWeblinks, {
- meta_a: [{name: 'stubb', url: '#s'}],
- meta_b: [{name: 'stubb', url: '#s'}],
- });
- });
+ test('page error on HTTP-409', () => {
+ element._handleGetDiffError({status: 409});
+ assert.isTrue(serverErrorStub.calledOnce);
+ assert.isFalse(pageErrorStub.called);
+ assert.isNotOk(element._errorMessage);
});
- test('_getDiff handles null diff responses', done => {
- stub('gr-rest-api-interface', {
- getDiff() { return Promise.resolve(null); },
- });
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
- element.path = 'file.txt';
- element._getDiff().then(done);
+ test('server error on non-HTTP-409', () => {
+ element._handleGetDiffError({status: 500});
+ assert.isFalse(serverErrorStub.called);
+ assert.isTrue(pageErrorStub.calledOnce);
+ assert.isNotOk(element._errorMessage);
});
- test('reload resolves on error', () => {
- const onErrStub = sandbox.stub(element, '_handleGetDiffError');
- const error = {ok: false, status: 500};
- sandbox.stub(element.$.restAPI, 'getDiff',
- (changeNum, basePatchNum, patchNum, path, onErr) => {
- onErr(error);
- });
- element.patchRange = {};
- return element.reload().then(() => {
- assert.isTrue(onErrStub.calledOnce);
- });
+ test('error message if showLoadFailure', () => {
+ element.showLoadFailure = true;
+ element._handleGetDiffError({status: 500, statusText: 'Failure!'});
+ assert.isFalse(serverErrorStub.called);
+ assert.isFalse(pageErrorStub.called);
+ assert.equal(element._errorMessage,
+ 'Encountered error when loading the diff: 500 Failure!');
+ });
+ });
+
+ suite('image diffs', () => {
+ let mockFile1;
+ let mockFile2;
+ setup(() => {
+ mockFile1 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
+ type: 'image/bmp',
+ };
+ mockFile2 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
+ type: 'image/bmp',
+ };
+ sandbox.stub(element.$.restAPI,
+ 'getB64FileContents',
+ (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
+ opt_parentIndex === 1 ? mockFile1 :
+ mockFile2)
+ );
+
+ element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+ element.comments = {
+ left: [],
+ right: [],
+ meta: {patchRange: element.patchRange},
+ };
});
- suite('_handleGetDiffError', () => {
- let serverErrorStub;
- let pageErrorStub;
+ test('renders image diffs with same file name', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
- setup(() => {
- serverErrorStub = sinon.stub();
- element.addEventListener('server-error', serverErrorStub);
- pageErrorStub = sinon.stub();
- element.addEventListener('page-error', pageErrorStub);
- });
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
- test('page error on HTTP-409', () => {
- element._handleGetDiffError({status: 409});
- assert.isTrue(serverErrorStub.calledOnce);
- assert.isFalse(pageErrorStub.called);
- assert.isNotOk(element._errorMessage);
- });
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diff.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
- test('server error on non-HTTP-409', () => {
- element._handleGetDiffError({status: 500});
- assert.isFalse(serverErrorStub.called);
- assert.isTrue(pageErrorStub.calledOnce);
- assert.isNotOk(element._errorMessage);
- });
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diff.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
- test('error message if showLoadFailure', () => {
- element.showLoadFailure = true;
- element._handleGetDiffError({status: 500, statusText: 'Failure!'});
- assert.isFalse(serverErrorStub.called);
- assert.isFalse(pageErrorStub.called);
- assert.equal(element._errorMessage,
- 'Encountered error when loading the diff: 500 Failure!');
- });
- });
+ assert.isNotOk(rightLabelName);
+ assert.isNotOk(leftLabelName);
- suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
- setup(() => {
- mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
- type: 'image/bmp',
- };
- mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
- type: 'image/bmp',
- };
- sandbox.stub(element.$.restAPI,
- 'getB64FileContents',
- (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
- opt_parentIndex === 1 ? mockFile1 :
- mockFile2)
- );
+ let leftLoaded = false;
+ let rightLoaded = false;
- element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
- element.comments = {
- left: [],
- right: [],
- meta: {patchRange: element.patchRange},
- };
- });
-
- test('renders image diffs with same file name', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diff.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diff.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isNotOk(rightLabelName);
- assert.isNotOk(leftLabelName);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
-
- test('renders image diffs with a different file name', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot2.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot2.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diff.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diff.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isOk(rightLabelName);
- assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
-
- test('renders added image', done => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'ADDED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 0000000..f9c2f2c 100644',
- '--- /dev/null',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
-
- assert.isNotOk(leftImage);
- assert.isOk(rightImage);
- done();
- });
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
-
- test('renders removed image', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
-
+ leftImage.addEventListener('load', () => {
assert.isOk(leftImage);
- assert.isNotOk(rightImage);
- done();
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
});
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
});
- });
+ };
- test('does not render disallowed image type', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- mockFile1.type = 'image/jpeg-evil';
+ element.addEventListener('render', rendered);
- sandbox.stub(element.$.restAPI, 'getDiff')
- .returns(Promise.resolve(mockDiff));
-
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- assert.isNotOk(leftImage);
- done();
- });
-
- element.$.restAPI.getDiffPreferences().then(prefs => {
- element.prefs = prefs;
- element.reload();
- });
- });
- });
- });
-
- test('delegates cancel()', () => {
- const stub = sandbox.stub(element.$.diff, 'cancel');
- element.patchRange = {};
- element.reload();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates getCursorStops()', () => {
- const returnValue = [document.createElement('b')];
- const stub = sandbox.stub(element.$.diff, 'getCursorStops')
- .returns(returnValue);
- assert.equal(element.getCursorStops(), returnValue);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates isRangeSelected()', () => {
- const returnValue = true;
- const stub = sandbox.stub(element.$.diff, 'isRangeSelected')
- .returns(returnValue);
- assert.equal(element.isRangeSelected(), returnValue);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates toggleLeftDiff()', () => {
- const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
- element.toggleLeftDiff();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- suite('blame', () => {
- setup(() => {
- element = fixture('basic');
- });
-
- test('clearBlame', () => {
- element._blame = [];
- const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame');
- element.clearBlame();
- assert.isNull(element._blame);
- assert.isTrue(setBlameSpy.calledWithExactly(null));
- assert.equal(element.isBlameLoaded, false);
- });
-
- test('loadBlame', () => {
- const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
- const showAlertStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
- .returns(Promise.resolve(mockBlame));
- element.changeNum = 42;
- element.patchRange = {patchNum: 5, basePatchNum: 4};
- element.path = 'foo/bar.baz';
- return element.loadBlame().then(() => {
- assert.isTrue(getBlameStub.calledWithExactly(
- 42, 5, 'foo/bar.baz', true));
- assert.isFalse(showAlertStub.called);
- assert.equal(element._blame, mockBlame);
- assert.equal(element.isBlameLoaded, true);
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
});
});
- test('loadBlame empty', () => {
- const mockBlame = [];
- const showAlertStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- sandbox.stub(element.$.restAPI, 'getBlame')
- .returns(Promise.resolve(mockBlame));
- element.changeNum = 42;
- element.patchRange = {patchNum: 5, basePatchNum: 4};
- element.path = 'foo/bar.baz';
- return element.loadBlame()
- .then(() => {
- assert.isTrue(false, 'Promise should not resolve');
- })
- .catch(() => {
- assert.isTrue(showAlertStub.calledOnce);
- assert.isNull(element._blame);
- assert.equal(element.isBlameLoaded, false);
- });
- });
- });
-
- test('getThreadEls() returns .comment-threads', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- Polymer.dom(element.$.diff).appendChild(threadEl);
- assert.deepEqual(element.getThreadEls(), [threadEl]);
- });
-
- test('delegates addDraftAtLine(el)', () => {
- const param0 = document.createElement('b');
- const stub = sandbox.stub(element.$.diff, 'addDraftAtLine');
- element.addDraftAtLine(param0);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 1);
- assert.equal(stub.lastCall.args[0], param0);
- });
-
- test('delegates clearDiffContent()', () => {
- const stub = sandbox.stub(element.$.diff, 'clearDiffContent');
- element.clearDiffContent();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates expandAllContext()', () => {
- const stub = sandbox.stub(element.$.diff, 'expandAllContext');
- element.expandAllContext();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('passes in changeNum', () => {
- const value = '12345';
- element.changeNum = value;
- assert.equal(element.$.diff.changeNum, value);
- });
-
- test('passes in noAutoRender', () => {
- const value = true;
- element.noAutoRender = value;
- assert.equal(element.$.diff.noAutoRender, value);
- });
-
- test('passes in patchRange', () => {
- const value = {patchNum: 'foo', basePatchNum: 'bar'};
- element.patchRange = value;
- assert.equal(element.$.diff.patchRange, value);
- });
-
- test('passes in path', () => {
- const value = 'some/file/path';
- element.path = value;
- assert.equal(element.$.diff.path, value);
- });
-
- test('passes in prefs', () => {
- const value = {};
- element.prefs = value;
- assert.equal(element.$.diff.prefs, value);
- });
-
- test('passes in changeNum', () => {
- const value = '12345';
- element.changeNum = value;
- assert.equal(element.$.diff.changeNum, value);
- });
-
- test('passes in projectName', () => {
- const value = 'Gerrit';
- element.projectName = value;
- assert.equal(element.$.diff.projectName, value);
- });
-
- test('passes in displayLine', () => {
- const value = true;
- element.displayLine = value;
- assert.equal(element.$.diff.displayLine, value);
- });
-
- test('passes in commitRange', () => {
- const value = {};
- element.commitRange = value;
- assert.equal(element.$.diff.commitRange, value);
- });
-
- test('passes in hidden', () => {
- const value = true;
- element.hidden = value;
- assert.equal(element.$.diff.hidden, value);
- assert.isNotNull(element.getAttribute('hidden'));
- });
-
- test('passes in noRenderOnPrefsChange', () => {
- const value = true;
- element.noRenderOnPrefsChange = value;
- assert.equal(element.$.diff.noRenderOnPrefsChange, value);
- });
-
- test('passes in lineWrapping', () => {
- const value = true;
- element.lineWrapping = value;
- assert.equal(element.$.diff.lineWrapping, value);
- });
-
- test('passes in viewMode', () => {
- const value = 'SIDE_BY_SIDE';
- element.viewMode = value;
- assert.equal(element.$.diff.viewMode, value);
- });
-
- test('passes in lineOfInterest', () => {
- const value = {number: 123, leftSide: true};
- element.lineOfInterest = value;
- assert.equal(element.$.diff.lineOfInterest, value);
- });
-
- suite('_reportDiff', () => {
- let reportStub;
-
- setup(() => {
- element = fixture('basic');
- element.patchRange = {basePatchNum: 1};
- reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
- });
-
- test('null and content-less', () => {
- element._reportDiff(null);
- assert.isFalse(reportStub.called);
-
- element._reportDiff({});
- assert.isFalse(reportStub.called);
- });
-
- test('diff w/ no delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {ab: ['baz', 'foo']},
+ test('renders image diffs with a different file name', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot2.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot2.jpg',
+ 'Binary files differ',
],
+ content: [{skip: 66}],
+ binary: true,
};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
- assert.isUndefined(reportStub.lastCall.args[1]);
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diff.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diff.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isOk(rightLabelName);
+ assert.isOk(leftLabelName);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+
+ let leftLoaded = false;
+ let rightLoaded = false;
+
+ leftImage.addEventListener('load', () => {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
});
- test('diff w/ no rebase delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo']},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], b: ['bar', 'baz']},
- {ab: ['foo', 'bar']},
- {b: ['baz', 'foo']},
- {ab: ['foo', 'bar']},
+ test('renders added image', done => {
+ const mockDiff = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'ADDED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 0000000..f9c2f2c 100644',
+ '--- /dev/null',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
],
+ content: [{skip: 66}],
+ binary: true,
};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
- assert.isUndefined(reportStub.lastCall.args[1]);
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+
+ assert.isNotOk(leftImage);
+ assert.isOk(rightImage);
+ done();
+ });
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
});
- test('diff w/ some rebase delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], due_to_rebase: true},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], b: ['bar', 'baz']},
- {ab: ['foo', 'bar']},
- {b: ['baz', 'foo'], due_to_rebase: true},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo']},
+ test('renders removed image', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
],
+ content: [{skip: 66}],
+ binary: true,
};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.isTrue(reportStub.calledWith(
- 'rebase-percent-nonzero',
- {percentRebaseDelta: 50}
- ));
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ const rightImage =
+ element.$.diff.$.diffTable.querySelector('td.right img');
+
+ assert.isOk(leftImage);
+ assert.isNotOk(rightImage);
+ done();
+ });
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
});
- test('diff w/ all rebase delta', () => {
- const diff = {content: [{
- a: ['foo', 'bar'],
- b: ['baz', 'foo'],
- due_to_rebase: true,
- }]};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.isTrue(reportStub.calledWith(
- 'rebase-percent-nonzero',
- {percentRebaseDelta: 100}
- ));
- });
+ test('does not render disallowed image type', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ mockFile1.type = 'image/jpeg-evil';
- test('diff against parent event', () => {
- element.patchRange.basePatchNum = 'PARENT';
- const diff = {content: [{
- a: ['foo', 'bar'],
- b: ['baz', 'foo'],
- }]};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
- assert.isUndefined(reportStub.lastCall.args[1]);
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
+
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ const leftImage =
+ element.$.diff.$.diffTable.querySelector('td.left img');
+ assert.isNotOk(leftImage);
+ done();
+ });
+
+ element.$.restAPI.getDiffPreferences().then(prefs => {
+ element.prefs = prefs;
+ element.reload();
+ });
+ });
+ });
+ });
+
+ test('delegates cancel()', () => {
+ const stub = sandbox.stub(element.$.diff, 'cancel');
+ element.patchRange = {};
+ element.reload();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates getCursorStops()', () => {
+ const returnValue = [document.createElement('b')];
+ const stub = sandbox.stub(element.$.diff, 'getCursorStops')
+ .returns(returnValue);
+ assert.equal(element.getCursorStops(), returnValue);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates isRangeSelected()', () => {
+ const returnValue = true;
+ const stub = sandbox.stub(element.$.diff, 'isRangeSelected')
+ .returns(returnValue);
+ assert.equal(element.isRangeSelected(), returnValue);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates toggleLeftDiff()', () => {
+ const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
+ element.toggleLeftDiff();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ suite('blame', () => {
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('clearBlame', () => {
+ element._blame = [];
+ const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame');
+ element.clearBlame();
+ assert.isNull(element._blame);
+ assert.isTrue(setBlameSpy.calledWithExactly(null));
+ assert.equal(element.isBlameLoaded, false);
+ });
+
+ test('loadBlame', () => {
+ const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame')
+ .returns(Promise.resolve(mockBlame));
+ element.changeNum = 42;
+ element.patchRange = {patchNum: 5, basePatchNum: 4};
+ element.path = 'foo/bar.baz';
+ return element.loadBlame().then(() => {
+ assert.isTrue(getBlameStub.calledWithExactly(
+ 42, 5, 'foo/bar.baz', true));
+ assert.isFalse(showAlertStub.called);
+ assert.equal(element._blame, mockBlame);
+ assert.equal(element.isBlameLoaded, true);
});
});
- test('comments sorting', () => {
- const comments = [
- {
- id: 'new_draft',
- message: 'i do not like either of you',
- __commentSide: 'left',
- __draft: true,
- updated: '2015-12-20 15:01:20.396000000',
- },
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- line: 1,
- __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- __commentSide: 'left',
- line: 1,
- in_reply_to: 'sallys_confession',
- },
- ];
- const sortedComments = element._sortComments(comments);
- assert.equal(sortedComments[0], comments[1]);
- assert.equal(sortedComments[1], comments[2]);
- assert.equal(sortedComments[2], comments[0]);
+ test('loadBlame empty', () => {
+ const mockBlame = [];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ sandbox.stub(element.$.restAPI, 'getBlame')
+ .returns(Promise.resolve(mockBlame));
+ element.changeNum = 42;
+ element.patchRange = {patchNum: 5, basePatchNum: 4};
+ element.path = 'foo/bar.baz';
+ return element.loadBlame()
+ .then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ })
+ .catch(() => {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isNull(element._blame);
+ assert.equal(element.isBlameLoaded, false);
+ });
+ });
+ });
+
+ test('getThreadEls() returns .comment-threads', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ dom(element.$.diff).appendChild(threadEl);
+ assert.deepEqual(element.getThreadEls(), [threadEl]);
+ });
+
+ test('delegates addDraftAtLine(el)', () => {
+ const param0 = document.createElement('b');
+ const stub = sandbox.stub(element.$.diff, 'addDraftAtLine');
+ element.addDraftAtLine(param0);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 1);
+ assert.equal(stub.lastCall.args[0], param0);
+ });
+
+ test('delegates clearDiffContent()', () => {
+ const stub = sandbox.stub(element.$.diff, 'clearDiffContent');
+ element.clearDiffContent();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates expandAllContext()', () => {
+ const stub = sandbox.stub(element.$.diff, 'expandAllContext');
+ element.expandAllContext();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('passes in changeNum', () => {
+ const value = '12345';
+ element.changeNum = value;
+ assert.equal(element.$.diff.changeNum, value);
+ });
+
+ test('passes in noAutoRender', () => {
+ const value = true;
+ element.noAutoRender = value;
+ assert.equal(element.$.diff.noAutoRender, value);
+ });
+
+ test('passes in patchRange', () => {
+ const value = {patchNum: 'foo', basePatchNum: 'bar'};
+ element.patchRange = value;
+ assert.equal(element.$.diff.patchRange, value);
+ });
+
+ test('passes in path', () => {
+ const value = 'some/file/path';
+ element.path = value;
+ assert.equal(element.$.diff.path, value);
+ });
+
+ test('passes in prefs', () => {
+ const value = {};
+ element.prefs = value;
+ assert.equal(element.$.diff.prefs, value);
+ });
+
+ test('passes in changeNum', () => {
+ const value = '12345';
+ element.changeNum = value;
+ assert.equal(element.$.diff.changeNum, value);
+ });
+
+ test('passes in projectName', () => {
+ const value = 'Gerrit';
+ element.projectName = value;
+ assert.equal(element.$.diff.projectName, value);
+ });
+
+ test('passes in displayLine', () => {
+ const value = true;
+ element.displayLine = value;
+ assert.equal(element.$.diff.displayLine, value);
+ });
+
+ test('passes in commitRange', () => {
+ const value = {};
+ element.commitRange = value;
+ assert.equal(element.$.diff.commitRange, value);
+ });
+
+ test('passes in hidden', () => {
+ const value = true;
+ element.hidden = value;
+ assert.equal(element.$.diff.hidden, value);
+ assert.isNotNull(element.getAttribute('hidden'));
+ });
+
+ test('passes in noRenderOnPrefsChange', () => {
+ const value = true;
+ element.noRenderOnPrefsChange = value;
+ assert.equal(element.$.diff.noRenderOnPrefsChange, value);
+ });
+
+ test('passes in lineWrapping', () => {
+ const value = true;
+ element.lineWrapping = value;
+ assert.equal(element.$.diff.lineWrapping, value);
+ });
+
+ test('passes in viewMode', () => {
+ const value = 'SIDE_BY_SIDE';
+ element.viewMode = value;
+ assert.equal(element.$.diff.viewMode, value);
+ });
+
+ test('passes in lineOfInterest', () => {
+ const value = {number: 123, leftSide: true};
+ element.lineOfInterest = value;
+ assert.equal(element.$.diff.lineOfInterest, value);
+ });
+
+ suite('_reportDiff', () => {
+ let reportStub;
+
+ setup(() => {
+ element = fixture('basic');
+ element.patchRange = {basePatchNum: 1};
+ reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
});
- test('_createThreads', () => {
- const comments = [
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- line: 1,
- __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- __commentSide: 'left',
- line: 1,
- in_reply_to: 'sallys_confession',
- },
- {
- id: 'new_draft',
- message: 'i do not like either of you',
- __commentSide: 'left',
- __draft: true,
- updated: '2015-12-20 15:01:20.396000000',
- },
- ];
+ test('null and content-less', () => {
+ element._reportDiff(null);
+ assert.isFalse(reportStub.called);
- const actualThreads = element._createThreads(comments);
-
- assert.equal(actualThreads.length, 2);
-
- assert.equal(
- actualThreads[0].start_datetime, '2015-12-23 15:00:20.396000000');
- assert.equal(actualThreads[0].commentSide, 'left');
- assert.equal(actualThreads[0].comments.length, 2);
- assert.deepEqual(actualThreads[0].comments[0], comments[0]);
- assert.deepEqual(actualThreads[0].comments[1], comments[1]);
- assert.equal(actualThreads[0].patchNum, undefined);
- assert.equal(actualThreads[0].rootId, 'sallys_confession');
- assert.equal(actualThreads[0].lineNum, 1);
-
- assert.equal(
- actualThreads[1].start_datetime, '2015-12-20 15:01:20.396000000');
- assert.equal(actualThreads[1].commentSide, 'left');
- assert.equal(actualThreads[1].comments.length, 1);
- assert.deepEqual(actualThreads[1].comments[0], comments[2]);
- assert.equal(actualThreads[1].patchNum, undefined);
- assert.equal(actualThreads[1].rootId, 'new_draft');
- assert.equal(actualThreads[1].lineNum, undefined);
+ element._reportDiff({});
+ assert.isFalse(reportStub.called);
});
- test('_createThreads inherits patchNum and range', () => {
- const comments = [{
- id: 'betsys_confession',
+ test('diff w/ no delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {ab: ['baz', 'foo']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+
+ test('diff w/ no rebase delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+
+ test('diff w/ some rebase delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.isTrue(reportStub.calledWith(
+ 'rebase-percent-nonzero',
+ {percentRebaseDelta: 50}
+ ));
+ });
+
+ test('diff w/ all rebase delta', () => {
+ const diff = {content: [{
+ a: ['foo', 'bar'],
+ b: ['baz', 'foo'],
+ due_to_rebase: true,
+ }]};
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.isTrue(reportStub.calledWith(
+ 'rebase-percent-nonzero',
+ {percentRebaseDelta: 100}
+ ));
+ });
+
+ test('diff against parent event', () => {
+ element.patchRange.basePatchNum = 'PARENT';
+ const diff = {content: [{
+ a: ['foo', 'bar'],
+ b: ['baz', 'foo'],
+ }]};
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+ });
+
+ test('comments sorting', () => {
+ const comments = [
+ {
+ id: 'new_draft',
+ message: 'i do not like either of you',
+ __commentSide: 'left',
+ __draft: true,
+ updated: '2015-12-20 15:01:20.396000000',
+ },
+ {
+ id: 'sallys_confession',
message: 'i like you, jack',
- updated: '2015-12-24 15:00:10.396000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 2,
- },
- patch_set: 5,
+ updated: '2015-12-23 15:00:20.396000000',
+ line: 1,
+ __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
__commentSide: 'left',
line: 1,
- }];
+ in_reply_to: 'sallys_confession',
+ },
+ ];
+ const sortedComments = element._sortComments(comments);
+ assert.equal(sortedComments[0], comments[1]);
+ assert.equal(sortedComments[1], comments[2]);
+ assert.equal(sortedComments[2], comments[0]);
+ });
- const expectedThreads = [
- {
- start_datetime: '2015-12-24 15:00:10.396000000',
- commentSide: 'left',
- comments: [{
- id: 'betsys_confession',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:10.396000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 2,
- },
- patch_set: 5,
- __commentSide: 'left',
- line: 1,
- }],
- patchNum: 5,
- rootId: 'betsys_confession',
+ test('_createThreads', () => {
+ const comments = [
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-23 15:00:20.396000000',
+ line: 1,
+ __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
+ __commentSide: 'left',
+ line: 1,
+ in_reply_to: 'sallys_confession',
+ },
+ {
+ id: 'new_draft',
+ message: 'i do not like either of you',
+ __commentSide: 'left',
+ __draft: true,
+ updated: '2015-12-20 15:01:20.396000000',
+ },
+ ];
+
+ const actualThreads = element._createThreads(comments);
+
+ assert.equal(actualThreads.length, 2);
+
+ assert.equal(
+ actualThreads[0].start_datetime, '2015-12-23 15:00:20.396000000');
+ assert.equal(actualThreads[0].commentSide, 'left');
+ assert.equal(actualThreads[0].comments.length, 2);
+ assert.deepEqual(actualThreads[0].comments[0], comments[0]);
+ assert.deepEqual(actualThreads[0].comments[1], comments[1]);
+ assert.equal(actualThreads[0].patchNum, undefined);
+ assert.equal(actualThreads[0].rootId, 'sallys_confession');
+ assert.equal(actualThreads[0].lineNum, 1);
+
+ assert.equal(
+ actualThreads[1].start_datetime, '2015-12-20 15:01:20.396000000');
+ assert.equal(actualThreads[1].commentSide, 'left');
+ assert.equal(actualThreads[1].comments.length, 1);
+ assert.deepEqual(actualThreads[1].comments[0], comments[2]);
+ assert.equal(actualThreads[1].patchNum, undefined);
+ assert.equal(actualThreads[1].rootId, 'new_draft');
+ assert.equal(actualThreads[1].lineNum, undefined);
+ });
+
+ test('_createThreads inherits patchNum and range', () => {
+ const comments = [{
+ id: 'betsys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:10.396000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 2,
+ },
+ patch_set: 5,
+ __commentSide: 'left',
+ line: 1,
+ }];
+
+ const expectedThreads = [
+ {
+ start_datetime: '2015-12-24 15:00:10.396000000',
+ commentSide: 'left',
+ comments: [{
+ id: 'betsys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:10.396000000',
range: {
start_line: 1,
start_character: 1,
end_line: 1,
end_character: 2,
},
- lineNum: 1,
- isOnParent: false,
+ patch_set: 5,
+ __commentSide: 'left',
+ line: 1,
+ }],
+ patchNum: 5,
+ rootId: 'betsys_confession',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 2,
},
- ];
+ lineNum: 1,
+ isOnParent: false,
+ },
+ ];
- assert.deepEqual(
- element._createThreads(comments),
- expectedThreads);
- });
+ assert.deepEqual(
+ element._createThreads(comments),
+ expectedThreads);
+ });
- test('_createThreads does not thread unrelated comments at same location',
- () => {
- const comments = [
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- __commentSide: 'left',
- },
- ];
- assert.equal(element._createThreads(comments).length, 2);
- });
-
- test('_createThreads derives isOnParent using side from first comment',
- () => {
- const comments = [
- {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-23 15:00:20.396000000',
- // line: 1,
- // __commentSide: 'left',
- }, {
- id: 'jacks_reply',
- message: 'i like you, too',
- updated: '2015-12-24 15:01:20.396000000',
- // __commentSide: 'left',
- // line: 1,
- in_reply_to: 'sallys_confession',
- },
- ];
-
- assert.equal(element._createThreads(comments)[0].isOnParent, false);
-
- comments[0].side = 'REVISION';
- assert.equal(element._createThreads(comments)[0].isOnParent, false);
-
- comments[0].side = 'PARENT';
- assert.equal(element._createThreads(comments)[0].isOnParent, true);
- });
-
- test('_getOrCreateThread', () => {
- const commentSide = 'left';
-
- assert.isOk(element._getOrCreateThread('2', 3,
- commentSide, undefined, false));
-
- let threads = Polymer.dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 1);
- assert.equal(threads[0].commentSide, commentSide);
- assert.equal(threads[0].range, undefined);
- assert.equal(threads[0].isOnParent, false);
- assert.equal(threads[0].patchNum, 2);
-
- // Try to fetch a thread with a different range.
- const range = {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 3,
- };
-
- assert.isOk(element._getOrCreateThread(
- '3', 1, commentSide, range, true));
-
- threads = Polymer.dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 2);
- assert.equal(threads[1].commentSide, commentSide);
- assert.equal(threads[1].range, range);
- assert.equal(threads[1].isOnParent, true);
- assert.equal(threads[1].patchNum, 3);
- });
-
- test('_filterThreadElsForLocation with no threads', () => {
- const line = {beforeNumber: 3, afterNumber: 5};
-
- const threads = [];
- assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
- assert.deepEqual(element._filterThreadElsForLocation(threads, line,
- Gerrit.DiffSide.LEFT), []);
- assert.deepEqual(element._filterThreadElsForLocation(threads, line,
- Gerrit.DiffSide.RIGHT), []);
- });
-
- test('_filterThreadElsForLocation for line comments', () => {
- const line = {beforeNumber: 3, afterNumber: 5};
-
- const l3 = document.createElement('div');
- l3.setAttribute('line-num', 3);
- l3.setAttribute('comment-side', 'left');
-
- const l5 = document.createElement('div');
- l5.setAttribute('line-num', 5);
- l5.setAttribute('comment-side', 'left');
-
- const r3 = document.createElement('div');
- r3.setAttribute('line-num', 3);
- r3.setAttribute('comment-side', 'right');
-
- const r5 = document.createElement('div');
- r5.setAttribute('line-num', 5);
- r5.setAttribute('comment-side', 'right');
-
- const threadEls = [l3, l5, r3, r5];
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
- [l3, r5]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.LEFT), [l3]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.RIGHT), [r5]);
- });
-
- test('_filterThreadElsForLocation for file comments', () => {
- const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};
-
- const l = document.createElement('div');
- l.setAttribute('comment-side', 'left');
- l.setAttribute('line-num', 'FILE');
-
- const r = document.createElement('div');
- r.setAttribute('comment-side', 'right');
- r.setAttribute('line-num', 'FILE');
-
- const threadEls = [l, r];
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
- [l, r]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.BOTH), [l, r]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.LEFT), [l]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Gerrit.DiffSide.RIGHT), [r]);
- });
-
- suite('syntax layer with syntax_highlighting on', () => {
- setup(() => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
- element.patchRange = {};
- element.prefs = prefs;
- });
-
- test('gr-diff-host provides syntax highlighting layer to gr-diff', () => {
- element.reload();
- assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
- });
-
- test('rendering normal-sized diff does not disable syntax', () => {
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- assert.isTrue(element.$.syntaxLayer.enabled);
- });
-
- test('rendering large diff disables syntax', () => {
- // Before it renders, set the first diff line to 500 '*' characters.
- element.diff = {
- content: [{
- a: [new Array(501).join('*')],
- }],
- };
- assert.isFalse(element.$.syntaxLayer.enabled);
- });
-
- test('starts syntax layer processing on render event', done => {
- sandbox.stub(element.$.syntaxLayer, 'process')
- .returns(Promise.resolve());
- sandbox.stub(element.$.restAPI, 'getDiff').returns(
- Promise.resolve({content: []}));
- element.reload();
- setTimeout(() => {
- element.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- assert.isTrue(element.$.syntaxLayer.process.called);
- done();
- });
- });
- });
-
- suite('syntax layer with syntax_highlgihting off', () => {
- setup(() => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- };
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- element.patchRange = {};
- element.prefs = prefs;
- });
-
- test('gr-diff-host provides syntax highlighting layer', () => {
- element.reload();
- assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
- });
-
- test('syntax layer should be disabled', () => {
- assert.isFalse(element.$.syntaxLayer.enabled);
- });
-
- test('still disabled for large diff', () => {
- // Before it renders, set the first diff line to 500 '*' characters.
- element.diff = {
- content: [{
- a: [new Array(501).join('*')],
- }],
- };
- assert.isFalse(element.$.syntaxLayer.enabled);
- });
- });
-
- suite('coverage layer', () => {
- let notifyStub;
- setup(() => {
- notifyStub = sinon.stub();
- stub('gr-js-api-interface', {
- getCoverageAnnotationApi() {
- return Promise.resolve({
- notify: notifyStub,
- getCoverageProvider() {
- return () => Promise.resolve([
- {
- type: 'COVERED',
- side: 'right',
- code_range: {
- start_line: 1,
- end_line: 2,
- },
- },
- {
- type: 'NOT_COVERED',
- side: 'right',
- code_range: {
- start_line: 3,
- end_line: 4,
- },
- },
- ]);
- },
- });
+ test('_createThreads does not thread unrelated comments at same location',
+ () => {
+ const comments = [
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-23 15:00:20.396000000',
+ __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
+ __commentSide: 'left',
},
- });
- element = fixture('basic');
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- };
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- element.patchRange = {};
- element.prefs = prefs;
+ ];
+ assert.equal(element._createThreads(comments).length, 2);
});
- test('getCoverageAnnotationApi should be called', done => {
- element.reload();
- flush(() => {
- assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce);
- done();
- });
+ test('_createThreads derives isOnParent using side from first comment',
+ () => {
+ const comments = [
+ {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-23 15:00:20.396000000',
+ // line: 1,
+ // __commentSide: 'left',
+ }, {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ updated: '2015-12-24 15:01:20.396000000',
+ // __commentSide: 'left',
+ // line: 1,
+ in_reply_to: 'sallys_confession',
+ },
+ ];
+
+ assert.equal(element._createThreads(comments)[0].isOnParent, false);
+
+ comments[0].side = 'REVISION';
+ assert.equal(element._createThreads(comments)[0].isOnParent, false);
+
+ comments[0].side = 'PARENT';
+ assert.equal(element._createThreads(comments)[0].isOnParent, true);
});
- test('coverageRangeChanged should be called', done => {
- element.reload();
- flush(() => {
- assert.equal(notifyStub.callCount, 2);
- done();
- });
- });
+ test('_getOrCreateThread', () => {
+ const commentSide = 'left';
+
+ assert.isOk(element._getOrCreateThread('2', 3,
+ commentSide, undefined, false));
+
+ let threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ assert.equal(threads[0].commentSide, commentSide);
+ assert.equal(threads[0].range, undefined);
+ assert.equal(threads[0].isOnParent, false);
+ assert.equal(threads[0].patchNum, 2);
+
+ // Try to fetch a thread with a different range.
+ const range = {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 3,
+ };
+
+ assert.isOk(element._getOrCreateThread(
+ '3', 1, commentSide, range, true));
+
+ threads = dom(element.$.diff)
+ .queryDistributedElements('gr-comment-thread');
+
+ assert.equal(threads.length, 2);
+ assert.equal(threads[1].commentSide, commentSide);
+ assert.equal(threads[1].range, range);
+ assert.equal(threads[1].isOnParent, true);
+ assert.equal(threads[1].patchNum, 3);
+ });
+
+ test('_filterThreadElsForLocation with no threads', () => {
+ const line = {beforeNumber: 3, afterNumber: 5};
+
+ const threads = [];
+ assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
+ assert.deepEqual(element._filterThreadElsForLocation(threads, line,
+ Gerrit.DiffSide.LEFT), []);
+ assert.deepEqual(element._filterThreadElsForLocation(threads, line,
+ Gerrit.DiffSide.RIGHT), []);
+ });
+
+ test('_filterThreadElsForLocation for line comments', () => {
+ const line = {beforeNumber: 3, afterNumber: 5};
+
+ const l3 = document.createElement('div');
+ l3.setAttribute('line-num', 3);
+ l3.setAttribute('comment-side', 'left');
+
+ const l5 = document.createElement('div');
+ l5.setAttribute('line-num', 5);
+ l5.setAttribute('comment-side', 'left');
+
+ const r3 = document.createElement('div');
+ r3.setAttribute('line-num', 3);
+ r3.setAttribute('comment-side', 'right');
+
+ const r5 = document.createElement('div');
+ r5.setAttribute('line-num', 5);
+ r5.setAttribute('comment-side', 'right');
+
+ const threadEls = [l3, l5, r3, r5];
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
+ [l3, r5]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.LEFT), [l3]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.RIGHT), [r5]);
+ });
+
+ test('_filterThreadElsForLocation for file comments', () => {
+ const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};
+
+ const l = document.createElement('div');
+ l.setAttribute('comment-side', 'left');
+ l.setAttribute('line-num', 'FILE');
+
+ const r = document.createElement('div');
+ r.setAttribute('comment-side', 'right');
+ r.setAttribute('line-num', 'FILE');
+
+ const threadEls = [l, r];
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line),
+ [l, r]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.BOTH), [l, r]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.LEFT), [l]);
+ assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
+ Gerrit.DiffSide.RIGHT), [r]);
+ });
+
+ suite('syntax layer with syntax_highlighting on', () => {
+ setup(() => {
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ element.patchRange = {};
+ element.prefs = prefs;
});
- suite('trailing newlines', () => {
- setup(() => {
- });
+ test('gr-diff-host provides syntax highlighting layer to gr-diff', () => {
+ element.reload();
+ assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
+ });
- suite('_lastChunkForSide', () => {
- test('deltas', () => {
- const diff = {content: [
- {a: ['foo', 'bar'], b: ['baz']},
- {ab: ['foo', 'bar', 'baz']},
- {b: ['foo']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
- assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
+ test('rendering normal-sized diff does not disable syntax', () => {
+ element.diff = {
+ content: [{
+ a: ['foo'],
+ }],
+ };
+ assert.isTrue(element.$.syntaxLayer.enabled);
+ });
- diff.content.push({a: ['foo'], b: ['bar']});
- assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
- assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
- });
+ test('rendering large diff disables syntax', () => {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff = {
+ content: [{
+ a: [new Array(501).join('*')],
+ }],
+ };
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
- test('addition with a undefined', () => {
- const diff = {content: [
- {b: ['foo', 'bar', 'baz']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
- assert.isNull(element._lastChunkForSide(diff, true));
- });
-
- test('addition with a empty', () => {
- const diff = {content: [
- {a: [], b: ['foo', 'bar', 'baz']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
- assert.isNull(element._lastChunkForSide(diff, true));
- });
-
- test('deletion with b undefined', () => {
- const diff = {content: [
- {a: ['foo', 'bar', 'baz']},
- ]};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
- });
-
- test('deletion with b empty', () => {
- const diff = {content: [
- {a: ['foo', 'bar', 'baz'], b: []},
- ]};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
- });
-
- test('empty', () => {
- const diff = {content: []};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.isNull(element._lastChunkForSide(diff, true));
- });
- });
-
- suite('_hasTrailingNewlines', () => {
- test('shared no trailing', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide')
- .returns({ab: ['foo', 'bar']});
- assert.isFalse(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
-
- test('delta trailing in right', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide')
- .returns({a: ['foo', 'bar'], b: ['baz', '']});
- assert.isTrue(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
-
- test('addition', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
- if (leftSide) { return null; }
- return {b: ['foo', '']};
- });
- assert.isTrue(element._hasTrailingNewlines(diff, false));
- assert.isNull(element._hasTrailingNewlines(diff, true));
- });
-
- test('deletion', () => {
- const diff = undefined;
- sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
- if (!leftSide) { return null; }
- return {a: ['foo']};
- });
- assert.isNull(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
+ test('starts syntax layer processing on render event', done => {
+ sandbox.stub(element.$.syntaxLayer, 'process')
+ .returns(Promise.resolve());
+ sandbox.stub(element.$.restAPI, 'getDiff').returns(
+ Promise.resolve({content: []}));
+ element.reload();
+ setTimeout(() => {
+ element.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ assert.isTrue(element.$.syntaxLayer.process.called);
+ done();
});
});
});
+
+ suite('syntax layer with syntax_highlgihting off', () => {
+ setup(() => {
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ };
+ element.diff = {
+ content: [{
+ a: ['foo'],
+ }],
+ };
+ element.patchRange = {};
+ element.prefs = prefs;
+ });
+
+ test('gr-diff-host provides syntax highlighting layer', () => {
+ element.reload();
+ assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
+ });
+
+ test('syntax layer should be disabled', () => {
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
+
+ test('still disabled for large diff', () => {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff = {
+ content: [{
+ a: [new Array(501).join('*')],
+ }],
+ };
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
+ });
+
+ suite('coverage layer', () => {
+ let notifyStub;
+ setup(() => {
+ notifyStub = sinon.stub();
+ stub('gr-js-api-interface', {
+ getCoverageAnnotationApi() {
+ return Promise.resolve({
+ notify: notifyStub,
+ getCoverageProvider() {
+ return () => Promise.resolve([
+ {
+ type: 'COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 1,
+ end_line: 2,
+ },
+ },
+ {
+ type: 'NOT_COVERED',
+ side: 'right',
+ code_range: {
+ start_line: 3,
+ end_line: 4,
+ },
+ },
+ ]);
+ },
+ });
+ },
+ });
+ element = fixture('basic');
+ const prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ };
+ element.diff = {
+ content: [{
+ a: ['foo'],
+ }],
+ };
+ element.patchRange = {};
+ element.prefs = prefs;
+ });
+
+ test('getCoverageAnnotationApi should be called', done => {
+ element.reload();
+ flush(() => {
+ assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce);
+ done();
+ });
+ });
+
+ test('coverageRangeChanged should be called', done => {
+ element.reload();
+ flush(() => {
+ assert.equal(notifyStub.callCount, 2);
+ done();
+ });
+ });
+ });
+
+ suite('trailing newlines', () => {
+ setup(() => {
+ });
+
+ suite('_lastChunkForSide', () => {
+ test('deltas', () => {
+ const diff = {content: [
+ {a: ['foo', 'bar'], b: ['baz']},
+ {ab: ['foo', 'bar', 'baz']},
+ {b: ['foo']},
+ ]};
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
+
+ diff.content.push({a: ['foo'], b: ['bar']});
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
+ });
+
+ test('addition with a undefined', () => {
+ const diff = {content: [
+ {b: ['foo', 'bar', 'baz']},
+ ]};
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+
+ test('addition with a empty', () => {
+ const diff = {content: [
+ {a: [], b: ['foo', 'bar', 'baz']},
+ ]};
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+
+ test('deletion with b undefined', () => {
+ const diff = {content: [
+ {a: ['foo', 'bar', 'baz']},
+ ]};
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+ });
+
+ test('deletion with b empty', () => {
+ const diff = {content: [
+ {a: ['foo', 'bar', 'baz'], b: []},
+ ]};
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+ });
+
+ test('empty', () => {
+ const diff = {content: []};
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+ });
+
+ suite('_hasTrailingNewlines', () => {
+ test('shared no trailing', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide')
+ .returns({ab: ['foo', 'bar']});
+ assert.isFalse(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('delta trailing in right', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide')
+ .returns({a: ['foo', 'bar'], b: ['baz', '']});
+ assert.isTrue(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('addition', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
+ if (leftSide) { return null; }
+ return {b: ['foo', '']};
+ });
+ assert.isTrue(element._hasTrailingNewlines(diff, false));
+ assert.isNull(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('deletion', () => {
+ const diff = undefined;
+ sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => {
+ if (!leftSide) { return null; }
+ return {a: ['foo']};
+ });
+ assert.isNull(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
index 68bca23..acd9457 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -14,65 +14,74 @@
* 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 GrDiffModeSelector extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-diff-mode-selector'; }
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.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-diff-mode-selector_html.js';
- static get properties() {
- return {
- mode: {
- type: String,
- notify: true,
+/** @extends Polymer.Element */
+class GrDiffModeSelector extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-mode-selector'; }
+
+ static get properties() {
+ return {
+ mode: {
+ type: String,
+ notify: true,
+ },
+
+ /**
+ * If set to true, the user's preference will be updated every time a
+ * button is tapped. Don't set to true if there is no user.
+ */
+ saveOnChange: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {?} */
+ _VIEW_MODES: {
+ type: Object,
+ readOnly: true,
+ value: {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
},
-
- /**
- * If set to true, the user's preference will be updated every time a
- * button is tapped. Don't set to true if there is no user.
- */
- saveOnChange: {
- type: Boolean,
- value: false,
- },
-
- /** @type {?} */
- _VIEW_MODES: {
- type: Object,
- readOnly: true,
- value: {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- },
- },
- };
- }
-
- /**
- * Set the mode. If save on change is enabled also update the preference.
- */
- setMode(newMode) {
- if (this.saveOnChange && this.mode && this.mode !== newMode) {
- this.$.restAPI.savePreferences({diff_view: newMode});
- }
- this.mode = newMode;
- }
-
- _computeSelectedClass(diffViewMode, buttonViewMode) {
- return buttonViewMode === diffViewMode ? 'selected' : '';
- }
-
- _handleSideBySideTap() {
- this.setMode(this._VIEW_MODES.SIDE_BY_SIDE);
- }
-
- _handleUnifiedTap() {
- this.setMode(this._VIEW_MODES.UNIFIED);
- }
+ },
+ };
}
- customElements.define(GrDiffModeSelector.is, GrDiffModeSelector);
-})();
+ /**
+ * Set the mode. If save on change is enabled also update the preference.
+ */
+ setMode(newMode) {
+ if (this.saveOnChange && this.mode && this.mode !== newMode) {
+ this.$.restAPI.savePreferences({diff_view: newMode});
+ }
+ this.mode = newMode;
+ }
+
+ _computeSelectedClass(diffViewMode, buttonViewMode) {
+ return buttonViewMode === diffViewMode ? 'selected' : '';
+ }
+
+ _handleSideBySideTap() {
+ this.setMode(this._VIEW_MODES.SIDE_BY_SIDE);
+ }
+
+ _handleUnifiedTap() {
+ this.setMode(this._VIEW_MODES.UNIFIED);
+ }
+}
+
+customElements.define(GrDiffModeSelector.is, GrDiffModeSelector);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
index 47cf771..5fe516c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
@@ -1,28 +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="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-diff-mode-selector">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
/* Used to remove horizontal whitespace between the icons. */
@@ -36,25 +30,11 @@
width: 1.3rem;
}
</style>
- <gr-button
- id="sideBySideBtn"
- link
- has-tooltip
- class$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]"
- title="Side-by-side diff"
- on-click="_handleSideBySideTap">
+ <gr-button id="sideBySideBtn" link="" has-tooltip="" class\$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]" title="Side-by-side diff" on-click="_handleSideBySideTap">
<iron-icon icon="gr-icons:side-by-side"></iron-icon>
</gr-button>
- <gr-button
- id="unifiedBtn"
- link
- has-tooltip
- title="Unified diff"
- class$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]"
- on-click="_handleUnifiedTap">
+ <gr-button id="unifiedBtn" link="" has-tooltip="" title="Unified diff" class\$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]" on-click="_handleUnifiedTap">
<iron-icon icon="gr-icons:unified"></iron-icon>
</gr-button>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-diff-mode-selector.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
index 2f3d262..921bc74 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_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-diff-mode-selector</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="../../../scripts/util.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>
+<script type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-diff-mode-selector.html">
+<script type="module" src="./gr-diff-mode-selector.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-diff-mode-selector.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -38,52 +44,55 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-mode-selector tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-diff-mode-selector.js';
+suite('gr-diff-mode-selector tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeSelectedClass', () => {
- assert.equal(
- element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'),
- 'selected');
- assert.equal(
- element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), '');
- });
-
- test('setMode', () => {
- const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences');
-
- // Setting the mode initially does not save prefs.
- element.saveOnChange = true;
- element.setMode('SIDE_BY_SIDE');
- assert.isFalse(saveStub.called);
-
- // Setting the mode to itself does not save prefs.
- element.setMode('SIDE_BY_SIDE');
- assert.isFalse(saveStub.called);
-
- // Setting the mode to something else does not save prefs if saveOnChange
- // is false.
- element.saveOnChange = false;
- element.setMode('UNIFIED_DIFF');
- assert.isFalse(saveStub.called);
-
- // Setting the mode to something else does not save prefs if saveOnChange
- // is false.
- element.saveOnChange = true;
- element.setMode('SIDE_BY_SIDE');
- assert.isTrue(saveStub.calledOnce);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeSelectedClass', () => {
+ assert.equal(
+ element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'),
+ 'selected');
+ assert.equal(
+ element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), '');
+ });
+
+ test('setMode', () => {
+ const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences');
+
+ // Setting the mode initially does not save prefs.
+ element.saveOnChange = true;
+ element.setMode('SIDE_BY_SIDE');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to itself does not save prefs.
+ element.setMode('SIDE_BY_SIDE');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to something else does not save prefs if saveOnChange
+ // is false.
+ element.saveOnChange = false;
+ element.setMode('UNIFIED_DIFF');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to something else does not save prefs if saveOnChange
+ // is false.
+ element.saveOnChange = true;
+ element.setMode('SIDE_BY_SIDE');
+ assert.isTrue(saveStub.calledOnce);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
index 6aad66c..fa79e49 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -14,65 +14,76 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrDiffPreferencesDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-preferences-dialog'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-diff-preferences/gr-diff-preferences.js';
+import '../../shared/gr-overlay/gr-overlay.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-diff-preferences-dialog_html.js';
- static get properties() {
- return {
- /** @type {?} */
- diffPrefs: Object,
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrDiffPreferencesDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _diffPrefsChanged: Boolean,
- };
- }
+ static get is() { return 'gr-diff-preferences-dialog'; }
- getFocusStops() {
- return {
- start: this.$.diffPreferences.$.contextSelect,
- end: this.$.saveButton,
- };
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ diffPrefs: Object,
- resetFocus() {
- this.$.diffPreferences.$.contextSelect.focus();
- }
-
- _computeHeaderClass(changed) {
- return changed ? 'edited' : '';
- }
-
- _handleCancelDiff(e) {
- e.stopPropagation();
- this.$.diffPrefsOverlay.close();
- }
-
- open() {
- this.$.diffPrefsOverlay.open().then(() => {
- const focusStops = this.getFocusStops();
- this.$.diffPrefsOverlay.setFocusStops(focusStops);
- this.resetFocus();
- });
- }
-
- _handleSaveDiffPreferences() {
- this.$.diffPreferences.save().then(() => {
- this.fire('reload-diff-preference', null, {bubbles: false});
-
- this.$.diffPrefsOverlay.close();
- });
- }
+ _diffPrefsChanged: Boolean,
+ };
}
- customElements.define(GrDiffPreferencesDialog.is, GrDiffPreferencesDialog);
-})();
+ getFocusStops() {
+ return {
+ start: this.$.diffPreferences.$.contextSelect,
+ end: this.$.saveButton,
+ };
+ }
+
+ resetFocus() {
+ this.$.diffPreferences.$.contextSelect.focus();
+ }
+
+ _computeHeaderClass(changed) {
+ return changed ? 'edited' : '';
+ }
+
+ _handleCancelDiff(e) {
+ e.stopPropagation();
+ this.$.diffPrefsOverlay.close();
+ }
+
+ open() {
+ this.$.diffPrefsOverlay.open().then(() => {
+ const focusStops = this.getFocusStops();
+ this.$.diffPrefsOverlay.setFocusStops(focusStops);
+ this.resetFocus();
+ });
+ }
+
+ _handleSaveDiffPreferences() {
+ this.$.diffPreferences.save().then(() => {
+ this.fire('reload-diff-preference', null, {bubbles: false});
+
+ this.$.diffPrefsOverlay.close();
+ });
+ }
+}
+
+customElements.define(GrDiffPreferencesDialog.is, GrDiffPreferencesDialog);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
index 21f6282..cd26a0b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
@@ -1,29 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 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="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-
-<dom-module id="gr-diff-preferences-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.diffHeader,
.diffActions {
@@ -54,28 +47,16 @@
padding: var(--spacing-s) var(--spacing-xl);
}
</style>
- <gr-overlay id="diffPrefsOverlay" with-backdrop>
- <div class$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div>
- <gr-diff-preferences
- id="diffPreferences"
- diff-prefs="{{diffPrefs}}"
- has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
+ <gr-overlay id="diffPrefsOverlay" with-backdrop="">
+ <div class\$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div>
+ <gr-diff-preferences id="diffPreferences" diff-prefs="{{diffPrefs}}" has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
<div class="diffActions">
- <gr-button
- id="cancelButton"
- link
- on-click="_handleCancelDiff">
+ <gr-button id="cancelButton" link="" on-click="_handleCancelDiff">
Cancel
</gr-button>
- <gr-button
- id="saveButton"
- link primary
- on-click="_handleSaveDiffPreferences"
- disabled$="[[!_diffPrefsChanged]]">
+ <gr-button id="saveButton" link="" primary="" on-click="_handleSaveDiffPreferences" disabled\$="[[!_diffPrefsChanged]]">
Save
</gr-button>
</div>
</gr-overlay>
- </template>
- <script src="gr-diff-preferences-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
deleted file mode 100644
index 7a0bce1..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html
+++ /dev/null
@@ -1,25 +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">
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../gr-diff/gr-diff-group.js"></script>
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-diff-processor">
- <script src="gr-diff-processor.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index dcda64d..adcb375 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -14,653 +14,658 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const WHOLE_FILE = -1;
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff/gr-diff-group.js';
+import '../../../scripts/util.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';
- const DiffSide = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+const WHOLE_FILE = -1;
- const DiffHighlights = {
- ADDED: 'edit_b',
- REMOVED: 'edit_a',
- };
+const DiffSide = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+const DiffHighlights = {
+ ADDED: 'edit_b',
+ REMOVED: 'edit_a',
+};
+
+/**
+ * The maximum size for an addition or removal chunk before it is broken down
+ * into a series of chunks that are this size at most.
+ *
+ * Note: The value of 120 is chosen so that it is larger than the default
+ * _asyncThreshold of 64, but feel free to tune this constant to your
+ * performance needs.
+ */
+const MAX_GROUP_SIZE = 120;
+
+/**
+ * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering.
+ *
+ * Glossary:
+ * - "chunk": A single `DiffContent` as returned by the API.
+ * - "group": A single `GrDiffGroup` as used for rendering.
+ * - "common" chunk/group: A chunk/group that should be considered unchanged
+ * for diffing purposes. This can mean its either actually unchanged, or it
+ * has only whitespace changes.
+ * - "key location": A line number and side of the diff that should not be
+ * collapsed e.g. because a comment is attached to it, or because it was
+ * provided in the URL and thus should be visible
+ * - "uncollapsible" chunk/group: A chunk/group that is either not "common",
+ * or cannot be collapsed because it contains a key location
+ *
+ * Here a a number of tasks this processor performs:
+ * - splitting large chunks to allow more granular async rendering
+ * - adding a group for the "File" pseudo line that file-level comments can
+ * be attached to
+ * - replacing common parts of the diff that are outside the user's
+ * context setting and do not have comments with a group representing the
+ * "expand context" widget. This may require splitting a chunk/group so
+ * that the part that is within the context or has comments is shown, while
+ * the rest is not.
+ *
+ * @extends Polymer.Element
+ */
+class GrDiffProcessor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-diff-processor'; }
+
+ static get properties() {
+ return {
+
+ /**
+ * The amount of context around collapsed groups.
+ */
+ context: Number,
+
+ /**
+ * The array of groups output by the processor.
+ */
+ groups: {
+ type: Array,
+ notify: true,
+ },
+
+ /**
+ * Locations that should not be collapsed, including the locations of
+ * comments.
+ */
+ keyLocations: {
+ type: Object,
+ value() { return {left: {}, right: {}}; },
+ },
+
+ /**
+ * The maximum number of lines to process synchronously.
+ */
+ _asyncThreshold: {
+ type: Number,
+ value: 64,
+ },
+
+ /** @type {?number} */
+ _nextStepHandle: Number,
+ /**
+ * The promise last returned from `process()` while the asynchronous
+ * processing is running - `null` otherwise. Provides a `cancel()`
+ * method that rejects it with `{isCancelled: true}`.
+ *
+ * @type {?Object}
+ */
+ _processPromise: {
+ type: Object,
+ value: null,
+ },
+ _isScrolling: Boolean,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.listen(window, 'scroll', '_handleWindowScroll');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.cancel();
+ this.unlisten(window, 'scroll', '_handleWindowScroll');
+ }
+
+ _handleWindowScroll() {
+ this._isScrolling = true;
+ this.debounce('resetIsScrolling', () => {
+ this._isScrolling = false;
+ }, 50);
+ }
/**
- * The maximum size for an addition or removal chunk before it is broken down
- * into a series of chunks that are this size at most.
+ * Asynchronously process the diff chunks into groups. As it processes, it
+ * will splice groups into the `groups` property of the component.
*
- * Note: The value of 120 is chosen so that it is larger than the default
- * _asyncThreshold of 64, but feel free to tune this constant to your
- * performance needs.
+ * @param {!Array<!Gerrit.DiffChunk>} chunks
+ * @param {boolean} isBinary
+ *
+ * @return {!Promise<!Array<!Object>>} A promise that resolves with an
+ * array of GrDiffGroups when the diff is completely processed.
*/
- const MAX_GROUP_SIZE = 120;
+ process(chunks, isBinary) {
+ // Cancel any still running process() calls, because they append to the
+ // same groups field.
+ this.cancel();
- /**
- * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering.
- *
- * Glossary:
- * - "chunk": A single `DiffContent` as returned by the API.
- * - "group": A single `GrDiffGroup` as used for rendering.
- * - "common" chunk/group: A chunk/group that should be considered unchanged
- * for diffing purposes. This can mean its either actually unchanged, or it
- * has only whitespace changes.
- * - "key location": A line number and side of the diff that should not be
- * collapsed e.g. because a comment is attached to it, or because it was
- * provided in the URL and thus should be visible
- * - "uncollapsible" chunk/group: A chunk/group that is either not "common",
- * or cannot be collapsed because it contains a key location
- *
- * Here a a number of tasks this processor performs:
- * - splitting large chunks to allow more granular async rendering
- * - adding a group for the "File" pseudo line that file-level comments can
- * be attached to
- * - replacing common parts of the diff that are outside the user's
- * context setting and do not have comments with a group representing the
- * "expand context" widget. This may require splitting a chunk/group so
- * that the part that is within the context or has comments is shown, while
- * the rest is not.
- *
- * @extends Polymer.Element
- */
- class GrDiffProcessor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-diff-processor'; }
+ this.groups = [];
+ this.push('groups', this._makeFileComments());
- static get properties() {
- return {
+ // If it's a binary diff, we won't be rendering hunks of text differences
+ // so finish processing.
+ if (isBinary) { return Promise.resolve(); }
- /**
- * The amount of context around collapsed groups.
- */
- context: Number,
+ this._processPromise = util.makeCancelable(
+ new Promise(resolve => {
+ const state = {
+ lineNums: {left: 0, right: 0},
+ chunkIndex: 0,
+ };
- /**
- * The array of groups output by the processor.
- */
- groups: {
- type: Array,
- notify: true,
- },
+ chunks = this._splitLargeChunks(chunks);
+ chunks = this._splitCommonChunksWithKeyLocations(chunks);
- /**
- * Locations that should not be collapsed, including the locations of
- * comments.
- */
- keyLocations: {
- type: Object,
- value() { return {left: {}, right: {}}; },
- },
-
- /**
- * The maximum number of lines to process synchronously.
- */
- _asyncThreshold: {
- type: Number,
- value: 64,
- },
-
- /** @type {?number} */
- _nextStepHandle: Number,
- /**
- * The promise last returned from `process()` while the asynchronous
- * processing is running - `null` otherwise. Provides a `cancel()`
- * method that rejects it with `{isCancelled: true}`.
- *
- * @type {?Object}
- */
- _processPromise: {
- type: Object,
- value: null,
- },
- _isScrolling: Boolean,
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- this.listen(window, 'scroll', '_handleWindowScroll');
- }
-
- /** @override */
- detached() {
- super.detached();
- this.cancel();
- this.unlisten(window, 'scroll', '_handleWindowScroll');
- }
-
- _handleWindowScroll() {
- this._isScrolling = true;
- this.debounce('resetIsScrolling', () => {
- this._isScrolling = false;
- }, 50);
- }
-
- /**
- * Asynchronously process the diff chunks into groups. As it processes, it
- * will splice groups into the `groups` property of the component.
- *
- * @param {!Array<!Gerrit.DiffChunk>} chunks
- * @param {boolean} isBinary
- *
- * @return {!Promise<!Array<!Object>>} A promise that resolves with an
- * array of GrDiffGroups when the diff is completely processed.
- */
- process(chunks, isBinary) {
- // Cancel any still running process() calls, because they append to the
- // same groups field.
- this.cancel();
-
- this.groups = [];
- this.push('groups', this._makeFileComments());
-
- // If it's a binary diff, we won't be rendering hunks of text differences
- // so finish processing.
- if (isBinary) { return Promise.resolve(); }
-
- this._processPromise = util.makeCancelable(
- new Promise(resolve => {
- const state = {
- lineNums: {left: 0, right: 0},
- chunkIndex: 0,
- };
-
- chunks = this._splitLargeChunks(chunks);
- chunks = this._splitCommonChunksWithKeyLocations(chunks);
-
- let currentBatch = 0;
- const nextStep = () => {
- if (this._isScrolling) {
- this._nextStepHandle = this.async(nextStep, 100);
- return;
- }
- // If we are done, resolve the promise.
- if (state.chunkIndex >= chunks.length) {
- resolve();
- this._nextStepHandle = null;
- return;
- }
-
- // Process the next chunk and incorporate the result.
- const stateUpdate = this._processNext(state, chunks);
- for (const group of stateUpdate.groups) {
- this.push('groups', group);
- currentBatch += group.lines.length;
- }
- state.lineNums.left += stateUpdate.lineDelta.left;
- state.lineNums.right += stateUpdate.lineDelta.right;
-
- // Increment the index and recurse.
- state.chunkIndex = stateUpdate.newChunkIndex;
- if (currentBatch >= this._asyncThreshold) {
- currentBatch = 0;
- this._nextStepHandle = this.async(nextStep, 1);
- } else {
- nextStep.call(this);
- }
- };
-
- nextStep.call(this);
- }));
- return this._processPromise
- .finally(() => { this._processPromise = null; });
- }
-
- /**
- * Cancel any jobs that are running.
- */
- cancel() {
- if (this._nextStepHandle != null) {
- this.cancelAsync(this._nextStepHandle);
- this._nextStepHandle = null;
- }
- if (this._processPromise) {
- this._processPromise.cancel();
- }
- }
-
- /**
- * Process the next uncollapsible chunk, or the next collapsible chunks.
- *
- * @param {!Object} state
- * @param {!Array<!Object>} chunks
- * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
- */
- _processNext(state, chunks) {
- const firstUncollapsibleChunkIndex =
- this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex);
- if (firstUncollapsibleChunkIndex === state.chunkIndex) {
- const chunk = chunks[state.chunkIndex];
- return {
- lineDelta: {
- left: this._linesLeft(chunk).length,
- right: this._linesRight(chunk).length,
- },
- groups: [this._chunkToGroup(
- chunk, state.lineNums.left + 1, state.lineNums.right + 1)],
- newChunkIndex: state.chunkIndex + 1,
- };
- }
-
- return this._processCollapsibleChunks(
- state, chunks, firstUncollapsibleChunkIndex);
- }
-
- _linesLeft(chunk) {
- return chunk.ab || chunk.a || [];
- }
-
- _linesRight(chunk) {
- return chunk.ab || chunk.b || [];
- }
-
- _firstUncollapsibleChunkIndex(chunks, offset) {
- let chunkIndex = offset;
- while (chunkIndex < chunks.length &&
- this._isCollapsibleChunk(chunks[chunkIndex])) {
- chunkIndex++;
- }
- return chunkIndex;
- }
-
- _isCollapsibleChunk(chunk) {
- return (chunk.ab || chunk.common) && !chunk.keyLocation;
- }
-
- /**
- * Process a stretch of collapsible chunks.
- *
- * Outputs up to three groups:
- * 1) Visible context before the hidden common code, unless it's the
- * very beginning of the file.
- * 2) Context hidden behind a context bar, unless empty.
- * 3) Visible context after the hidden common code, unless it's the very
- * end of the file.
- *
- * @param {!Object} state
- * @param {!Array<Object>} chunks
- * @param {number} firstUncollapsibleChunkIndex
- * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
- */
- _processCollapsibleChunks(
- state, chunks, firstUncollapsibleChunkIndex) {
- const collapsibleChunks = chunks.slice(
- state.chunkIndex, firstUncollapsibleChunkIndex);
- const lineCount = collapsibleChunks.reduce(
- (sum, chunk) => sum + this._commonChunkLength(chunk), 0);
-
- let groups = this._chunksToGroups(
- collapsibleChunks,
- state.lineNums.left + 1,
- state.lineNums.right + 1);
-
- if (this.context !== WHOLE_FILE) {
- const hiddenStart = state.chunkIndex === 0 ? 0 : this.context;
- const hiddenEnd = lineCount - (
- firstUncollapsibleChunkIndex === chunks.length ?
- 0 : this.context);
- groups = GrDiffGroup.hideInContextControl(
- groups, hiddenStart, hiddenEnd);
- }
-
- return {
- lineDelta: {
- left: lineCount,
- right: lineCount,
- },
- groups,
- newChunkIndex: firstUncollapsibleChunkIndex,
- };
- }
-
- _commonChunkLength(chunk) {
- console.assert(chunk.ab || chunk.common);
- console.assert(
- !chunk.a || (chunk.b && chunk.a.length === chunk.b.length),
- `common chunk needs same number of a and b lines: `, chunk);
- return this._linesLeft(chunk).length;
- }
-
- /**
- * @param {!Array<!Object>} chunks
- * @param {number} offsetLeft
- * @param {number} offsetRight
- * @return {!Array<!Object>} (GrDiffGroup)
- */
- _chunksToGroups(chunks, offsetLeft, offsetRight) {
- return chunks.map(chunk => {
- const group = this._chunkToGroup(chunk, offsetLeft, offsetRight);
- const chunkLength = this._commonChunkLength(chunk);
- offsetLeft += chunkLength;
- offsetRight += chunkLength;
- return group;
- });
- }
-
- /**
- * @param {!Object} chunk
- * @param {number} offsetLeft
- * @param {number} offsetRight
- * @return {!Object} (GrDiffGroup)
- */
- _chunkToGroup(chunk, offsetLeft, offsetRight) {
- const type = chunk.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA;
- const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
- const group = new GrDiffGroup(type, lines);
- group.keyLocation = chunk.keyLocation;
- group.dueToRebase = chunk.due_to_rebase;
- group.ignoredWhitespaceOnly = chunk.common;
- return group;
- }
-
- _linesFromChunk(chunk, offsetLeft, offsetRight) {
- if (chunk.ab) {
- return chunk.ab.map((row, i) => this._lineFromRow(
- GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i));
- }
- let lines = [];
- if (chunk.a) {
- // Avoiding a.push(...b) because that causes callstack overflows for
- // large b, which can occur when large files are added removed.
- lines = lines.concat(this._linesFromRows(
- GrDiffLine.Type.REMOVE, chunk.a, offsetLeft,
- chunk[DiffHighlights.REMOVED]));
- }
- if (chunk.b) {
- // Avoiding a.push(...b) because that causes callstack overflows for
- // large b, which can occur when large files are added removed.
- lines = lines.concat(this._linesFromRows(
- GrDiffLine.Type.ADD, chunk.b, offsetRight,
- chunk[DiffHighlights.ADDED]));
- }
- return lines;
- }
-
- /**
- * @param {string} lineType (GrDiffLine.Type)
- * @param {!Array<string>} rows
- * @param {number} offset
- * @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos
- * @return {!Array<!Object>} (GrDiffLine)
- */
- _linesFromRows(lineType, rows, offset, opt_intralineInfos) {
- const grDiffHighlights = opt_intralineInfos ?
- this._convertIntralineInfos(rows, opt_intralineInfos) : undefined;
- return rows.map((row, i) => this._lineFromRow(
- lineType, offset, offset, row, i, grDiffHighlights));
- }
-
- /**
- * @param {string} type (GrDiffLine.Type)
- * @param {number} offsetLeft
- * @param {number} offsetRight
- * @param {string} row
- * @param {number} i
- * @param {!Array<!Object>=} opt_highlights
- * @return {!Object} (GrDiffLine)
- */
- _lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) {
- const line = new GrDiffLine(type);
- line.text = row;
- if (type !== GrDiffLine.Type.ADD) line.beforeNumber = offsetLeft + i;
- if (type !== GrDiffLine.Type.REMOVE) line.afterNumber = offsetRight + i;
- if (opt_highlights) {
- line.hasIntralineInfo = true;
- line.highlights = opt_highlights.filter(hl => hl.contentIndex === i);
- } else {
- line.hasIntralineInfo = false;
- }
- return line;
- }
-
- _makeFileComments() {
- const line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.beforeNumber = GrDiffLine.FILE;
- line.afterNumber = GrDiffLine.FILE;
- return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]);
- }
-
- /**
- * Split chunks into smaller chunks of the same kind.
- *
- * This is done to prevent doing too much work on the main thread in one
- * uninterrupted rendering step, which would make the browser unresponsive.
- *
- * Note that in the case of unmodified chunks, we only split chunks if the
- * context is set to file (because otherwise they are split up further down
- * the processing into the visible and hidden context), and only split it
- * into 2 chunks, one max sized one and the rest (for reasons that are
- * unclear to me).
- *
- * @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server
- * @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks.
- */
- _splitLargeChunks(chunks) {
- const newChunks = [];
-
- for (const chunk of chunks) {
- if (!chunk.ab) {
- for (const subChunk of this._breakdownChunk(chunk)) {
- newChunks.push(subChunk);
- }
- continue;
- }
-
- // If the context is set to "whole file", then break down the shared
- // chunks so they can be rendered incrementally. Note: this is not
- // enabled for any other context preference because manipulating the
- // chunks in this way violates assumptions by the context grouper logic.
- if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
- // Split large shared chunks in two, where the first is the maximum
- // group size.
- newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
- newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
- } else {
- newChunks.push(chunk);
- }
- }
- return newChunks;
- }
-
- /**
- * In order to show key locations, such as comments, out of the bounds of
- * the selected context, treat them as separate chunks within the model so
- * that the content (and context surrounding it) renders correctly.
- *
- * @param {!Array<!Object>} chunks DiffContents as returned from server.
- * @return {!Array<!Object>} Finer grained DiffContents.
- */
- _splitCommonChunksWithKeyLocations(chunks) {
- const result = [];
- let leftLineNum = 1;
- let rightLineNum = 1;
-
- for (const chunk of chunks) {
- // If it isn't a common chunk, append it as-is and update line numbers.
- if (!chunk.ab && !chunk.common) {
- if (chunk.a) {
- leftLineNum += chunk.a.length;
- }
- if (chunk.b) {
- rightLineNum += chunk.b.length;
- }
- result.push(chunk);
- continue;
- }
-
- if (chunk.common && chunk.a.length != chunk.b.length) {
- throw new Error(
- 'DiffContent with common=true must always have equal length');
- }
- const numLines = this._commonChunkLength(chunk);
- const chunkEnds = this._findChunkEndsAtKeyLocations(
- numLines, leftLineNum, rightLineNum);
- leftLineNum += numLines;
- rightLineNum += numLines;
-
- if (chunk.ab) {
- result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds)
- .map(({lines, keyLocation}) =>
- Object.assign({}, chunk, {ab: lines, keyLocation})));
- } else if (chunk.common) {
- const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds);
- const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds);
- result.push(...aChunks.map(({lines, keyLocation}, i) =>
- Object.assign(
- {}, chunk, {a: lines, b: bChunks[i].lines, keyLocation})));
- }
- }
-
- return result;
- }
-
- /**
- * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the
- * new chunk ends, including whether it's a key location.
- */
- _findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) {
- const result = [];
- let lastChunkEnd = 0;
- for (let i=0; i<numLines; i++) {
- // If this line should not be collapsed.
- if (this.keyLocations[DiffSide.LEFT][leftOffset + i] ||
- this.keyLocations[DiffSide.RIGHT][rightOffset + i]) {
- // If any lines have been accumulated into the chunk leading up to
- // this non-collapse line, then add them as a chunk and start a new
- // one.
- if (i > lastChunkEnd) {
- result.push({offset: i, keyLocation: false});
- lastChunkEnd = i;
- }
-
- // Add the non-collapse line as its own chunk.
- result.push({offset: i + 1, keyLocation: true});
- }
- }
-
- if (numLines > lastChunkEnd) {
- result.push({offset: numLines, keyLocation: false});
- }
-
- return result;
- }
-
- _splitAtChunkEnds(lines, chunkEnds) {
- const result = [];
- let lastChunkEndOffset = 0;
- for (const {offset, keyLocation} of chunkEnds) {
- result.push(
- {lines: lines.slice(lastChunkEndOffset, offset), keyLocation});
- lastChunkEndOffset = offset;
- }
- return result;
- }
-
- /**
- * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
- * for rendering.
- *
- * @param {!Array<string>} rows
- * @param {!Array<!Gerrit.IntralineInfo>} intralineInfos
- * @return {!Array<!Object>} (GrDiffLine.Highlight)
- */
- _convertIntralineInfos(rows, intralineInfos) {
- let rowIndex = 0;
- let idx = 0;
- const normalized = [];
- for (const [skipLength, markLength] of intralineInfos) {
- let line = rows[rowIndex] + '\n';
- let j = 0;
- while (j < skipLength) {
- if (idx === line.length) {
- idx = 0;
- line = rows[++rowIndex] + '\n';
- continue;
- }
- idx++;
- j++;
- }
- let lineHighlight = {
- contentIndex: rowIndex,
- startIndex: idx,
- };
-
- j = 0;
- while (line && j < markLength) {
- if (idx === line.length) {
- idx = 0;
- line = rows[++rowIndex] + '\n';
- normalized.push(lineHighlight);
- lineHighlight = {
- contentIndex: rowIndex,
- startIndex: idx,
- };
- continue;
- }
- idx++;
- j++;
- }
- lineHighlight.endIndex = idx;
- normalized.push(lineHighlight);
- }
- return normalized;
- }
-
- /**
- * If a group is an addition or a removal, break it down into smaller groups
- * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk
- * or a delta it is returned as the single element of the result array.
- *
- * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response.
- * @return {!Array<!Array<!Object>>}
- */
- _breakdownChunk(chunk) {
- let key = null;
- if (chunk.a && !chunk.b) {
- key = 'a';
- } else if (chunk.b && !chunk.a) {
- key = 'b';
- } else if (chunk.ab) {
- key = 'ab';
- }
-
- if (!key) { return [chunk]; }
-
- return this._breakdown(chunk[key], MAX_GROUP_SIZE)
- .map(subChunkLines => {
- const subChunk = {};
- subChunk[key] = subChunkLines;
- if (chunk.due_to_rebase) {
- subChunk.due_to_rebase = true;
+ let currentBatch = 0;
+ const nextStep = () => {
+ if (this._isScrolling) {
+ this._nextStepHandle = this.async(nextStep, 100);
+ return;
}
- return subChunk;
- });
+ // If we are done, resolve the promise.
+ if (state.chunkIndex >= chunks.length) {
+ resolve();
+ this._nextStepHandle = null;
+ return;
+ }
+
+ // Process the next chunk and incorporate the result.
+ const stateUpdate = this._processNext(state, chunks);
+ for (const group of stateUpdate.groups) {
+ this.push('groups', group);
+ currentBatch += group.lines.length;
+ }
+ state.lineNums.left += stateUpdate.lineDelta.left;
+ state.lineNums.right += stateUpdate.lineDelta.right;
+
+ // Increment the index and recurse.
+ state.chunkIndex = stateUpdate.newChunkIndex;
+ if (currentBatch >= this._asyncThreshold) {
+ currentBatch = 0;
+ this._nextStepHandle = this.async(nextStep, 1);
+ } else {
+ nextStep.call(this);
+ }
+ };
+
+ nextStep.call(this);
+ }));
+ return this._processPromise
+ .finally(() => { this._processPromise = null; });
+ }
+
+ /**
+ * Cancel any jobs that are running.
+ */
+ cancel() {
+ if (this._nextStepHandle != null) {
+ this.cancelAsync(this._nextStepHandle);
+ this._nextStepHandle = null;
}
-
- /**
- * Given an array and a size, return an array of arrays where no inner array
- * is larger than that size, preserving the original order.
- *
- * @param {!Array<T>} array
- * @param {number} size
- * @return {!Array<!Array<T>>}
- * @template T
- */
- _breakdown(array, size) {
- if (!array.length) { return []; }
- if (array.length < size) { return [array]; }
-
- const head = array.slice(0, array.length - size);
- const tail = array.slice(array.length - size);
-
- return this._breakdown(head, size).concat([tail]);
+ if (this._processPromise) {
+ this._processPromise.cancel();
}
}
- customElements.define(GrDiffProcessor.is, GrDiffProcessor);
-})();
+ /**
+ * Process the next uncollapsible chunk, or the next collapsible chunks.
+ *
+ * @param {!Object} state
+ * @param {!Array<!Object>} chunks
+ * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
+ */
+ _processNext(state, chunks) {
+ const firstUncollapsibleChunkIndex =
+ this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex);
+ if (firstUncollapsibleChunkIndex === state.chunkIndex) {
+ const chunk = chunks[state.chunkIndex];
+ return {
+ lineDelta: {
+ left: this._linesLeft(chunk).length,
+ right: this._linesRight(chunk).length,
+ },
+ groups: [this._chunkToGroup(
+ chunk, state.lineNums.left + 1, state.lineNums.right + 1)],
+ newChunkIndex: state.chunkIndex + 1,
+ };
+ }
+
+ return this._processCollapsibleChunks(
+ state, chunks, firstUncollapsibleChunkIndex);
+ }
+
+ _linesLeft(chunk) {
+ return chunk.ab || chunk.a || [];
+ }
+
+ _linesRight(chunk) {
+ return chunk.ab || chunk.b || [];
+ }
+
+ _firstUncollapsibleChunkIndex(chunks, offset) {
+ let chunkIndex = offset;
+ while (chunkIndex < chunks.length &&
+ this._isCollapsibleChunk(chunks[chunkIndex])) {
+ chunkIndex++;
+ }
+ return chunkIndex;
+ }
+
+ _isCollapsibleChunk(chunk) {
+ return (chunk.ab || chunk.common) && !chunk.keyLocation;
+ }
+
+ /**
+ * Process a stretch of collapsible chunks.
+ *
+ * Outputs up to three groups:
+ * 1) Visible context before the hidden common code, unless it's the
+ * very beginning of the file.
+ * 2) Context hidden behind a context bar, unless empty.
+ * 3) Visible context after the hidden common code, unless it's the very
+ * end of the file.
+ *
+ * @param {!Object} state
+ * @param {!Array<Object>} chunks
+ * @param {number} firstUncollapsibleChunkIndex
+ * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
+ */
+ _processCollapsibleChunks(
+ state, chunks, firstUncollapsibleChunkIndex) {
+ const collapsibleChunks = chunks.slice(
+ state.chunkIndex, firstUncollapsibleChunkIndex);
+ const lineCount = collapsibleChunks.reduce(
+ (sum, chunk) => sum + this._commonChunkLength(chunk), 0);
+
+ let groups = this._chunksToGroups(
+ collapsibleChunks,
+ state.lineNums.left + 1,
+ state.lineNums.right + 1);
+
+ if (this.context !== WHOLE_FILE) {
+ const hiddenStart = state.chunkIndex === 0 ? 0 : this.context;
+ const hiddenEnd = lineCount - (
+ firstUncollapsibleChunkIndex === chunks.length ?
+ 0 : this.context);
+ groups = GrDiffGroup.hideInContextControl(
+ groups, hiddenStart, hiddenEnd);
+ }
+
+ return {
+ lineDelta: {
+ left: lineCount,
+ right: lineCount,
+ },
+ groups,
+ newChunkIndex: firstUncollapsibleChunkIndex,
+ };
+ }
+
+ _commonChunkLength(chunk) {
+ console.assert(chunk.ab || chunk.common);
+ console.assert(
+ !chunk.a || (chunk.b && chunk.a.length === chunk.b.length),
+ `common chunk needs same number of a and b lines: `, chunk);
+ return this._linesLeft(chunk).length;
+ }
+
+ /**
+ * @param {!Array<!Object>} chunks
+ * @param {number} offsetLeft
+ * @param {number} offsetRight
+ * @return {!Array<!Object>} (GrDiffGroup)
+ */
+ _chunksToGroups(chunks, offsetLeft, offsetRight) {
+ return chunks.map(chunk => {
+ const group = this._chunkToGroup(chunk, offsetLeft, offsetRight);
+ const chunkLength = this._commonChunkLength(chunk);
+ offsetLeft += chunkLength;
+ offsetRight += chunkLength;
+ return group;
+ });
+ }
+
+ /**
+ * @param {!Object} chunk
+ * @param {number} offsetLeft
+ * @param {number} offsetRight
+ * @return {!Object} (GrDiffGroup)
+ */
+ _chunkToGroup(chunk, offsetLeft, offsetRight) {
+ const type = chunk.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA;
+ const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
+ const group = new GrDiffGroup(type, lines);
+ group.keyLocation = chunk.keyLocation;
+ group.dueToRebase = chunk.due_to_rebase;
+ group.ignoredWhitespaceOnly = chunk.common;
+ return group;
+ }
+
+ _linesFromChunk(chunk, offsetLeft, offsetRight) {
+ if (chunk.ab) {
+ return chunk.ab.map((row, i) => this._lineFromRow(
+ GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i));
+ }
+ let lines = [];
+ if (chunk.a) {
+ // Avoiding a.push(...b) because that causes callstack overflows for
+ // large b, which can occur when large files are added removed.
+ lines = lines.concat(this._linesFromRows(
+ GrDiffLine.Type.REMOVE, chunk.a, offsetLeft,
+ chunk[DiffHighlights.REMOVED]));
+ }
+ if (chunk.b) {
+ // Avoiding a.push(...b) because that causes callstack overflows for
+ // large b, which can occur when large files are added removed.
+ lines = lines.concat(this._linesFromRows(
+ GrDiffLine.Type.ADD, chunk.b, offsetRight,
+ chunk[DiffHighlights.ADDED]));
+ }
+ return lines;
+ }
+
+ /**
+ * @param {string} lineType (GrDiffLine.Type)
+ * @param {!Array<string>} rows
+ * @param {number} offset
+ * @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos
+ * @return {!Array<!Object>} (GrDiffLine)
+ */
+ _linesFromRows(lineType, rows, offset, opt_intralineInfos) {
+ const grDiffHighlights = opt_intralineInfos ?
+ this._convertIntralineInfos(rows, opt_intralineInfos) : undefined;
+ return rows.map((row, i) => this._lineFromRow(
+ lineType, offset, offset, row, i, grDiffHighlights));
+ }
+
+ /**
+ * @param {string} type (GrDiffLine.Type)
+ * @param {number} offsetLeft
+ * @param {number} offsetRight
+ * @param {string} row
+ * @param {number} i
+ * @param {!Array<!Object>=} opt_highlights
+ * @return {!Object} (GrDiffLine)
+ */
+ _lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) {
+ const line = new GrDiffLine(type);
+ line.text = row;
+ if (type !== GrDiffLine.Type.ADD) line.beforeNumber = offsetLeft + i;
+ if (type !== GrDiffLine.Type.REMOVE) line.afterNumber = offsetRight + i;
+ if (opt_highlights) {
+ line.hasIntralineInfo = true;
+ line.highlights = opt_highlights.filter(hl => hl.contentIndex === i);
+ } else {
+ line.hasIntralineInfo = false;
+ }
+ return line;
+ }
+
+ _makeFileComments() {
+ const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = GrDiffLine.FILE;
+ line.afterNumber = GrDiffLine.FILE;
+ return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]);
+ }
+
+ /**
+ * Split chunks into smaller chunks of the same kind.
+ *
+ * This is done to prevent doing too much work on the main thread in one
+ * uninterrupted rendering step, which would make the browser unresponsive.
+ *
+ * Note that in the case of unmodified chunks, we only split chunks if the
+ * context is set to file (because otherwise they are split up further down
+ * the processing into the visible and hidden context), and only split it
+ * into 2 chunks, one max sized one and the rest (for reasons that are
+ * unclear to me).
+ *
+ * @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server
+ * @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks.
+ */
+ _splitLargeChunks(chunks) {
+ const newChunks = [];
+
+ for (const chunk of chunks) {
+ if (!chunk.ab) {
+ for (const subChunk of this._breakdownChunk(chunk)) {
+ newChunks.push(subChunk);
+ }
+ continue;
+ }
+
+ // If the context is set to "whole file", then break down the shared
+ // chunks so they can be rendered incrementally. Note: this is not
+ // enabled for any other context preference because manipulating the
+ // chunks in this way violates assumptions by the context grouper logic.
+ if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
+ // Split large shared chunks in two, where the first is the maximum
+ // group size.
+ newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
+ newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
+ } else {
+ newChunks.push(chunk);
+ }
+ }
+ return newChunks;
+ }
+
+ /**
+ * In order to show key locations, such as comments, out of the bounds of
+ * the selected context, treat them as separate chunks within the model so
+ * that the content (and context surrounding it) renders correctly.
+ *
+ * @param {!Array<!Object>} chunks DiffContents as returned from server.
+ * @return {!Array<!Object>} Finer grained DiffContents.
+ */
+ _splitCommonChunksWithKeyLocations(chunks) {
+ const result = [];
+ let leftLineNum = 1;
+ let rightLineNum = 1;
+
+ for (const chunk of chunks) {
+ // If it isn't a common chunk, append it as-is and update line numbers.
+ if (!chunk.ab && !chunk.common) {
+ if (chunk.a) {
+ leftLineNum += chunk.a.length;
+ }
+ if (chunk.b) {
+ rightLineNum += chunk.b.length;
+ }
+ result.push(chunk);
+ continue;
+ }
+
+ if (chunk.common && chunk.a.length != chunk.b.length) {
+ throw new Error(
+ 'DiffContent with common=true must always have equal length');
+ }
+ const numLines = this._commonChunkLength(chunk);
+ const chunkEnds = this._findChunkEndsAtKeyLocations(
+ numLines, leftLineNum, rightLineNum);
+ leftLineNum += numLines;
+ rightLineNum += numLines;
+
+ if (chunk.ab) {
+ result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds)
+ .map(({lines, keyLocation}) =>
+ Object.assign({}, chunk, {ab: lines, keyLocation})));
+ } else if (chunk.common) {
+ const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds);
+ const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds);
+ result.push(...aChunks.map(({lines, keyLocation}, i) =>
+ Object.assign(
+ {}, chunk, {a: lines, b: bChunks[i].lines, keyLocation})));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the
+ * new chunk ends, including whether it's a key location.
+ */
+ _findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) {
+ const result = [];
+ let lastChunkEnd = 0;
+ for (let i=0; i<numLines; i++) {
+ // If this line should not be collapsed.
+ if (this.keyLocations[DiffSide.LEFT][leftOffset + i] ||
+ this.keyLocations[DiffSide.RIGHT][rightOffset + i]) {
+ // If any lines have been accumulated into the chunk leading up to
+ // this non-collapse line, then add them as a chunk and start a new
+ // one.
+ if (i > lastChunkEnd) {
+ result.push({offset: i, keyLocation: false});
+ lastChunkEnd = i;
+ }
+
+ // Add the non-collapse line as its own chunk.
+ result.push({offset: i + 1, keyLocation: true});
+ }
+ }
+
+ if (numLines > lastChunkEnd) {
+ result.push({offset: numLines, keyLocation: false});
+ }
+
+ return result;
+ }
+
+ _splitAtChunkEnds(lines, chunkEnds) {
+ const result = [];
+ let lastChunkEndOffset = 0;
+ for (const {offset, keyLocation} of chunkEnds) {
+ result.push(
+ {lines: lines.slice(lastChunkEndOffset, offset), keyLocation});
+ lastChunkEndOffset = offset;
+ }
+ return result;
+ }
+
+ /**
+ * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
+ * for rendering.
+ *
+ * @param {!Array<string>} rows
+ * @param {!Array<!Gerrit.IntralineInfo>} intralineInfos
+ * @return {!Array<!Object>} (GrDiffLine.Highlight)
+ */
+ _convertIntralineInfos(rows, intralineInfos) {
+ let rowIndex = 0;
+ let idx = 0;
+ const normalized = [];
+ for (const [skipLength, markLength] of intralineInfos) {
+ let line = rows[rowIndex] + '\n';
+ let j = 0;
+ while (j < skipLength) {
+ if (idx === line.length) {
+ idx = 0;
+ line = rows[++rowIndex] + '\n';
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ let lineHighlight = {
+ contentIndex: rowIndex,
+ startIndex: idx,
+ };
+
+ j = 0;
+ while (line && j < markLength) {
+ if (idx === line.length) {
+ idx = 0;
+ line = rows[++rowIndex] + '\n';
+ normalized.push(lineHighlight);
+ lineHighlight = {
+ contentIndex: rowIndex,
+ startIndex: idx,
+ };
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ lineHighlight.endIndex = idx;
+ normalized.push(lineHighlight);
+ }
+ return normalized;
+ }
+
+ /**
+ * If a group is an addition or a removal, break it down into smaller groups
+ * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk
+ * or a delta it is returned as the single element of the result array.
+ *
+ * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response.
+ * @return {!Array<!Array<!Object>>}
+ */
+ _breakdownChunk(chunk) {
+ let key = null;
+ if (chunk.a && !chunk.b) {
+ key = 'a';
+ } else if (chunk.b && !chunk.a) {
+ key = 'b';
+ } else if (chunk.ab) {
+ key = 'ab';
+ }
+
+ if (!key) { return [chunk]; }
+
+ return this._breakdown(chunk[key], MAX_GROUP_SIZE)
+ .map(subChunkLines => {
+ const subChunk = {};
+ subChunk[key] = subChunkLines;
+ if (chunk.due_to_rebase) {
+ subChunk.due_to_rebase = true;
+ }
+ return subChunk;
+ });
+ }
+
+ /**
+ * Given an array and a size, return an array of arrays where no inner array
+ * is larger than that size, preserving the original order.
+ *
+ * @param {!Array<T>} array
+ * @param {number} size
+ * @return {!Array<!Array<T>>}
+ * @template T
+ */
+ _breakdown(array, size) {
+ if (!array.length) { return []; }
+ if (array.length < size) { return [array]; }
+
+ const head = array.slice(0, array.length - size);
+ const tail = array.slice(array.length - size);
+
+ return this._breakdown(head, size).concat([tail]);
+ }
+}
+
+customElements.define(GrDiffProcessor.is, GrDiffProcessor);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 9b3f3b2..e62abe5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_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-diff-processor test</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-diff-processor.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-diff-processor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-processor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,902 +40,904 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-processor tests', async () => {
- await readyToTest();
- const WHOLE_FILE = -1;
- const loremIpsum =
- 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' +
- 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' +
- 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' +
- 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' +
- 'fugit assum per.';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-processor.js';
+suite('gr-diff-processor tests', () => {
+ const WHOLE_FILE = -1;
+ const loremIpsum =
+ 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' +
+ 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' +
+ 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' +
+ 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' +
+ 'fugit assum per.';
- let element;
- let sandbox;
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('not logged in', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+
+ element.context = 4;
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('not logged in', () => {
- setup(() => {
- element = fixture('basic');
-
- element.context = 4;
- });
-
- test('process loaded content', () => {
- const content = [
- {
- ab: [
- '<!DOCTYPE html>',
- '<meta charset="utf-8">',
- ],
- },
- {
- a: [
- ' Welcome ',
- ' to the wooorld of tomorrow!',
- ],
- b: [
- ' Hello, world!',
- ],
- },
- {
- ab: [
- 'Leela: This is the only place the ship can’t hear us, so ',
- 'everyone pretend to shower.',
- 'Fry: Same as every day. Got it.',
- ],
- },
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- assert.equal(groups.length, 4);
-
- let group = groups[0];
- assert.equal(group.type, GrDiffGroup.Type.BOTH);
- assert.equal(group.lines.length, 1);
- assert.equal(group.lines[0].text, '');
- assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
- assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
-
- group = groups[1];
- assert.equal(group.type, GrDiffGroup.Type.BOTH);
- assert.equal(group.lines.length, 2);
- assert.equal(group.lines.length, 2);
-
- function beforeNumberFn(l) { return l.beforeNumber; }
- function afterNumberFn(l) { return l.afterNumber; }
- function textFn(l) { return l.text; }
-
- assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
- assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
- assert.deepEqual(group.lines.map(textFn), [
+ test('process loaded content', () => {
+ const content = [
+ {
+ ab: [
'<!DOCTYPE html>',
'<meta charset="utf-8">',
- ]);
-
- group = groups[2];
- assert.equal(group.type, GrDiffGroup.Type.DELTA);
- assert.equal(group.lines.length, 3);
- assert.equal(group.adds.length, 1);
- assert.equal(group.removes.length, 2);
- assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
- assert.deepEqual(group.adds.map(afterNumberFn), [3]);
- assert.deepEqual(group.removes.map(textFn), [
+ ],
+ },
+ {
+ a: [
' Welcome ',
' to the wooorld of tomorrow!',
- ]);
- assert.deepEqual(group.adds.map(textFn), [
+ ],
+ b: [
' Hello, world!',
- ]);
-
- group = groups[3];
- assert.equal(group.type, GrDiffGroup.Type.BOTH);
- assert.equal(group.lines.length, 3);
- assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
- assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
- assert.deepEqual(group.lines.map(textFn), [
+ ],
+ },
+ {
+ ab: [
'Leela: This is the only place the ship can’t hear us, so ',
'everyone pretend to shower.',
'Fry: Same as every day. Got it.',
- ]);
- });
- });
+ ],
+ },
+ ];
- test('first group is for file', () => {
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ assert.equal(groups.length, 4);
+
+ let group = groups[0];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 1);
+ assert.equal(group.lines[0].text, '');
+ assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
+ assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
+
+ group = groups[1];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 2);
+ assert.equal(group.lines.length, 2);
+
+ function beforeNumberFn(l) { return l.beforeNumber; }
+ function afterNumberFn(l) { return l.afterNumber; }
+ function textFn(l) { return l.text; }
+
+ assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(textFn), [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ]);
+
+ group = groups[2];
+ assert.equal(group.type, GrDiffGroup.Type.DELTA);
+ assert.equal(group.lines.length, 3);
+ assert.equal(group.adds.length, 1);
+ assert.equal(group.removes.length, 2);
+ assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
+ assert.deepEqual(group.adds.map(afterNumberFn), [3]);
+ assert.deepEqual(group.removes.map(textFn), [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ]);
+ assert.deepEqual(group.adds.map(textFn), [
+ ' Hello, world!',
+ ]);
+
+ group = groups[3];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 3);
+ assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
+ assert.deepEqual(group.lines.map(textFn), [
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]);
+ });
+ });
+
+ test('first group is for file', () => {
+ const content = [
+ {b: ['foo']},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[0].lines.length, 1);
+ assert.equal(groups[0].lines[0].text, '');
+ assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
+ assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+ });
+ });
+
+ suite('context groups', () => {
+ test('at the beginning, larger than context', () => {
+ element.context = 10;
const content = [
- {b: ['foo']},
+ {ab: new Array(100)
+ .fill('all work and no play make jack a dull boy')},
+ {a: ['all work and no play make andybons a dull boy']},
];
return element.process(content).then(() => {
const groups = element.groups;
- assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[0].lines.length, 1);
- assert.equal(groups[0].lines[0].text, '');
- assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
- assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+ // group[0] is the file group
+
+ assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.instanceOf(groups[1].lines[0].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[1].lines[0].contextGroups[0].lines.length, 90);
+ for (const l of groups[1].lines[0].contextGroups[0].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
});
});
- suite('context groups', () => {
- test('at the beginning, larger than context', () => {
- element.context = 10;
- const content = [
- {ab: new Array(100)
- .fill('all work and no play make jack a dull boy')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
-
- assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.instanceOf(groups[1].lines[0].contextGroups[0], GrDiffGroup);
- assert.equal(groups[1].lines[0].contextGroups[0].lines.length, 90);
- for (const l of groups[1].lines[0].contextGroups[0].lines) {
- assert.equal(l.text, 'all work and no play make jack a dull boy');
- }
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 10);
- for (const l of groups[2].lines) {
- assert.equal(l.text, 'all work and no play make jack a dull boy');
- }
- });
- });
-
- test('at the beginning, smaller than context', () => {
- element.context = 10;
- const content = [
- {ab: new Array(5)
- .fill('all work and no play make jack a dull boy')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
-
- assert.equal(groups[1].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[1].lines.length, 5);
- for (const l of groups[1].lines) {
- assert.equal(l.text, 'all work and no play make jack a dull boy');
- }
- });
- });
-
- test('at the end, larger than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(100)
- .fill('all work and no play make jill a dull girl')},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 10);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
- assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 90);
- for (const l of groups[3].lines[0].contextGroups[0].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('at the end, smaller than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(5)
- .fill('all work and no play make jill a dull girl')},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 5);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('for interleaved ab and common: true chunks', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(3)
- .fill('all work and no play make jill a dull girl')},
- {
- a: new Array(3).fill(
- 'all work and no play make jill a dull girl'),
- b: new Array(3).fill(
- ' all work and no play make jill a dull girl'),
- common: true,
- },
- {ab: new Array(3)
- .fill('all work and no play make jill a dull girl')},
- {
- a: new Array(3).fill(
- 'all work and no play make jill a dull girl'),
- b: new Array(3).fill(
- ' all work and no play make jill a dull girl'),
- common: true,
- },
- {ab: new Array(3)
- .fill('all work and no play make jill a dull girl')},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- // The first three interleaved chunks are completely shown because
- // they are part of the context (3 * 3 <= 10)
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 3);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
- assert.equal(groups[3].lines.length, 6);
- assert.equal(groups[3].adds.length, 3);
- assert.equal(groups[3].removes.length, 3);
- for (const l of groups[3].removes) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- for (const l of groups[3].adds) {
- assert.equal(
- l.text, ' all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[4].lines.length, 3);
- for (const l of groups[4].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- // The next chunk is partially shown, so it results in two groups
-
- assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
- assert.equal(groups[5].lines.length, 2);
- assert.equal(groups[5].adds.length, 1);
- assert.equal(groups[5].removes.length, 1);
- for (const l of groups[5].removes) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- for (const l of groups[5].adds) {
- assert.equal(
- l.text, ' all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.equal(groups[6].lines[0].contextGroups.length, 2);
-
- assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4);
- assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2);
- assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2);
- for (const l of groups[6].lines[0].contextGroups[0].removes) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- for (const l of groups[6].lines[0].contextGroups[0].adds) {
- assert.equal(
- l.text, ' all work and no play make jill a dull girl');
- }
-
- // The final chunk is completely hidden
- assert.equal(
- groups[6].lines[0].contextGroups[1].type,
- GrDiffGroup.Type.BOTH);
- assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3);
- for (const l of groups[6].lines[0].contextGroups[1].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('in the middle, larger than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(100)
- .fill('all work and no play make jill a dull girl')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 10);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
- assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 80);
- for (const l of groups[3].lines[0].contextGroups[0].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
-
- assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[4].lines.length, 10);
- for (const l of groups[4].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
-
- test('in the middle, smaller than context', () => {
- element.context = 10;
- const content = [
- {a: ['all work and no play make andybons a dull boy']},
- {ab: new Array(5)
- .fill('all work and no play make jill a dull girl')},
- {a: ['all work and no play make andybons a dull boy']},
- ];
-
- return element.process(content).then(() => {
- const groups = element.groups;
-
- // group[0] is the file group
- // group[1] is the "a" group
-
- assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
- assert.equal(groups[2].lines.length, 5);
- for (const l of groups[2].lines) {
- assert.equal(
- l.text, 'all work and no play make jill a dull girl');
- }
- });
- });
- });
-
- test('break up common diff chunks', () => {
- element.keyLocations = {
- left: {1: true},
- right: {10: true},
- };
-
+ test('at the beginning, smaller than context', () => {
+ element.context = 10;
const content = [
- {
- ab: [
- '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.',
- '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.',
- ],
- },
+ {ab: new Array(5)
+ .fill('all work and no play make jack a dull boy')},
+ {a: ['all work and no play make andybons a dull boy']},
];
- const result =
- element._splitCommonChunksWithKeyLocations(content);
- assert.deepEqual(result, [
- {
- ab: ['Copyright (C) 2015 The Android Open Source Project'],
- keyLocation: true,
- },
- {
- ab: [
- '',
- '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, ',
- ],
- keyLocation: false,
- },
- {
- ab: [
- 'software distributed under the License is distributed on an '],
- keyLocation: true,
- },
- {
- ab: [
- '"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.',
- ],
- keyLocation: false,
- },
- ]);
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+
+ assert.equal(groups[1].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[1].lines.length, 5);
+ for (const l of groups[1].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+ });
});
- test('breaks down shared chunks w/ whole-file', () => {
- const size = 120 * 2 + 5;
- const content = [{
- ab: _.times(size, () => `${Math.random()}`),
- }];
- element.context = -1;
- const result = element._splitLargeChunks(content);
- assert.equal(result.length, 2);
- assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
- assert.deepEqual(result[1].ab, content[0].ab.slice(120));
+ test('at the end, larger than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(100)
+ .fill('all work and no play make jill a dull girl')},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 90);
+ for (const l of groups[3].lines[0].contextGroups[0].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
});
- test('does not break-down common chunks w/ context', () => {
- const content = [{
- ab: _.times(75, () => `${Math.random()}`),
- }];
- element.context = 4;
- const result =
- element._splitCommonChunksWithKeyLocations(content);
- assert.equal(result.length, 1);
- assert.deepEqual(result[0].ab, content[0].ab);
- assert.isFalse(result[0].keyLocation);
+ test('at the end, smaller than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(5)
+ .fill('all work and no play make jill a dull girl')},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 5);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
});
- test('intraline normalization', () => {
- // The content and highlights are in the format returned by the Gerrit
- // REST API.
- let content = [
- ' <section class="summary">',
- ' <gr-linked-text content="' +
- '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
- ' </section>',
- ];
- let highlights = [
- [31, 34], [42, 26],
+ test('for interleaved ab and common: true chunks', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(3)
+ .fill('all work and no play make jill a dull girl')},
+ {
+ a: new Array(3).fill(
+ 'all work and no play make jill a dull girl'),
+ b: new Array(3).fill(
+ ' all work and no play make jill a dull girl'),
+ common: true,
+ },
+ {ab: new Array(3)
+ .fill('all work and no play make jill a dull girl')},
+ {
+ a: new Array(3).fill(
+ 'all work and no play make jill a dull girl'),
+ b: new Array(3).fill(
+ ' all work and no play make jill a dull girl'),
+ common: true,
+ },
+ {ab: new Array(3)
+ .fill('all work and no play make jill a dull girl')},
];
- let results = element._convertIntralineInfos(content,
- highlights);
- assert.deepEqual(results, [
- {
- contentIndex: 0,
- startIndex: 31,
- },
- {
- contentIndex: 1,
- startIndex: 0,
- endIndex: 33,
- },
- {
- contentIndex: 1,
- startIndex: 75,
- },
- {
- contentIndex: 2,
- startIndex: 0,
- endIndex: 6,
- },
- ]);
+ return element.process(content).then(() => {
+ const groups = element.groups;
- content = [
- ' this._path = value.path;',
- '',
- ' // When navigating away from the page, there is a ' +
- 'possibility that the',
- ' // patch number is no longer a part of the URL ' +
- '(say when navigating to',
- ' // the top-level change info view) and therefore ' +
- 'undefined in `params`.',
- ' if (!this._patchRange.patchNum) {',
- ];
- highlights = [
- [14, 17],
- [11, 70],
- [12, 67],
- [12, 67],
- [14, 29],
- ];
- results = element._convertIntralineInfos(content, highlights);
- assert.deepEqual(results, [
- {
- contentIndex: 0,
- startIndex: 14,
- endIndex: 31,
- },
- {
- contentIndex: 2,
- startIndex: 8,
- endIndex: 78,
- },
- {
- contentIndex: 3,
- startIndex: 11,
- endIndex: 78,
- },
- {
- contentIndex: 4,
- startIndex: 11,
- endIndex: 78,
- },
- {
- contentIndex: 5,
- startIndex: 12,
- endIndex: 41,
- },
- ]);
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ // The first three interleaved chunks are completely shown because
+ // they are part of the context (3 * 3 <= 10)
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 3);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[3].lines.length, 6);
+ assert.equal(groups[3].adds.length, 3);
+ assert.equal(groups[3].removes.length, 3);
+ for (const l of groups[3].removes) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[3].adds) {
+ assert.equal(
+ l.text, ' all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[4].lines.length, 3);
+ for (const l of groups[4].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ // The next chunk is partially shown, so it results in two groups
+
+ assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[5].lines.length, 2);
+ assert.equal(groups[5].adds.length, 1);
+ assert.equal(groups[5].removes.length, 1);
+ for (const l of groups[5].removes) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[5].adds) {
+ assert.equal(
+ l.text, ' all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(groups[6].lines[0].contextGroups.length, 2);
+
+ assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4);
+ assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2);
+ assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2);
+ for (const l of groups[6].lines[0].contextGroups[0].removes) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[6].lines[0].contextGroups[0].adds) {
+ assert.equal(
+ l.text, ' all work and no play make jill a dull girl');
+ }
+
+ // The final chunk is completely hidden
+ assert.equal(
+ groups[6].lines[0].contextGroups[1].type,
+ GrDiffGroup.Type.BOTH);
+ assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3);
+ for (const l of groups[6].lines[0].contextGroups[1].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
});
- test('scrolling pauses rendering', () => {
- const contentRow = {
+ test('in the middle, larger than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(100)
+ .fill('all work and no play make jill a dull girl')},
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 80);
+ for (const l of groups[3].lines[0].contextGroups[0].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[4].lines.length, 10);
+ for (const l of groups[4].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+ });
+
+ test('in the middle, smaller than context', () => {
+ element.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: new Array(5)
+ .fill('all work and no play make jill a dull girl')},
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ return element.process(content).then(() => {
+ const groups = element.groups;
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, 5);
+ for (const l of groups[2].lines) {
+ assert.equal(
+ l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+ });
+ });
+
+ test('break up common diff chunks', () => {
+ element.keyLocations = {
+ left: {1: true},
+ right: {10: true},
+ };
+
+ const content = [
+ {
ab: [
- '<!DOCTYPE html>',
- '<meta charset="utf-8">',
+ '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.',
+ '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.',
],
- };
- const content = _.times(200, _.constant(contentRow));
- sandbox.stub(element, 'async');
- element._isScrolling = true;
- element.process(content);
- // Just the files group - no more processing during scrolling.
- assert.equal(element.groups.length, 1);
-
- element._isScrolling = false;
- element.process(content);
- // More groups have been processed. How many does not matter here.
- assert.isAtLeast(element.groups.length, 2);
- });
-
- test('image diffs', () => {
- const contentRow = {
+ },
+ ];
+ const result =
+ element._splitCommonChunksWithKeyLocations(content);
+ assert.deepEqual(result, [
+ {
+ ab: ['Copyright (C) 2015 The Android Open Source Project'],
+ keyLocation: true,
+ },
+ {
ab: [
- '<!DOCTYPE html>',
- '<meta charset="utf-8">',
+ '',
+ '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, ',
],
- };
- const content = _.times(200, _.constant(contentRow));
- sandbox.stub(element, 'async');
- element.process(content, true);
- assert.equal(element.groups.length, 1);
+ keyLocation: false,
+ },
+ {
+ ab: [
+ 'software distributed under the License is distributed on an '],
+ keyLocation: true,
+ },
+ {
+ ab: [
+ '"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.',
+ ],
+ keyLocation: false,
+ },
+ ]);
+ });
- // Image diffs don't process content, just the 'FILE' line.
- assert.equal(element.groups[0].lines.length, 1);
+ test('breaks down shared chunks w/ whole-file', () => {
+ const size = 120 * 2 + 5;
+ const content = [{
+ ab: _.times(size, () => `${Math.random()}`),
+ }];
+ element.context = -1;
+ const result = element._splitLargeChunks(content);
+ assert.equal(result.length, 2);
+ assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120));
+ assert.deepEqual(result[1].ab, content[0].ab.slice(120));
+ });
+
+ test('does not break-down common chunks w/ context', () => {
+ const content = [{
+ ab: _.times(75, () => `${Math.random()}`),
+ }];
+ element.context = 4;
+ const result =
+ element._splitCommonChunksWithKeyLocations(content);
+ assert.equal(result.length, 1);
+ assert.deepEqual(result[0].ab, content[0].ab);
+ assert.isFalse(result[0].keyLocation);
+ });
+
+ test('intraline normalization', () => {
+ // The content and highlights are in the format returned by the Gerrit
+ // REST API.
+ let content = [
+ ' <section class="summary">',
+ ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
+ ' </section>',
+ ];
+ let highlights = [
+ [31, 34], [42, 26],
+ ];
+
+ let results = element._convertIntralineInfos(content,
+ highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 31,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ endIndex: 33,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 75,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 0,
+ endIndex: 6,
+ },
+ ]);
+
+ content = [
+ ' this._path = value.path;',
+ '',
+ ' // When navigating away from the page, there is a ' +
+ 'possibility that the',
+ ' // patch number is no longer a part of the URL ' +
+ '(say when navigating to',
+ ' // the top-level change info view) and therefore ' +
+ 'undefined in `params`.',
+ ' if (!this._patchRange.patchNum) {',
+ ];
+ highlights = [
+ [14, 17],
+ [11, 70],
+ [12, 67],
+ [12, 67],
+ [14, 29],
+ ];
+ results = element._convertIntralineInfos(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 14,
+ endIndex: 31,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 8,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 3,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 4,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 5,
+ startIndex: 12,
+ endIndex: 41,
+ },
+ ]);
+ });
+
+ test('scrolling pauses rendering', () => {
+ const contentRow = {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ],
+ };
+ const content = _.times(200, _.constant(contentRow));
+ sandbox.stub(element, 'async');
+ element._isScrolling = true;
+ element.process(content);
+ // Just the files group - no more processing during scrolling.
+ assert.equal(element.groups.length, 1);
+
+ element._isScrolling = false;
+ element.process(content);
+ // More groups have been processed. How many does not matter here.
+ assert.isAtLeast(element.groups.length, 2);
+ });
+
+ test('image diffs', () => {
+ const contentRow = {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ],
+ };
+ const content = _.times(200, _.constant(contentRow));
+ sandbox.stub(element, 'async');
+ element.process(content, true);
+ assert.equal(element.groups.length, 1);
+
+ // Image diffs don't process content, just the 'FILE' line.
+ assert.equal(element.groups[0].lines.length, 1);
+ });
+
+ suite('_processNext', () => {
+ let rows;
+
+ setup(() => {
+ rows = loremIpsum.split(' ');
});
- suite('_processNext', () => {
- let rows;
+ test('WHOLE_FILE', () => {
+ element.context = WHOLE_FILE;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [
+ {a: ['foo']},
+ {ab: rows},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+
+ // Results in one, uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1);
+ assert.equal(result.groups[0].type, GrDiffGroup.Type.BOTH);
+ assert.equal(result.groups[0].lines.length, rows.length);
+
+ // Line numbers are set correctly.
+ assert.equal(
+ result.groups[0].lines[0].beforeNumber,
+ state.lineNums.left + 1);
+ assert.equal(
+ result.groups[0].lines[0].afterNumber,
+ state.lineNums.right + 1);
+
+ assert.equal(result.groups[0].lines[rows.length - 1].beforeNumber,
+ state.lineNums.left + rows.length);
+ assert.equal(result.groups[0].lines[rows.length - 1].afterNumber,
+ state.lineNums.right + rows.length);
+ });
+
+ test('with context', () => {
+ element.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [
+ {a: ['foo']},
+ {ab: rows},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+ const expectedCollapseSize = rows.length - 2 * element.context;
+
+ assert.equal(result.groups.length, 3, 'Results in three groups');
+
+ // The first and last are uncollapsed context, whereas the middle has
+ // a single context-control line.
+ assert.equal(result.groups[0].lines.length, element.context);
+ assert.equal(result.groups[1].lines.length, 1);
+ assert.equal(result.groups[2].lines.length, element.context);
+
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
+ expectedCollapseSize);
+ });
+
+ test('first', () => {
+ element.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 0,
+ };
+ const chunks = [
+ {ab: rows},
+ {a: ['foo']},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+ const expectedCollapseSize = rows.length - element.context;
+
+ assert.equal(result.groups.length, 2, 'Results in two groups');
+
+ // Only the first group is collapsed.
+ assert.equal(result.groups[0].lines.length, 1);
+ assert.equal(result.groups[1].lines.length, element.context);
+
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
+ expectedCollapseSize);
+ });
+
+ test('few-rows', () => {
+ // Only ten rows.
+ rows = rows.slice(0, 10);
+ element.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 0,
+ };
+ const chunks = [
+ {ab: rows},
+ {a: ['foo']},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+
+ // Results in one uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1, 'Results in one group');
+ assert.equal(result.groups[0].lines.length, rows.length);
+ });
+
+ test('no single line collapse', () => {
+ rows = rows.slice(0, 7);
+ element.context = 3;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [
+ {a: ['foo']},
+ {ab: rows},
+ {a: ['bar']},
+ ];
+ const result = element._processNext(state, chunks);
+
+ // Results in one uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1, 'Results in one group');
+ assert.equal(result.groups[0].lines.length, rows.length);
+ });
+
+ suite('with key location', () => {
+ let state;
+ let chunks;
setup(() => {
- rows = loremIpsum.split(' ');
- });
-
- test('WHOLE_FILE', () => {
- element.context = WHOLE_FILE;
- const state = {
+ state = {
lineNums: {left: 10, right: 100},
- chunkIndex: 1,
};
- const chunks = [
- {a: ['foo']},
- {ab: rows},
- {a: ['bar']},
- ];
- const result = element._processNext(state, chunks);
-
- // Results in one, uncollapsed group with all rows.
- assert.equal(result.groups.length, 1);
- assert.equal(result.groups[0].type, GrDiffGroup.Type.BOTH);
- assert.equal(result.groups[0].lines.length, rows.length);
-
- // Line numbers are set correctly.
- assert.equal(
- result.groups[0].lines[0].beforeNumber,
- state.lineNums.left + 1);
- assert.equal(
- result.groups[0].lines[0].afterNumber,
- state.lineNums.right + 1);
-
- assert.equal(result.groups[0].lines[rows.length - 1].beforeNumber,
- state.lineNums.left + rows.length);
- assert.equal(result.groups[0].lines[rows.length - 1].afterNumber,
- state.lineNums.right + rows.length);
- });
-
- test('with context', () => {
element.context = 10;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 1,
- };
- const chunks = [
- {a: ['foo']},
+ chunks = [
{ab: rows},
- {a: ['bar']},
+ {ab: ['foo'], keyLocation: true},
+ {ab: rows},
];
- const result = element._processNext(state, chunks);
- const expectedCollapseSize = rows.length - 2 * element.context;
-
- assert.equal(result.groups.length, 3, 'Results in three groups');
-
- // The first and last are uncollapsed context, whereas the middle has
- // a single context-control line.
- assert.equal(result.groups[0].lines.length, element.context);
- assert.equal(result.groups[1].lines.length, 1);
- assert.equal(result.groups[2].lines.length, element.context);
-
- // The collapsed group has the hidden lines as its context group.
- assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
- expectedCollapseSize);
});
- test('first', () => {
- element.context = 10;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 0,
- };
- const chunks = [
- {ab: rows},
- {a: ['foo']},
- {a: ['bar']},
- ];
+ test('context before', () => {
+ state.chunkIndex = 0;
const result = element._processNext(state, chunks);
- const expectedCollapseSize = rows.length - element.context;
- assert.equal(result.groups.length, 2, 'Results in two groups');
-
- // Only the first group is collapsed.
+ // The first chunk is split into two groups:
+ // 1) A context-control, hiding everything but the context before
+ // the key location.
+ // 2) The context before the key location.
+ // The key location is not processed in this call to _processNext
+ assert.equal(result.groups.length, 2);
assert.equal(result.groups[0].lines.length, 1);
- assert.equal(result.groups[1].lines.length, element.context);
-
// The collapsed group has the hidden lines as its context group.
assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
- expectedCollapseSize);
+ rows.length - element.context);
+ assert.equal(result.groups[1].lines.length, element.context);
});
- test('few-rows', () => {
- // Only ten rows.
- rows = rows.slice(0, 10);
- element.context = 10;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 0,
- };
- const chunks = [
- {ab: rows},
- {a: ['foo']},
- {a: ['bar']},
- ];
+ test('key location itself', () => {
+ state.chunkIndex = 1;
const result = element._processNext(state, chunks);
- // Results in one uncollapsed group with all rows.
- assert.equal(result.groups.length, 1, 'Results in one group');
- assert.equal(result.groups[0].lines.length, rows.length);
+ // The second chunk results in a single group, that is just the
+ // line with the key location
+ assert.equal(result.groups.length, 1);
+ assert.equal(result.groups[0].lines.length, 1);
+ assert.equal(result.lineDelta.left, 1);
+ assert.equal(result.lineDelta.right, 1);
});
- test('no single line collapse', () => {
- rows = rows.slice(0, 7);
- element.context = 3;
- const state = {
- lineNums: {left: 10, right: 100},
- chunkIndex: 1,
- };
- const chunks = [
- {a: ['foo']},
- {ab: rows},
- {a: ['bar']},
- ];
+ test('context after', () => {
+ state.chunkIndex = 2;
const result = element._processNext(state, chunks);
- // Results in one uncollapsed group with all rows.
- assert.equal(result.groups.length, 1, 'Results in one group');
- assert.equal(result.groups[0].lines.length, rows.length);
- });
-
- suite('with key location', () => {
- let state;
- let chunks;
-
- setup(() => {
- state = {
- lineNums: {left: 10, right: 100},
- };
- element.context = 10;
- chunks = [
- {ab: rows},
- {ab: ['foo'], keyLocation: true},
- {ab: rows},
- ];
- });
-
- test('context before', () => {
- state.chunkIndex = 0;
- const result = element._processNext(state, chunks);
-
- // The first chunk is split into two groups:
- // 1) A context-control, hiding everything but the context before
- // the key location.
- // 2) The context before the key location.
- // The key location is not processed in this call to _processNext
- assert.equal(result.groups.length, 2);
- assert.equal(result.groups[0].lines.length, 1);
- // The collapsed group has the hidden lines as its context group.
- assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length,
- rows.length - element.context);
- assert.equal(result.groups[1].lines.length, element.context);
- });
-
- test('key location itself', () => {
- state.chunkIndex = 1;
- const result = element._processNext(state, chunks);
-
- // The second chunk results in a single group, that is just the
- // line with the key location
- assert.equal(result.groups.length, 1);
- assert.equal(result.groups[0].lines.length, 1);
- assert.equal(result.lineDelta.left, 1);
- assert.equal(result.lineDelta.right, 1);
- });
-
- test('context after', () => {
- state.chunkIndex = 2;
- const result = element._processNext(state, chunks);
-
- // The last chunk is split into two groups:
- // 1) The context after the key location.
- // 1) A context-control, hiding everything but the context after the
- // key location.
- assert.equal(result.groups.length, 2);
- assert.equal(result.groups[0].lines.length, element.context);
- assert.equal(result.groups[1].lines.length, 1);
- // The collapsed group has the hidden lines as its context group.
- assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
- rows.length - element.context);
- });
- });
- });
-
- suite('gr-diff-processor helpers', () => {
- let rows;
-
- setup(() => {
- rows = loremIpsum.split(' ');
- });
-
- test('_linesFromRows', () => {
- const startLineNum = 10;
- let result = element._linesFromRows(GrDiffLine.Type.ADD, rows,
- startLineNum + 1);
-
- assert.equal(result.length, rows.length);
- assert.equal(result[0].type, GrDiffLine.Type.ADD);
- assert.equal(result[0].afterNumber, startLineNum + 1);
- assert.notOk(result[0].beforeNumber);
- assert.equal(result[result.length - 1].afterNumber,
- startLineNum + rows.length);
- assert.notOk(result[result.length - 1].beforeNumber);
-
- result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows,
- startLineNum + 1);
-
- assert.equal(result.length, rows.length);
- assert.equal(result[0].type, GrDiffLine.Type.REMOVE);
- assert.equal(result[0].beforeNumber, startLineNum + 1);
- assert.notOk(result[0].afterNumber);
- assert.equal(result[result.length - 1].beforeNumber,
- startLineNum + rows.length);
- assert.notOk(result[result.length - 1].afterNumber);
- });
- });
-
- suite('_breakdown*', () => {
- test('_breakdownChunk breaks down additions', () => {
- sandbox.spy(element, '_breakdown');
- const chunk = {b: ['blah', 'blah', 'blah']};
- const result = element._breakdownChunk(chunk);
- assert.deepEqual(result, [chunk]);
- assert.isTrue(element._breakdown.called);
- });
-
- test('_breakdownChunk keeps due_to_rebase for broken down additions',
- () => {
- sandbox.spy(element, '_breakdown');
- const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true};
- const result = element._breakdownChunk(chunk);
- for (const subResult of result) {
- assert.isTrue(subResult.due_to_rebase);
- }
- });
-
- test('_breakdown common case', () => {
- const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
- .split(' ');
- const size = 3;
-
- const result = element._breakdown(array, size);
-
- for (const subResult of result) {
- assert.isAtMost(subResult.length, size);
- }
- const flattened = result
- .reduce((a, b) => a.concat(b), []);
- assert.deepEqual(flattened, array);
- });
-
- test('_breakdown smaller than size', () => {
- const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
- .split(' ');
- const size = 10;
- const expected = [array];
-
- const result = element._breakdown(array, size);
-
- assert.deepEqual(result, expected);
- });
-
- test('_breakdown empty', () => {
- const array = [];
- const size = 10;
- const expected = [];
-
- const result = element._breakdown(array, size);
-
- assert.deepEqual(result, expected);
+ // The last chunk is split into two groups:
+ // 1) The context after the key location.
+ // 1) A context-control, hiding everything but the context after the
+ // key location.
+ assert.equal(result.groups.length, 2);
+ assert.equal(result.groups[0].lines.length, element.context);
+ assert.equal(result.groups[1].lines.length, 1);
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length,
+ rows.length - element.context);
});
});
});
- test('detaching cancels', () => {
- element = fixture('basic');
- sandbox.stub(element, 'cancel');
- element.detached();
- assert(element.cancel.called);
+ suite('gr-diff-processor helpers', () => {
+ let rows;
+
+ setup(() => {
+ rows = loremIpsum.split(' ');
+ });
+
+ test('_linesFromRows', () => {
+ const startLineNum = 10;
+ let result = element._linesFromRows(GrDiffLine.Type.ADD, rows,
+ startLineNum + 1);
+
+ assert.equal(result.length, rows.length);
+ assert.equal(result[0].type, GrDiffLine.Type.ADD);
+ assert.equal(result[0].afterNumber, startLineNum + 1);
+ assert.notOk(result[0].beforeNumber);
+ assert.equal(result[result.length - 1].afterNumber,
+ startLineNum + rows.length);
+ assert.notOk(result[result.length - 1].beforeNumber);
+
+ result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows,
+ startLineNum + 1);
+
+ assert.equal(result.length, rows.length);
+ assert.equal(result[0].type, GrDiffLine.Type.REMOVE);
+ assert.equal(result[0].beforeNumber, startLineNum + 1);
+ assert.notOk(result[0].afterNumber);
+ assert.equal(result[result.length - 1].beforeNumber,
+ startLineNum + rows.length);
+ assert.notOk(result[result.length - 1].afterNumber);
+ });
+ });
+
+ suite('_breakdown*', () => {
+ test('_breakdownChunk breaks down additions', () => {
+ sandbox.spy(element, '_breakdown');
+ const chunk = {b: ['blah', 'blah', 'blah']};
+ const result = element._breakdownChunk(chunk);
+ assert.deepEqual(result, [chunk]);
+ assert.isTrue(element._breakdown.called);
+ });
+
+ test('_breakdownChunk keeps due_to_rebase for broken down additions',
+ () => {
+ sandbox.spy(element, '_breakdown');
+ const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true};
+ const result = element._breakdownChunk(chunk);
+ for (const subResult of result) {
+ assert.isTrue(subResult.due_to_rebase);
+ }
+ });
+
+ test('_breakdown common case', () => {
+ const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
+ .split(' ');
+ const size = 3;
+
+ const result = element._breakdown(array, size);
+
+ for (const subResult of result) {
+ assert.isAtMost(subResult.length, size);
+ }
+ const flattened = result
+ .reduce((a, b) => a.concat(b), []);
+ assert.deepEqual(flattened, array);
+ });
+
+ test('_breakdown smaller than size', () => {
+ const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos'
+ .split(' ');
+ const size = 10;
+ const expected = [array];
+
+ const result = element._breakdown(array, size);
+
+ assert.deepEqual(result, expected);
+ });
+
+ test('_breakdown empty', () => {
+ const array = [];
+ const size = 10;
+ const expected = [];
+
+ const result = element._breakdown(array, size);
+
+ assert.deepEqual(result, expected);
+ });
});
});
+
+ test('detaching cancels', () => {
+ element = fixture('basic');
+ sandbox.stub(element, 'cancel');
+ element.detached();
+ assert(element.cancel.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index e46f959..e59b0c2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -14,352 +14,364 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * Possible CSS classes indicating the state of selection. Dynamically added/
- * removed based on where the user clicks within the diff.
- */
- const SelectionClass = {
- COMMENT: 'selected-comment',
- LEFT: 'selected-left',
- RIGHT: 'selected-right',
- BLAME: 'selected-blame',
- };
+import '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../../scripts/util.js';
+import '../gr-diff-highlight/gr-range-normalizer.js';
+import {addListener} from '@polymer/polymer/lib/utils/gestures.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-diff-selection_html.js';
- const getNewCache = () => { return {left: null, right: null}; };
+/**
+ * Possible CSS classes indicating the state of selection. Dynamically added/
+ * removed based on where the user clicks within the diff.
+ */
+const SelectionClass = {
+ COMMENT: 'selected-comment',
+ LEFT: 'selected-left',
+ RIGHT: 'selected-right',
+ BLAME: 'selected-blame',
+};
- /**
- * @appliesMixin Gerrit.DomUtilMixin
- * @extends Polymer.Element
- */
- class GrDiffSelection extends Polymer.mixinBehaviors( [
- Gerrit.DomUtilBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-selection'; }
+const getNewCache = () => { return {left: null, right: null}; };
- static get properties() {
- return {
- diff: Object,
- /** @type {?Object} */
- _cachedDiffBuilder: Object,
- _linesCache: {
- type: Object,
- value: getNewCache(),
- },
- };
+/**
+ * @appliesMixin Gerrit.DomUtilMixin
+ * @extends Polymer.Element
+ */
+class GrDiffSelection extends mixinBehaviors( [
+ Gerrit.DomUtilBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-selection'; }
+
+ static get properties() {
+ return {
+ diff: Object,
+ /** @type {?Object} */
+ _cachedDiffBuilder: Object,
+ _linesCache: {
+ type: Object,
+ value: getNewCache(),
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_diffChanged(diff)',
+ ];
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('copy',
+ e => this._handleCopy(e));
+ addListener(this, 'down',
+ e => this._handleDown(e));
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.classList.add(SelectionClass.RIGHT);
+ }
+
+ get diffBuilder() {
+ if (!this._cachedDiffBuilder) {
+ this._cachedDiffBuilder =
+ dom(this).querySelector('gr-diff-builder');
}
+ return this._cachedDiffBuilder;
+ }
- static get observers() {
- return [
- '_diffChanged(diff)',
- ];
- }
+ _diffChanged() {
+ this._linesCache = getNewCache();
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('copy',
- e => this._handleCopy(e));
- Polymer.Gestures.addListener(this, 'down',
- e => this._handleDown(e));
- }
-
- /** @override */
- attached() {
- super.attached();
- this.classList.add(SelectionClass.RIGHT);
- }
-
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder =
- Polymer.dom(this).querySelector('gr-diff-builder');
- }
- return this._cachedDiffBuilder;
- }
-
- _diffChanged() {
- this._linesCache = getNewCache();
- }
-
- _handleDownOnRangeComment(node) {
- if (node &&
- node.nodeName &&
- node.nodeName.toLowerCase() === 'gr-comment-thread') {
- this._setClasses([
- SelectionClass.COMMENT,
- node.commentSide === 'left' ?
- SelectionClass.LEFT :
- SelectionClass.RIGHT,
- ]);
- return true;
- }
- return false;
- }
-
- _handleDown(e) {
- // Handle the down event on comment thread in Polymer 2
- const handled = this._handleDownOnRangeComment(e.target);
- if (handled) return;
-
- const lineEl = this.diffBuilder.getLineElByChild(e.target);
- const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
- if (!lineEl && !blameSelected) { return; }
-
- const targetClasses = [];
-
- if (blameSelected) {
- targetClasses.push(SelectionClass.BLAME);
- } else {
- const commentSelected =
- this._elementDescendedFromClass(e.target, 'gr-comment');
- const side = this.diffBuilder.getSideByLineEl(lineEl);
-
- targetClasses.push(side === 'left' ?
+ _handleDownOnRangeComment(node) {
+ if (node &&
+ node.nodeName &&
+ node.nodeName.toLowerCase() === 'gr-comment-thread') {
+ this._setClasses([
+ SelectionClass.COMMENT,
+ node.commentSide === 'left' ?
SelectionClass.LEFT :
- SelectionClass.RIGHT);
-
- if (commentSelected) {
- targetClasses.push(SelectionClass.COMMENT);
- }
- }
-
- this._setClasses(targetClasses);
+ SelectionClass.RIGHT,
+ ]);
+ return true;
}
+ return false;
+ }
- /**
- * Set the provided list of classes on the element, to the exclusion of all
- * other SelectionClass values.
- *
- * @param {!Array<!string>} targetClasses
- */
- _setClasses(targetClasses) {
- // Remove any selection classes that do not belong.
- for (const key in SelectionClass) {
- if (SelectionClass.hasOwnProperty(key)) {
- const className = SelectionClass[key];
- if (!targetClasses.includes(className)) {
- this.classList.remove(SelectionClass[key]);
- }
- }
- }
- // Add new selection classes iff they are not already present.
- for (const _class of targetClasses) {
- if (!this.classList.contains(_class)) {
- this.classList.add(_class);
- }
- }
- }
+ _handleDown(e) {
+ // Handle the down event on comment thread in Polymer 2
+ const handled = this._handleDownOnRangeComment(e.target);
+ if (handled) return;
- _getCopyEventTarget(e) {
- return Polymer.dom(e).rootTarget;
- }
+ const lineEl = this.diffBuilder.getLineElByChild(e.target);
+ const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
+ if (!lineEl && !blameSelected) { return; }
- /**
- * Utility function to determine whether an element is a descendant of
- * another element with the particular className.
- *
- * @param {!Element} element
- * @param {!string} className
- * @return {boolean}
- */
- _elementDescendedFromClass(element, className) {
- return this.descendedFromClass(element, className,
- this.diffBuilder.diffElement);
- }
+ const targetClasses = [];
- _handleCopy(e) {
- let commentSelected = false;
- const target = this._getCopyEventTarget(e);
- if (target.type === 'textarea') { return; }
- if (!this._elementDescendedFromClass(target, 'diff-row')) { return; }
- if (this.classList.contains(SelectionClass.COMMENT)) {
- commentSelected = true;
- }
- const lineEl = this.diffBuilder.getLineElByChild(target);
- if (!lineEl) {
- return;
- }
+ if (blameSelected) {
+ targetClasses.push(SelectionClass.BLAME);
+ } else {
+ const commentSelected =
+ this._elementDescendedFromClass(e.target, 'gr-comment');
const side = this.diffBuilder.getSideByLineEl(lineEl);
- const text = this._getSelectedText(side, commentSelected);
- if (text) {
- e.clipboardData.setData('Text', text);
- e.preventDefault();
- }
- }
- _getSelection() {
- const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
- if (!diffHosts.length) return window.getSelection();
+ targetClasses.push(side === 'left' ?
+ SelectionClass.LEFT :
+ SelectionClass.RIGHT);
- const curDiffHost = diffHosts.find(diffHost => {
- if (!diffHost || !diffHost.shadowRoot) return false;
- const selection = diffHost.shadowRoot.getSelection();
- // Pick the one with valid selection:
- // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type
- return selection && selection.type !== 'None';
- });
-
- return curDiffHost ?
- curDiffHost.shadowRoot.getSelection(): window.getSelection();
- }
-
- /**
- * Get the text of the current selection. If commentSelected is
- * true, it returns only the text of comments within the selection.
- * Otherwise it returns the text of the selected diff region.
- *
- * @param {!string} side The side that is selected.
- * @param {boolean} commentSelected Whether or not a comment is selected.
- * @return {string} The selected text.
- */
- _getSelectedText(side, commentSelected) {
- const sel = this._getSelection();
- if (sel.rangeCount != 1) {
- return ''; // No multi-select support yet.
- }
if (commentSelected) {
- return this._getCommentLines(sel, side);
+ targetClasses.push(SelectionClass.COMMENT);
}
- const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
- const startLineEl =
- this.diffBuilder.getLineElByChild(range.startContainer);
- const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
- // Happens when triple click in side-by-side mode with other side empty.
- const endsAtOtherEmptySide = !endLineEl &&
- range.endOffset === 0 &&
- range.endContainer.nodeName === 'TD' &&
- (range.endContainer.classList.contains('left') ||
- range.endContainer.classList.contains('right'));
- const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
- let endLineNum;
- if (endsAtOtherEmptySide) {
- endLineNum = startLineNum + 1;
- } else if (endLineEl) {
- endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
- }
-
- return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
- range.endOffset, side);
}
- /**
- * Query the diff object for the selected lines.
- *
- * @param {number} startLineNum
- * @param {number} startOffset
- * @param {number|undefined} endLineNum Use undefined to get the range
- * extending to the end of the file.
- * @param {number} endOffset
- * @param {!string} side The side that is currently selected.
- * @return {string} The selected diff text.
- */
- _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) {
- const lines =
- this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
- if (lines.length) {
- lines[lines.length - 1] = lines[lines.length - 1]
- .substring(0, endOffset);
- lines[0] = lines[0].substring(startOffset);
+ this._setClasses(targetClasses);
+ }
+
+ /**
+ * Set the provided list of classes on the element, to the exclusion of all
+ * other SelectionClass values.
+ *
+ * @param {!Array<!string>} targetClasses
+ */
+ _setClasses(targetClasses) {
+ // Remove any selection classes that do not belong.
+ for (const key in SelectionClass) {
+ if (SelectionClass.hasOwnProperty(key)) {
+ const className = SelectionClass[key];
+ if (!targetClasses.includes(className)) {
+ this.classList.remove(SelectionClass[key]);
+ }
}
- return lines.join('\n');
}
-
- /**
- * Query the diff object for the lines from a particular side.
- *
- * @param {!string} side The side that is currently selected.
- * @return {!Array<string>} An array of strings indexed by line number.
- */
- _getDiffLines(side) {
- if (this._linesCache[side]) {
- return this._linesCache[side];
+ // Add new selection classes iff they are not already present.
+ for (const _class of targetClasses) {
+ if (!this.classList.contains(_class)) {
+ this.classList.add(_class);
}
- let lines = [];
- const key = side === 'left' ? 'a' : 'b';
- for (const chunk of this.diff.content) {
- if (chunk.ab) {
- lines = lines.concat(chunk.ab);
- } else if (chunk[key]) {
- lines = lines.concat(chunk[key]);
- }
- }
- this._linesCache[side] = lines;
- return lines;
- }
-
- /**
- * Query the diffElement for comments and check whether they lie inside the
- * selection range.
- *
- * @param {!Selection} sel The selection of the window.
- * @param {!string} side The side that is currently selected.
- * @return {string} The selected comment text.
- */
- _getCommentLines(sel, side) {
- const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
- const content = [];
- // Query the diffElement for comments.
- const messages = this.diffBuilder.diffElement.querySelectorAll(
- `.side-by-side [data-side="${side
- }"] .message *, .unified .message *`);
-
- for (let i = 0; i < messages.length; i++) {
- const el = messages[i];
- // Check if the comment element exists inside the selection.
- if (sel.containsNode(el, true)) {
- // Padded elements require newlines for accurate spacing.
- if (el.parentElement.id === 'container' ||
- el.parentElement.nodeName === 'BLOCKQUOTE') {
- if (content.length && content[content.length - 1] !== '') {
- content.push('');
- }
- }
-
- if (el.id === 'output' &&
- !this._elementDescendedFromClass(el, 'collapsed')) {
- content.push(this._getTextContentForRange(el, sel, range));
- }
- }
- }
-
- return content.join('\n');
- }
-
- /**
- * Given a DOM node, a selection, and a selection range, recursively get all
- * of the text content within that selection.
- * Using a domNode that isn't in the selection returns an empty string.
- *
- * @param {!Node} domNode The root DOM node.
- * @param {!Selection} sel The selection.
- * @param {!Range} range The normalized selection range.
- * @return {string} The text within the selection.
- */
- _getTextContentForRange(domNode, sel, range) {
- if (!sel.containsNode(domNode, true)) { return ''; }
-
- let text = '';
- if (domNode instanceof Text) {
- text = domNode.textContent;
- if (domNode === range.endContainer) {
- text = text.substring(0, range.endOffset);
- }
- if (domNode === range.startContainer) {
- text = text.substring(range.startOffset);
- }
- } else {
- for (const childNode of domNode.childNodes) {
- text += this._getTextContentForRange(childNode, sel, range);
- }
- }
- return text;
}
}
- customElements.define(GrDiffSelection.is, GrDiffSelection);
-})();
+ _getCopyEventTarget(e) {
+ return dom(e).rootTarget;
+ }
+
+ /**
+ * Utility function to determine whether an element is a descendant of
+ * another element with the particular className.
+ *
+ * @param {!Element} element
+ * @param {!string} className
+ * @return {boolean}
+ */
+ _elementDescendedFromClass(element, className) {
+ return this.descendedFromClass(element, className,
+ this.diffBuilder.diffElement);
+ }
+
+ _handleCopy(e) {
+ let commentSelected = false;
+ const target = this._getCopyEventTarget(e);
+ if (target.type === 'textarea') { return; }
+ if (!this._elementDescendedFromClass(target, 'diff-row')) { return; }
+ if (this.classList.contains(SelectionClass.COMMENT)) {
+ commentSelected = true;
+ }
+ const lineEl = this.diffBuilder.getLineElByChild(target);
+ if (!lineEl) {
+ return;
+ }
+ const side = this.diffBuilder.getSideByLineEl(lineEl);
+ const text = this._getSelectedText(side, commentSelected);
+ if (text) {
+ e.clipboardData.setData('Text', text);
+ e.preventDefault();
+ }
+ }
+
+ _getSelection() {
+ const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
+ if (!diffHosts.length) return window.getSelection();
+
+ const curDiffHost = diffHosts.find(diffHost => {
+ if (!diffHost || !diffHost.shadowRoot) return false;
+ const selection = diffHost.shadowRoot.getSelection();
+ // Pick the one with valid selection:
+ // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type
+ return selection && selection.type !== 'None';
+ });
+
+ return curDiffHost ?
+ curDiffHost.shadowRoot.getSelection(): window.getSelection();
+ }
+
+ /**
+ * Get the text of the current selection. If commentSelected is
+ * true, it returns only the text of comments within the selection.
+ * Otherwise it returns the text of the selected diff region.
+ *
+ * @param {!string} side The side that is selected.
+ * @param {boolean} commentSelected Whether or not a comment is selected.
+ * @return {string} The selected text.
+ */
+ _getSelectedText(side, commentSelected) {
+ const sel = this._getSelection();
+ if (sel.rangeCount != 1) {
+ return ''; // No multi-select support yet.
+ }
+ if (commentSelected) {
+ return this._getCommentLines(sel, side);
+ }
+ const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
+ const startLineEl =
+ this.diffBuilder.getLineElByChild(range.startContainer);
+ const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
+ // Happens when triple click in side-by-side mode with other side empty.
+ const endsAtOtherEmptySide = !endLineEl &&
+ range.endOffset === 0 &&
+ range.endContainer.nodeName === 'TD' &&
+ (range.endContainer.classList.contains('left') ||
+ range.endContainer.classList.contains('right'));
+ const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
+ let endLineNum;
+ if (endsAtOtherEmptySide) {
+ endLineNum = startLineNum + 1;
+ } else if (endLineEl) {
+ endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
+ }
+
+ return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
+ range.endOffset, side);
+ }
+
+ /**
+ * Query the diff object for the selected lines.
+ *
+ * @param {number} startLineNum
+ * @param {number} startOffset
+ * @param {number|undefined} endLineNum Use undefined to get the range
+ * extending to the end of the file.
+ * @param {number} endOffset
+ * @param {!string} side The side that is currently selected.
+ * @return {string} The selected diff text.
+ */
+ _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) {
+ const lines =
+ this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
+ if (lines.length) {
+ lines[lines.length - 1] = lines[lines.length - 1]
+ .substring(0, endOffset);
+ lines[0] = lines[0].substring(startOffset);
+ }
+ return lines.join('\n');
+ }
+
+ /**
+ * Query the diff object for the lines from a particular side.
+ *
+ * @param {!string} side The side that is currently selected.
+ * @return {!Array<string>} An array of strings indexed by line number.
+ */
+ _getDiffLines(side) {
+ if (this._linesCache[side]) {
+ return this._linesCache[side];
+ }
+ let lines = [];
+ const key = side === 'left' ? 'a' : 'b';
+ for (const chunk of this.diff.content) {
+ if (chunk.ab) {
+ lines = lines.concat(chunk.ab);
+ } else if (chunk[key]) {
+ lines = lines.concat(chunk[key]);
+ }
+ }
+ this._linesCache[side] = lines;
+ return lines;
+ }
+
+ /**
+ * Query the diffElement for comments and check whether they lie inside the
+ * selection range.
+ *
+ * @param {!Selection} sel The selection of the window.
+ * @param {!string} side The side that is currently selected.
+ * @return {string} The selected comment text.
+ */
+ _getCommentLines(sel, side) {
+ const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
+ const content = [];
+ // Query the diffElement for comments.
+ const messages = this.diffBuilder.diffElement.querySelectorAll(
+ `.side-by-side [data-side="${side
+ }"] .message *, .unified .message *`);
+
+ for (let i = 0; i < messages.length; i++) {
+ const el = messages[i];
+ // Check if the comment element exists inside the selection.
+ if (sel.containsNode(el, true)) {
+ // Padded elements require newlines for accurate spacing.
+ if (el.parentElement.id === 'container' ||
+ el.parentElement.nodeName === 'BLOCKQUOTE') {
+ if (content.length && content[content.length - 1] !== '') {
+ content.push('');
+ }
+ }
+
+ if (el.id === 'output' &&
+ !this._elementDescendedFromClass(el, 'collapsed')) {
+ content.push(this._getTextContentForRange(el, sel, range));
+ }
+ }
+ }
+
+ return content.join('\n');
+ }
+
+ /**
+ * Given a DOM node, a selection, and a selection range, recursively get all
+ * of the text content within that selection.
+ * Using a domNode that isn't in the selection returns an empty string.
+ *
+ * @param {!Node} domNode The root DOM node.
+ * @param {!Selection} sel The selection.
+ * @param {!Range} range The normalized selection range.
+ * @return {string} The text within the selection.
+ */
+ _getTextContentForRange(domNode, sel, range) {
+ if (!sel.containsNode(domNode, true)) { return ''; }
+
+ let text = '';
+ if (domNode instanceof Text) {
+ text = domNode.textContent;
+ if (domNode === range.endContainer) {
+ text = text.substring(0, range.endOffset);
+ }
+ if (domNode === range.startContainer) {
+ text = text.substring(range.startOffset);
+ }
+ } else {
+ for (const childNode of domNode.childNodes) {
+ text += this._getTextContentForRange(childNode, sel, range);
+ }
+ }
+ return text;
+ }
+}
+
+customElements.define(GrDiffSelection.is, GrDiffSelection);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
index 016305c..ce6008e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
@@ -1,30 +1,23 @@
-<!--
-@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/dom-util-behavior/dom-util-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<script src="../../../scripts/util.js"></script>
-<script src="../gr-diff-highlight/gr-range-normalizer.js"></script>
-
-<dom-module id="gr-diff-selection">
- <template>
+export const htmlTemplate = html`
<div class="contentWrapper">
<slot></slot>
</div>
- </template>
- <script src="gr-diff-selection.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index a8e85e2..ac3a87f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_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-diff-selection</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-diff-selection.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-diff-selection.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-selection.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -107,298 +112,300 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-selection', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-selection.js';
+suite('gr-diff-selection', () => {
+ let element;
+ let sandbox;
- const emulateCopyOn = function(target) {
- const fakeEvent = {
- target,
- preventDefault: sandbox.stub(),
- clipboardData: {
- setData: sandbox.stub(),
+ const emulateCopyOn = function(target) {
+ const fakeEvent = {
+ target,
+ preventDefault: sandbox.stub(),
+ clipboardData: {
+ setData: sandbox.stub(),
+ },
+ };
+ element._getCopyEventTarget.returns(target);
+ element._handleCopy(fakeEvent);
+ return fakeEvent;
+ };
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(element, '_getCopyEventTarget');
+ element._cachedDiffBuilder = {
+ getLineElByChild: sandbox.stub().returns({}),
+ getSideByLineEl: sandbox.stub(),
+ diffElement: element.querySelector('#diffTable'),
+ };
+ element.diff = {
+ content: [
+ {
+ a: ['ba ba'],
+ b: ['some other text'],
},
- };
- element._getCopyEventTarget.returns(target);
- element._handleCopy(fakeEvent);
- return fakeEvent;
+ {
+ a: ['zin'],
+ b: ['more more more'],
+ },
+ {
+ a: ['ga ga'],
+ b: ['some other text'],
+ },
+ ],
+ };
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('applies selected-left on left side click', () => {
+ element.classList.add('selected-right');
+ element._cachedDiffBuilder.getSideByLineEl.returns('left');
+ MockInteractions.down(element);
+ assert.isTrue(
+ element.classList.contains('selected-left'), 'adds selected-left');
+ assert.isFalse(
+ element.classList.contains('selected-right'),
+ 'removes selected-right');
+ });
+
+ test('applies selected-right on right side click', () => {
+ element.classList.add('selected-left');
+ element._cachedDiffBuilder.getSideByLineEl.returns('right');
+ MockInteractions.down(element);
+ assert.isTrue(
+ element.classList.contains('selected-right'), 'adds selected-right');
+ assert.isFalse(
+ element.classList.contains('selected-left'), 'removes selected-left');
+ });
+
+ test('applies selected-blame on blame click', () => {
+ element.classList.add('selected-left');
+ element.diffBuilder.getLineElByChild.returns(null);
+ sandbox.stub(element, '_elementDescendedFromClass',
+ (el, className) => className === 'blame');
+ MockInteractions.down(element);
+ assert.isTrue(
+ element.classList.contains('selected-blame'), 'adds selected-right');
+ assert.isFalse(
+ element.classList.contains('selected-left'), 'removes selected-left');
+ });
+
+ test('ignores copy for non-content Element', () => {
+ sandbox.stub(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('.not-diff-row'));
+ assert.isFalse(element._getSelectedText.called);
+ });
+
+ test('asks for text for left side Elements', () => {
+ element._cachedDiffBuilder.getSideByLineEl.returns('left');
+ sandbox.stub(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('div.contentText'));
+ assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
+ });
+
+ test('reacts to copy for content Elements', () => {
+ sandbox.stub(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('div.contentText'));
+ assert.isTrue(element._getSelectedText.called);
+ });
+
+ test('copy event is prevented for content Elements', () => {
+ sandbox.stub(element, '_getSelectedText');
+ element._cachedDiffBuilder.getSideByLineEl.returns('left');
+ element._getSelectedText.returns('test');
+ const event = emulateCopyOn(element.querySelector('div.contentText'));
+ assert.isTrue(event.preventDefault.called);
+ });
+
+ test('inserts text into clipboard on copy', () => {
+ sandbox.stub(element, '_getSelectedText').returns('the text');
+ const event = emulateCopyOn(element.querySelector('div.contentText'));
+ assert.deepEqual(
+ ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
+ });
+
+ test('_setClasses adds given SelectionClass values, removes others', () => {
+ element.classList.add('selected-right');
+ element._setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(element.classList.contains('selected-comment'));
+ assert.isTrue(element.classList.contains('selected-left'));
+ assert.isFalse(element.classList.contains('selected-right'));
+ assert.isFalse(element.classList.contains('selected-blame'));
+
+ element._setClasses(['selected-blame']);
+ assert.isFalse(element.classList.contains('selected-comment'));
+ assert.isFalse(element.classList.contains('selected-left'));
+ assert.isFalse(element.classList.contains('selected-right'));
+ assert.isTrue(element.classList.contains('selected-blame'));
+ });
+
+ test('_setClasses removes before it ads', () => {
+ element.classList.add('selected-right');
+ const addStub = sandbox.stub(element.classList, 'add');
+ const removeStub = sandbox.stub(element.classList, 'remove', () => {
+ assert.isFalse(addStub.called);
+ });
+ element._setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(addStub.called);
+ assert.isTrue(removeStub.called);
+ });
+
+ test('copies content correctly', () => {
+ // Fetch the line number.
+ element._cachedDiffBuilder.getLineElByChild = function(child) {
+ while (!child.classList.contains('content') && child.parentElement) {
+ child = child.parentElement;
+ }
+ return child.previousElementSibling;
};
+ element.classList.add('selected-left');
+ element.classList.remove('selected-right');
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(element.querySelector('div.contentText').firstChild, 3);
+ range.setEnd(
+ element.querySelectorAll('div.contentText')[4].firstChild, 2);
+ selection.addRange(range);
+ assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
+ });
+
+ test('copies comments', () => {
+ element.classList.add('selected-left');
+ element.classList.add('selected-comment');
+ element.classList.remove('selected-right');
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(
+ element.querySelector('.gr-formatted-text *').firstChild, 3);
+ range.setEnd(
+ element.querySelectorAll('.gr-formatted-text *')[2].childNodes[2], 7);
+ selection.addRange(range);
+ assert.equal('s is a comment\nThis is a differ',
+ element._getSelectedText('left', true));
+ });
+
+ test('respects astral chars in comments', () => {
+ element.classList.add('selected-left');
+ element.classList.add('selected-comment');
+ element.classList.remove('selected-right');
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ const nodes = element.querySelectorAll('.gr-formatted-text *');
+ range.setStart(nodes[2].childNodes[2], 13);
+ range.setEnd(nodes[2].childNodes[2], 23);
+ selection.addRange(range);
+ assert.equal('mment 💩 u',
+ element._getSelectedText('left', true));
+ });
+
+ test('defers to default behavior for textarea', () => {
+ element.classList.add('selected-left');
+ element.classList.remove('selected-right');
+ const selectedTextSpy = sandbox.spy(element, '_getSelectedText');
+ emulateCopyOn(element.querySelector('textarea'));
+ assert.isFalse(selectedTextSpy.called);
+ });
+
+ test('regression test for 4794', () => {
+ element._cachedDiffBuilder.getLineElByChild = function(child) {
+ while (!child.classList.contains('content') && child.parentElement) {
+ child = child.parentElement;
+ }
+ return child.previousElementSibling;
+ };
+
+ element.classList.add('selected-right');
+ element.classList.remove('selected-left');
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(
+ element.querySelectorAll('div.contentText')[1].firstChild, 4);
+ range.setEnd(
+ element.querySelectorAll('div.contentText')[1].firstChild, 10);
+ selection.addRange(range);
+ assert.equal(element._getSelectedText('right'), ' other');
+ });
+
+ test('copies to end of side (issue 7895)', () => {
+ element._cachedDiffBuilder.getLineElByChild = function(child) {
+ // Return null for the end container.
+ if (child.textContent === 'ga ga') { return null; }
+ while (!child.classList.contains('content') && child.parentElement) {
+ child = child.parentElement;
+ }
+ return child.previousElementSibling;
+ };
+ element.classList.add('selected-left');
+ element.classList.remove('selected-right');
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(element.querySelector('div.contentText').firstChild, 3);
+ range.setEnd(
+ element.querySelectorAll('div.contentText')[4].firstChild, 2);
+ selection.addRange(range);
+ assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
+ });
+
+ suite('_getTextContentForRange', () => {
+ let selection;
+ let range;
+ let nodes;
+
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(element, '_getCopyEventTarget');
- element._cachedDiffBuilder = {
- getLineElByChild: sandbox.stub().returns({}),
- getSideByLineEl: sandbox.stub(),
- diffElement: element.querySelector('#diffTable'),
- };
- element.diff = {
- content: [
- {
- a: ['ba ba'],
- b: ['some other text'],
- },
- {
- a: ['zin'],
- b: ['more more more'],
- },
- {
- a: ['ga ga'],
- b: ['some other text'],
- },
- ],
- };
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('applies selected-left on left side click', () => {
- element.classList.add('selected-right');
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- MockInteractions.down(element);
- assert.isTrue(
- element.classList.contains('selected-left'), 'adds selected-left');
- assert.isFalse(
- element.classList.contains('selected-right'),
- 'removes selected-right');
- });
-
- test('applies selected-right on right side click', () => {
- element.classList.add('selected-left');
- element._cachedDiffBuilder.getSideByLineEl.returns('right');
- MockInteractions.down(element);
- assert.isTrue(
- element.classList.contains('selected-right'), 'adds selected-right');
- assert.isFalse(
- element.classList.contains('selected-left'), 'removes selected-left');
- });
-
- test('applies selected-blame on blame click', () => {
- element.classList.add('selected-left');
- element.diffBuilder.getLineElByChild.returns(null);
- sandbox.stub(element, '_elementDescendedFromClass',
- (el, className) => className === 'blame');
- MockInteractions.down(element);
- assert.isTrue(
- element.classList.contains('selected-blame'), 'adds selected-right');
- assert.isFalse(
- element.classList.contains('selected-left'), 'removes selected-left');
- });
-
- test('ignores copy for non-content Element', () => {
- sandbox.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('.not-diff-row'));
- assert.isFalse(element._getSelectedText.called);
- });
-
- test('asks for text for left side Elements', () => {
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- sandbox.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
- assert.deepEqual(['left', false], element._getSelectedText.lastCall.args);
- });
-
- test('reacts to copy for content Elements', () => {
- sandbox.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
- assert.isTrue(element._getSelectedText.called);
- });
-
- test('copy event is prevented for content Elements', () => {
- sandbox.stub(element, '_getSelectedText');
- element._cachedDiffBuilder.getSideByLineEl.returns('left');
- element._getSelectedText.returns('test');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
- assert.isTrue(event.preventDefault.called);
- });
-
- test('inserts text into clipboard on copy', () => {
- sandbox.stub(element, '_getSelectedText').returns('the text');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
- assert.deepEqual(
- ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
- });
-
- test('_setClasses adds given SelectionClass values, removes others', () => {
- element.classList.add('selected-right');
- element._setClasses(['selected-comment', 'selected-left']);
- assert.isTrue(element.classList.contains('selected-comment'));
- assert.isTrue(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isFalse(element.classList.contains('selected-blame'));
-
- element._setClasses(['selected-blame']);
- assert.isFalse(element.classList.contains('selected-comment'));
- assert.isFalse(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isTrue(element.classList.contains('selected-blame'));
- });
-
- test('_setClasses removes before it ads', () => {
- element.classList.add('selected-right');
- const addStub = sandbox.stub(element.classList, 'add');
- const removeStub = sandbox.stub(element.classList, 'remove', () => {
- assert.isFalse(addStub.called);
- });
- element._setClasses(['selected-comment', 'selected-left']);
- assert.isTrue(addStub.called);
- assert.isTrue(removeStub.called);
- });
-
- test('copies content correctly', () => {
- // Fetch the line number.
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
-
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(element.querySelector('div.contentText').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('div.contentText')[4].firstChild, 2);
- selection.addRange(range);
- assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
- });
-
- test('copies comments', () => {
element.classList.add('selected-left');
element.classList.add('selected-comment');
element.classList.remove('selected-right');
- const selection = window.getSelection();
+ selection = window.getSelection();
selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(
- element.querySelector('.gr-formatted-text *').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('.gr-formatted-text *')[2].childNodes[2], 7);
+ range = document.createRange();
+ nodes = element.querySelectorAll('.gr-formatted-text *');
+ });
+
+ test('multi level element contained in range', () => {
+ range.setStart(nodes[2].childNodes[0], 1);
+ range.setEnd(nodes[2].childNodes[2], 7);
selection.addRange(range);
- assert.equal('s is a comment\nThis is a differ',
- element._getSelectedText('left', true));
+ assert.equal(element._getTextContentForRange(element, selection, range),
+ 'his is a differ');
});
- test('respects astral chars in comments', () => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- const nodes = element.querySelectorAll('.gr-formatted-text *');
- range.setStart(nodes[2].childNodes[2], 13);
- range.setEnd(nodes[2].childNodes[2], 23);
+ test('multi level element as startContainer of range', () => {
+ range.setStart(nodes[2].childNodes[1], 0);
+ range.setEnd(nodes[2].childNodes[2], 7);
selection.addRange(range);
- assert.equal('mment 💩 u',
- element._getSelectedText('left', true));
+ assert.equal(element._getTextContentForRange(element, selection, range),
+ 'a differ');
});
- test('defers to default behavior for textarea', () => {
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
- const selectedTextSpy = sandbox.spy(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('textarea'));
- assert.isFalse(selectedTextSpy.called);
- });
-
- test('regression test for 4794', () => {
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
-
- element.classList.add('selected-right');
- element.classList.remove('selected-left');
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(
- element.querySelectorAll('div.contentText')[1].firstChild, 4);
- range.setEnd(
- element.querySelectorAll('div.contentText')[1].firstChild, 10);
+ test('startContainer === endContainer', () => {
+ range.setStart(nodes[0].firstChild, 2);
+ range.setEnd(nodes[0].firstChild, 12);
selection.addRange(range);
- assert.equal(element._getSelectedText('right'), ' other');
- });
-
- test('copies to end of side (issue 7895)', () => {
- element._cachedDiffBuilder.getLineElByChild = function(child) {
- // Return null for the end container.
- if (child.textContent === 'ga ga') { return null; }
- while (!child.classList.contains('content') && child.parentElement) {
- child = child.parentElement;
- }
- return child.previousElementSibling;
- };
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
- const selection = window.getSelection();
- selection.removeAllRanges();
- const range = document.createRange();
- range.setStart(element.querySelector('div.contentText').firstChild, 3);
- range.setEnd(
- element.querySelectorAll('div.contentText')[4].firstChild, 2);
- selection.addRange(range);
- assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
- });
-
- suite('_getTextContentForRange', () => {
- let selection;
- let range;
- let nodes;
-
- setup(() => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
- selection = window.getSelection();
- selection.removeAllRanges();
- range = document.createRange();
- nodes = element.querySelectorAll('.gr-formatted-text *');
- });
-
- test('multi level element contained in range', () => {
- range.setStart(nodes[2].childNodes[0], 1);
- range.setEnd(nodes[2].childNodes[2], 7);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'his is a differ');
- });
-
- test('multi level element as startContainer of range', () => {
- range.setStart(nodes[2].childNodes[1], 0);
- range.setEnd(nodes[2].childNodes[2], 7);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'a differ');
- });
-
- test('startContainer === endContainer', () => {
- range.setStart(nodes[0].firstChild, 2);
- range.setEnd(nodes[0].firstChild, 12);
- selection.addRange(range);
- assert.equal(element._getTextContentForRange(element, selection, range),
- 'is is a co');
- });
- });
-
- test('cache is reset when diff changes', () => {
- element._linesCache = {left: 'test', right: 'test'};
- element.diff = {};
- flushAsynchronousOperations();
- assert.deepEqual(element._linesCache, {left: null, right: null});
+ assert.equal(element._getTextContentForRange(element, selection, range),
+ 'is is a co');
});
});
+
+ test('cache is reset when diff changes', () => {
+ element._linesCache = {left: 'test', right: 'test'};
+ element.diff = {};
+ flushAsynchronousOperations();
+ assert.deepEqual(element._linesCache, {left: null, right: null});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index eb5ea017..cf29a03 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.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,1225 +14,1258 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
- const MSG_LOADING_BLAME = 'Loading blame...';
- const MSG_LOADED_BLAME = 'Blame loaded';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
+import '../../shared/gr-fixed-panel/gr-fixed-panel.js';
+import '../../shared/gr-icons/gr-icons.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../../shared/revision-info/revision-info.js';
+import '../gr-comment-api/gr-comment-api.js';
+import '../gr-diff-cursor/gr-diff-cursor.js';
+import '../gr-apply-fix-dialog/gr-apply-fix-dialog.js';
+import '../gr-diff-host/gr-diff-host.js';
+import '../gr-diff-mode-selector/gr-diff-mode-selector.js';
+import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
+import '../gr-patch-range-select/gr-patch-range-select.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-diff-view_html.js';
- const PARENT = 'PARENT';
+const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
+const MSG_LOADING_BLAME = 'Loading blame...';
+const MSG_LOADED_BLAME = 'Blame loaded';
- const DiffSides = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+const PARENT = 'PARENT';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const DiffSides = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDiffView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when user tries to navigate away while comments are pending save.
+ *
+ * @event show-alert
*/
- class GrDiffView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.PathListBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff-view'; }
- /**
- * Fired when the title of the page should change.
- *
- * @event title-change
- */
+ static get properties() {
+ return {
/**
- * Fired when user tries to navigate away while comments are pending save.
- *
- * @event show-alert
+ * URL params passed from the router.
*/
-
- static get properties() {
- return {
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
/**
- * URL params passed from the router.
+ * @type {{ diffMode: (string|undefined) }}
*/
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- /**
- * @type {{ diffMode: (string|undefined) }}
- */
- changeViewState: {
- type: Object,
- notify: true,
- value() { return {}; },
- observer: '_changeViewStateChanged',
- },
- disableDiffPrefs: {
- type: Boolean,
- value: false,
- },
- _diffPrefsDisabled: {
- type: Boolean,
- computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
- },
- /** @type {?} */
- _patchRange: Object,
- /** @type {?} */
- _commitRange: Object,
- /**
- * @type {{
- * subject: string,
- * project: string,
- * revisions: string,
- * }}
- */
- _change: Object,
- /** @type {?} */
- _changeComments: Object,
- _changeNum: String,
- /**
- * This is a DiffInfo object.
- * This is retrieved and owned by a child component.
- */
- _diff: Object,
- // An array specifically formatted to be used in a gr-dropdown-list
- // element for selected a file to view.
- _formattedFiles: {
- type: Array,
- computed: '_formatFilesForDropdown(_files, ' +
- '_patchRange.patchNum, _changeComments)',
- },
- // An sorted array of files, as returned by the rest API.
- _fileList: {
- type: Array,
- computed: '_getSortedFileList(_files)',
- },
- /**
- * Contains information about files as returned by the rest API.
- *
- * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }}
- */
- _files: {
- type: Object,
- value() { return {sortedFileList: [], changeFilesByPath: {}}; },
- },
+ changeViewState: {
+ type: Object,
+ notify: true,
+ value() { return {}; },
+ observer: '_changeViewStateChanged',
+ },
+ disableDiffPrefs: {
+ type: Boolean,
+ value: false,
+ },
+ _diffPrefsDisabled: {
+ type: Boolean,
+ computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)',
+ },
+ /** @type {?} */
+ _patchRange: Object,
+ /** @type {?} */
+ _commitRange: Object,
+ /**
+ * @type {{
+ * subject: string,
+ * project: string,
+ * revisions: string,
+ * }}
+ */
+ _change: Object,
+ /** @type {?} */
+ _changeComments: Object,
+ _changeNum: String,
+ /**
+ * This is a DiffInfo object.
+ * This is retrieved and owned by a child component.
+ */
+ _diff: Object,
+ // An array specifically formatted to be used in a gr-dropdown-list
+ // element for selected a file to view.
+ _formattedFiles: {
+ type: Array,
+ computed: '_formatFilesForDropdown(_files, ' +
+ '_patchRange.patchNum, _changeComments)',
+ },
+ // An sorted array of files, as returned by the rest API.
+ _fileList: {
+ type: Array,
+ computed: '_getSortedFileList(_files)',
+ },
+ /**
+ * Contains information about files as returned by the rest API.
+ *
+ * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }}
+ */
+ _files: {
+ type: Object,
+ value() { return {sortedFileList: [], changeFilesByPath: {}}; },
+ },
- _path: {
- type: String,
- observer: '_pathChanged',
- },
- _fileNum: {
- type: Number,
- computed: '_computeFileNum(_path, _formattedFiles)',
- },
- _loggedIn: {
- type: Boolean,
- value: false,
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _prefs: Object,
- _localPrefs: Object,
- _projectConfig: Object,
- _userPrefs: Object,
- _diffMode: {
- type: String,
- computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
- },
- _isImageDiff: Boolean,
- _filesWeblinks: Object,
+ _path: {
+ type: String,
+ observer: '_pathChanged',
+ },
+ _fileNum: {
+ type: Number,
+ computed: '_computeFileNum(_path, _formattedFiles)',
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _prefs: Object,
+ _localPrefs: Object,
+ _projectConfig: Object,
+ _userPrefs: Object,
+ _diffMode: {
+ type: String,
+ computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
+ },
+ _isImageDiff: Boolean,
+ _filesWeblinks: Object,
- /**
- * Map of paths in the current change and patch range that have comments
- * or drafts or robot comments.
- */
- _commentMap: Object,
+ /**
+ * Map of paths in the current change and patch range that have comments
+ * or drafts or robot comments.
+ */
+ _commentMap: Object,
- _commentsForDiff: Object,
+ _commentsForDiff: Object,
- /**
- * Object to contain the path of the next and previous file in the current
- * change and patch range that has comments.
- */
- _commentSkips: {
- type: Object,
- computed: '_computeCommentSkips(_commentMap, _fileList, _path)',
- },
- _panelFloatingDisabled: {
- type: Boolean,
- value: () => window.PANEL_FLOATING_DISABLED,
- },
- _editMode: {
- type: Boolean,
- computed: '_computeEditMode(_patchRange.*)',
- },
- _isBlameLoaded: Boolean,
- _isBlameLoading: {
- type: Boolean,
- value: false,
- },
- _allPatchSets: {
- type: Array,
- computed: 'computeAllPatchSets(_change, _change.revisions.*)',
- },
- _revisionInfo: {
- type: Object,
- computed: '_getRevisionInfo(_change)',
- },
- _reviewedFiles: {
- type: Object,
- value: () => new Set(),
- },
+ /**
+ * Object to contain the path of the next and previous file in the current
+ * change and patch range that has comments.
+ */
+ _commentSkips: {
+ type: Object,
+ computed: '_computeCommentSkips(_commentMap, _fileList, _path)',
+ },
+ _panelFloatingDisabled: {
+ type: Boolean,
+ value: () => window.PANEL_FLOATING_DISABLED,
+ },
+ _editMode: {
+ type: Boolean,
+ computed: '_computeEditMode(_patchRange.*)',
+ },
+ _isBlameLoaded: Boolean,
+ _isBlameLoading: {
+ type: Boolean,
+ value: false,
+ },
+ _allPatchSets: {
+ type: Array,
+ computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+ },
+ _revisionInfo: {
+ type: Object,
+ computed: '_getRevisionInfo(_change)',
+ },
+ _reviewedFiles: {
+ type: Object,
+ value: () => new Set(),
+ },
- /**
- * gr-diff-view has gr-fixed-panel on top. The panel can
- * intersect a main element and partially hides a content of
- * the main element. To correctly calculates visibility of an
- * element, the cursor must know how much height occuped by a fixed
- * panel.
- * The scrollTopMargin defines margin occuped by fixed panel.
- */
- _scrollTopMargin: {
- type: Number,
- value: 0,
- },
- };
- }
+ /**
+ * gr-diff-view has gr-fixed-panel on top. The panel can
+ * intersect a main element and partially hides a content of
+ * the main element. To correctly calculates visibility of an
+ * element, the cursor must know how much height occuped by a fixed
+ * panel.
+ * The scrollTopMargin defines margin occuped by fixed panel.
+ */
+ _scrollTopMargin: {
+ type: Number,
+ value: 0,
+ },
+ };
+ }
- static get observers() {
- return [
- '_getProjectConfig(_change.project)',
- '_getFiles(_changeNum, _patchRange.*, _changeComments)',
- '_setReviewedObserver(_loggedIn, params.*, _prefs)',
- ];
- }
+ static get observers() {
+ return [
+ '_getProjectConfig(_change.project)',
+ '_getFiles(_changeNum, _patchRange.*, _changeComments)',
+ '_setReviewedObserver(_loggedIn, params.*, _prefs)',
+ ];
+ }
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- };
- }
+ get keyBindings() {
+ return {
+ esc: '_handleEscKey',
+ };
+ }
- keyboardShortcuts() {
- return {
- [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
- [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
- [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
- [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
- [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
- [this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
- '_handleNextLineOrFileWithComments',
- [this.Shortcut.PREV_FILE_WITH_COMMENTS]:
- '_handlePrevLineOrFileWithComments',
- [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
- [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
- [this.Shortcut.NEXT_FILE]: '_handleNextFile',
- [this.Shortcut.PREV_FILE]: '_handlePrevFile',
- [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
- [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
- [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
- [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
- [this.Shortcut.OPEN_REPLY_DIALOG]:
- '_handleOpenReplyDialogOrToggleLeftPane',
- [this.Shortcut.TOGGLE_LEFT_PANE]:
- '_handleOpenReplyDialogOrToggleLeftPane',
- [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
- [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
- [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
- [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
- [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
- [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
- [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
+ [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
+ [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
+ [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
+ [this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
+ '_handleNextLineOrFileWithComments',
+ [this.Shortcut.PREV_FILE_WITH_COMMENTS]:
+ '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
+ [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
+ [this.Shortcut.NEXT_FILE]: '_handleNextFile',
+ [this.Shortcut.PREV_FILE]: '_handlePrevFile',
+ [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
+ [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
+ [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
+ [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
+ [this.Shortcut.OPEN_REPLY_DIALOG]:
+ '_handleOpenReplyDialogOrToggleLeftPane',
+ [this.Shortcut.TOGGLE_LEFT_PANE]:
+ '_handleOpenReplyDialogOrToggleLeftPane',
+ [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
+ [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
+ [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+ [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+ [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
+ [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
+ [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
- // Final two are actually handled by gr-comment-thread.
- [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
- [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
- };
- }
+ // Final two are actually handled by gr-comment-thread.
+ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+ [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
- this.addEventListener('open-fix-preview',
- this._onOpenFixPreview.bind(this));
- this.$.cursor.push('diffs', this.$.diffHost);
- }
+ this.addEventListener('open-fix-preview',
+ this._onOpenFixPreview.bind(this));
+ this.$.cursor.push('diffs', this.$.diffHost);
+ }
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
- _getProjectConfig(project) {
- return this.$.restAPI.getProjectConfig(project).then(
- config => {
- this._projectConfig = config;
- });
- }
-
- _getChangeDetail(changeNum) {
- return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
- this._change = change;
- return change;
- });
- }
-
- _getChangeEdit(changeNum) {
- return this.$.restAPI.getChangeEdit(this._changeNum);
- }
-
- _getSortedFileList(files) {
- return files.sortedFileList;
- }
-
- _getFiles(changeNum, patchRangeRecord, changeComments) {
- // Polymer 2: check for undefined
- if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments]
- .some(arg => arg === undefined)) {
- return Promise.resolve();
- }
-
- const patchRange = patchRangeRecord.base;
- return this.$.restAPI.getChangeFiles(
- changeNum, patchRange).then(changeFiles => {
- if (!changeFiles) return;
- const commentedPaths = changeComments.getPaths(patchRange);
- const files = Object.assign({}, changeFiles);
- Object.keys(commentedPaths).forEach(commentedPath => {
- if (files.hasOwnProperty(commentedPath)) { return; }
- files[commentedPath] = {status: 'U'};
+ _getProjectConfig(project) {
+ return this.$.restAPI.getProjectConfig(project).then(
+ config => {
+ this._projectConfig = config;
});
- this._files = {
- sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
- changeFilesByPath: files,
- };
+ }
+
+ _getChangeDetail(changeNum) {
+ return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
+ this._change = change;
+ return change;
+ });
+ }
+
+ _getChangeEdit(changeNum) {
+ return this.$.restAPI.getChangeEdit(this._changeNum);
+ }
+
+ _getSortedFileList(files) {
+ return files.sortedFileList;
+ }
+
+ _getFiles(changeNum, patchRangeRecord, changeComments) {
+ // Polymer 2: check for undefined
+ if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments]
+ .some(arg => arg === undefined)) {
+ return Promise.resolve();
+ }
+
+ const patchRange = patchRangeRecord.base;
+ return this.$.restAPI.getChangeFiles(
+ changeNum, patchRange).then(changeFiles => {
+ if (!changeFiles) return;
+ const commentedPaths = changeComments.getPaths(patchRange);
+ const files = Object.assign({}, changeFiles);
+ Object.keys(commentedPaths).forEach(commentedPath => {
+ if (files.hasOwnProperty(commentedPath)) { return; }
+ files[commentedPath] = {status: 'U'};
});
+ this._files = {
+ sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
+ changeFilesByPath: files,
+ };
+ });
+ }
+
+ _getDiffPreferences() {
+ return this.$.restAPI.getDiffPreferences().then(prefs => {
+ this._prefs = prefs;
+ });
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ _getWindowWidth() {
+ return window.innerWidth;
+ }
+
+ _handleReviewedChange(e) {
+ this._setReviewed(dom(e).rootTarget.checked);
+ }
+
+ _setReviewed(reviewed) {
+ if (this._editMode) { return; }
+ this.$.reviewed.checked = reviewed;
+ this._saveReviewedState(reviewed).catch(err => {
+ this.fire('show-alert', {message: ERR_REVIEW_STATUS});
+ throw err;
+ });
+ }
+
+ _saveReviewedState(reviewed) {
+ return this.$.restAPI.saveFileReviewed(this._changeNum,
+ this._patchRange.patchNum, this._path, reviewed);
+ }
+
+ _handleToggleFileReviewed(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._setReviewed(!this.$.reviewed.checked);
+ }
+
+ _handleEscKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.diffHost.displayLine = false;
+ }
+
+ _handleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveLeft();
+ }
+
+ _handleRightPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveRight();
+ }
+
+ _handlePrevLineOrFileWithComments(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ if (e.detail.keyboardEvent.shiftKey &&
+ e.detail.keyboardEvent.keyCode === 75) { // 'K'
+ this._moveToPreviousFileWithComment();
+ return;
}
+ if (this.modifierPressed(e)) { return; }
- _getDiffPreferences() {
- return this.$.restAPI.getDiffPreferences().then(prefs => {
- this._prefs = prefs;
- });
+ e.preventDefault();
+ this.$.diffHost.displayLine = true;
+ this.$.cursor.moveUp();
+ }
+
+ _handleVisibleLine(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ this.$.cursor.moveToVisibleArea();
+ }
+
+ _onOpenFixPreview(e) {
+ this.$.applyFixDialog.open(e);
+ }
+
+ _handleNextLineOrFileWithComments(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ if (e.detail.keyboardEvent.shiftKey &&
+ e.detail.keyboardEvent.keyCode === 74) { // 'J'
+ this._moveToNextFileWithComment();
+ return;
}
+ if (this.modifierPressed(e)) { return; }
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
+ e.preventDefault();
+ this.$.diffHost.displayLine = true;
+ this.$.cursor.moveDown();
+ }
- _getWindowWidth() {
- return window.innerWidth;
- }
+ _moveToPreviousFileWithComment() {
+ if (!this._commentSkips) { return; }
- _handleReviewedChange(e) {
- this._setReviewed(Polymer.dom(e).rootTarget.checked);
- }
-
- _setReviewed(reviewed) {
- if (this._editMode) { return; }
- this.$.reviewed.checked = reviewed;
- this._saveReviewedState(reviewed).catch(err => {
- this.fire('show-alert', {message: ERR_REVIEW_STATUS});
- throw err;
- });
- }
-
- _saveReviewedState(reviewed) {
- return this.$.restAPI.saveFileReviewed(this._changeNum,
- this._patchRange.patchNum, this._path, reviewed);
- }
-
- _handleToggleFileReviewed(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this._setReviewed(!this.$.reviewed.checked);
- }
-
- _handleEscKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.diffHost.displayLine = false;
- }
-
- _handleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.moveLeft();
- }
-
- _handleRightPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.moveRight();
- }
-
- _handlePrevLineOrFileWithComments(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- if (e.detail.keyboardEvent.shiftKey &&
- e.detail.keyboardEvent.keyCode === 75) { // 'K'
- this._moveToPreviousFileWithComment();
- return;
- }
- if (this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.diffHost.displayLine = true;
- this.$.cursor.moveUp();
- }
-
- _handleVisibleLine(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- this.$.cursor.moveToVisibleArea();
- }
-
- _onOpenFixPreview(e) {
- this.$.applyFixDialog.open(e);
- }
-
- _handleNextLineOrFileWithComments(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- if (e.detail.keyboardEvent.shiftKey &&
- e.detail.keyboardEvent.keyCode === 74) { // 'J'
- this._moveToNextFileWithComment();
- return;
- }
- if (this.modifierPressed(e)) { return; }
-
- e.preventDefault();
- this.$.diffHost.displayLine = true;
- this.$.cursor.moveDown();
- }
-
- _moveToPreviousFileWithComment() {
- if (!this._commentSkips) { return; }
-
- // If there is no previous diff with comments, then return to the change
- // view.
- if (!this._commentSkips.previous) {
- this._navToChangeView();
- return;
- }
-
- Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous,
- this._patchRange.patchNum, this._patchRange.basePatchNum);
- }
-
- _moveToNextFileWithComment() {
- if (!this._commentSkips) { return; }
-
- // If there is no next diff with comments, then return to the change view.
- if (!this._commentSkips.next) {
- this._navToChangeView();
- return;
- }
-
- Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next,
- this._patchRange.patchNum, this._patchRange.basePatchNum);
- }
-
- _handleNewComment(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- e.preventDefault();
- this.$.cursor.createCommentInPlace();
- }
-
- _handlePrevFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._navToFile(this._path, this._fileList, -1);
- }
-
- _handleNextFile(e) {
- // Check for meta key to avoid overriding native chrome shortcut.
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.getKeyboardEvent(e).metaKey) { return; }
-
- e.preventDefault();
- this._navToFile(this._path, this._fileList, 1);
- }
-
- _handleNextChunkOrCommentThread(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- if (e.detail.keyboardEvent.shiftKey) {
- this.$.cursor.moveToNextCommentThread();
- } else {
- if (this.modifierPressed(e)) { return; }
- this.$.cursor.moveToNextChunk();
- }
- }
-
- _handlePrevChunkOrCommentThread(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- e.preventDefault();
- if (e.detail.keyboardEvent.shiftKey) {
- this.$.cursor.moveToPreviousCommentThread();
- } else {
- if (this.modifierPressed(e)) { return; }
- this.$.cursor.moveToPreviousChunk();
- }
- }
-
- _handleOpenReplyDialogOrToggleLeftPane(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
- e.preventDefault();
- this.$.diffHost.toggleLeftDiff();
- return;
- }
-
- if (this.modifierPressed(e)) { return; }
-
- if (!this._loggedIn) { return; }
-
- this.set('changeViewState.showReplyDialog', true);
- e.preventDefault();
+ // If there is no previous diff with comments, then return to the change
+ // view.
+ if (!this._commentSkips.previous) {
this._navToChangeView();
+ return;
}
- _handleUpToChange(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
+ Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous,
+ this._patchRange.patchNum, this._patchRange.basePatchNum);
+ }
- e.preventDefault();
+ _moveToNextFileWithComment() {
+ if (!this._commentSkips) { return; }
+
+ // If there is no next diff with comments, then return to the change view.
+ if (!this._commentSkips.next) {
this._navToChangeView();
+ return;
}
- _handleCommaKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- if (this._diffPrefsDisabled) { return; }
+ Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next,
+ this._patchRange.patchNum, this._patchRange.basePatchNum);
+ }
+ _handleNewComment(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ e.preventDefault();
+ this.$.cursor.createCommentInPlace();
+ }
+
+ _handlePrevFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._navToFile(this._path, this._fileList, -1);
+ }
+
+ _handleNextFile(e) {
+ // Check for meta key to avoid overriding native chrome shortcut.
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.getKeyboardEvent(e).metaKey) { return; }
+
+ e.preventDefault();
+ this._navToFile(this._path, this._fileList, 1);
+ }
+
+ _handleNextChunkOrCommentThread(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ if (e.detail.keyboardEvent.shiftKey) {
+ this.$.cursor.moveToNextCommentThread();
+ } else {
+ if (this.modifierPressed(e)) { return; }
+ this.$.cursor.moveToNextChunk();
+ }
+ }
+
+ _handlePrevChunkOrCommentThread(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ e.preventDefault();
+ if (e.detail.keyboardEvent.shiftKey) {
+ this.$.cursor.moveToPreviousCommentThread();
+ } else {
+ if (this.modifierPressed(e)) { return; }
+ this.$.cursor.moveToPreviousChunk();
+ }
+ }
+
+ _handleOpenReplyDialogOrToggleLeftPane(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
e.preventDefault();
- this.$.diffPreferencesDialog.open();
+ this.$.diffHost.toggleLeftDiff();
+ return;
}
- _handleToggleDiffMode(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
+ if (this.modifierPressed(e)) { return; }
- e.preventDefault();
- if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
- } else {
- this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
- }
+ if (!this._loggedIn) { return; }
+
+ this.set('changeViewState.showReplyDialog', true);
+ e.preventDefault();
+ this._navToChangeView();
+ }
+
+ _handleUpToChange(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._navToChangeView();
+ }
+
+ _handleCommaKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ if (this._diffPrefsDisabled) { return; }
+
+ e.preventDefault();
+ this.$.diffPreferencesDialog.open();
+ }
+
+ _handleToggleDiffMode(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
+ } else {
+ this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
}
+ }
- _navToChangeView() {
- if (!this._changeNum || !this._patchRange.patchNum) { return; }
+ _navToChangeView() {
+ if (!this._changeNum || !this._patchRange.patchNum) { return; }
+ this._navigateToChange(
+ this._change,
+ this._patchRange,
+ this._change && this._change.revisions);
+ }
+
+ _navToFile(path, fileList, direction) {
+ const newPath = this._getNavLinkPath(path, fileList, direction);
+ if (!newPath) { return; }
+
+ if (newPath.up) {
this._navigateToChange(
this._change,
this._patchRange,
this._change && this._change.revisions);
+ return;
}
- _navToFile(path, fileList, direction) {
- const newPath = this._getNavLinkPath(path, fileList, direction);
- if (!newPath) { return; }
+ Gerrit.Nav.navigateToDiff(this._change, newPath.path,
+ this._patchRange.patchNum, this._patchRange.basePatchNum);
+ }
- if (newPath.up) {
- this._navigateToChange(
- this._change,
- this._patchRange,
- this._change && this._change.revisions);
- return;
- }
+ /**
+ * @param {?string} path The path of the current file being shown.
+ * @param {!Array<string>} fileList The list of files in this change and
+ * patch range.
+ * @param {number} direction Either 1 (next file) or -1 (prev file).
+ * @param {(number|boolean)} opt_noUp Whether to return to the change view
+ * when advancing the file goes outside the bounds of fileList.
+ *
+ * @return {?string} The next URL when proceeding in the specified
+ * direction.
+ */
+ _computeNavLinkURL(change, path, fileList, direction, opt_noUp) {
+ const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp);
+ if (!newPath) { return null; }
- Gerrit.Nav.navigateToDiff(this._change, newPath.path,
- this._patchRange.patchNum, this._patchRange.basePatchNum);
+ if (newPath.up) {
+ return this._getChangePath(
+ this._change,
+ this._patchRange,
+ this._change && this._change.revisions);
+ }
+ return this._getDiffUrl(this._change, this._patchRange, newPath.path);
+ }
+
+ _goToEditFile() {
+ // TODO(taoalpha): add a shortcut for editing
+ const editUrl = Gerrit.Nav.getEditUrlForDiff(
+ this._change, this._path, this._patchRange.patchNum);
+ return Gerrit.Nav.navigateToRelativeUrl(editUrl);
+ }
+
+ /**
+ * Gives an object representing the target of navigating either left or
+ * right through the change. The resulting object will have one of the
+ * following forms:
+ * * {path: "<target file path>"} - When another file path should be the
+ * result of the navigation.
+ * * {up: true} - When the result of navigating should go back to the
+ * change view.
+ * * null - When no navigation is possible for the given direction.
+ *
+ * @param {?string} path The path of the current file being shown.
+ * @param {!Array<string>} fileList The list of files in this change and
+ * patch range.
+ * @param {number} direction Either 1 (next file) or -1 (prev file).
+ * @param {?number|boolean=} opt_noUp Whether to return to the change view
+ * when advancing the file goes outside the bounds of fileList.
+ * @return {?Object}
+ */
+ _getNavLinkPath(path, fileList, direction, opt_noUp) {
+ if (!path || !fileList || fileList.length === 0) { return null; }
+
+ let idx = fileList.indexOf(path);
+ if (idx === -1) {
+ const file = direction > 0 ?
+ fileList[0] :
+ fileList[fileList.length - 1];
+ return {path: file};
}
- /**
- * @param {?string} path The path of the current file being shown.
- * @param {!Array<string>} fileList The list of files in this change and
- * patch range.
- * @param {number} direction Either 1 (next file) or -1 (prev file).
- * @param {(number|boolean)} opt_noUp Whether to return to the change view
- * when advancing the file goes outside the bounds of fileList.
- *
- * @return {?string} The next URL when proceeding in the specified
- * direction.
- */
- _computeNavLinkURL(change, path, fileList, direction, opt_noUp) {
- const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp);
- if (!newPath) { return null; }
-
- if (newPath.up) {
- return this._getChangePath(
- this._change,
- this._patchRange,
- this._change && this._change.revisions);
- }
- return this._getDiffUrl(this._change, this._patchRange, newPath.path);
+ idx += direction;
+ // Redirect to the change view if opt_noUp isn’t truthy and idx falls
+ // outside the bounds of [0, fileList.length).
+ if (idx < 0 || idx > fileList.length - 1) {
+ if (opt_noUp) { return null; }
+ return {up: true};
}
- _goToEditFile() {
- // TODO(taoalpha): add a shortcut for editing
- const editUrl = Gerrit.Nav.getEditUrlForDiff(
- this._change, this._path, this._patchRange.patchNum);
- return Gerrit.Nav.navigateToRelativeUrl(editUrl);
+ return {path: fileList[idx]};
+ }
+
+ _getReviewedFiles(changeNum, patchNum) {
+ return this.$.restAPI.getReviewedFiles(changeNum, patchNum)
+ .then(files => {
+ this._reviewedFiles = new Set(files);
+ return this._reviewedFiles;
+ });
+ }
+
+ _getReviewedStatus(editMode, changeNum, patchNum, path) {
+ if (editMode) { return Promise.resolve(false); }
+ return this._getReviewedFiles(changeNum, patchNum)
+ .then(files => files.has(path));
+ }
+
+ _paramsChanged(value) {
+ if (value.view !== Gerrit.Nav.View.DIFF) { return; }
+
+ if (value.changeNum && value.project) {
+ this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
}
- /**
- * Gives an object representing the target of navigating either left or
- * right through the change. The resulting object will have one of the
- * following forms:
- * * {path: "<target file path>"} - When another file path should be the
- * result of the navigation.
- * * {up: true} - When the result of navigating should go back to the
- * change view.
- * * null - When no navigation is possible for the given direction.
- *
- * @param {?string} path The path of the current file being shown.
- * @param {!Array<string>} fileList The list of files in this change and
- * patch range.
- * @param {number} direction Either 1 (next file) or -1 (prev file).
- * @param {?number|boolean=} opt_noUp Whether to return to the change view
- * when advancing the file goes outside the bounds of fileList.
- * @return {?Object}
- */
- _getNavLinkPath(path, fileList, direction, opt_noUp) {
- if (!path || !fileList || fileList.length === 0) { return null; }
+ this.$.diffHost.lineOfInterest = this._getLineOfInterest(this.params);
+ this._initCursor(this.params);
- let idx = fileList.indexOf(path);
- if (idx === -1) {
- const file = direction > 0 ?
- fileList[0] :
- fileList[fileList.length - 1];
- return {path: file};
- }
+ this._changeNum = value.changeNum;
+ this._path = value.path;
+ this._patchRange = {
+ patchNum: value.patchNum,
+ basePatchNum: value.basePatchNum || PARENT,
+ };
- idx += direction;
- // Redirect to the change view if opt_noUp isn’t truthy and idx falls
- // outside the bounds of [0, fileList.length).
- if (idx < 0 || idx > fileList.length - 1) {
- if (opt_noUp) { return null; }
- return {up: true};
- }
+ // NOTE: This may be called before attachment (e.g. while parentElement is
+ // null). Fire title-change in an async so that, if attachment to the DOM
+ // has been queued, the event can bubble up to the handler in gr-app.
+ this.async(() => {
+ this.fire('title-change',
+ {title: this.computeTruncatedPath(this._path)});
+ });
- return {path: fileList[idx]};
+ // When navigating away from the page, there is a possibility that the
+ // patch number is no longer a part of the URL (say when navigating to
+ // the top-level change info view) and therefore undefined in `params`.
+ if (!this._patchRange.patchNum) {
+ return;
}
- _getReviewedFiles(changeNum, patchNum) {
- return this.$.restAPI.getReviewedFiles(changeNum, patchNum)
- .then(files => {
- this._reviewedFiles = new Set(files);
- return this._reviewedFiles;
- });
- }
+ const promises = [];
- _getReviewedStatus(editMode, changeNum, patchNum, path) {
- if (editMode) { return Promise.resolve(false); }
- return this._getReviewedFiles(changeNum, patchNum)
- .then(files => files.has(path));
- }
+ promises.push(this._getDiffPreferences());
- _paramsChanged(value) {
- if (value.view !== Gerrit.Nav.View.DIFF) { return; }
+ promises.push(this._getPreferences().then(prefs => {
+ this._userPrefs = prefs;
+ }));
- if (value.changeNum && value.project) {
- this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
- }
-
- this.$.diffHost.lineOfInterest = this._getLineOfInterest(this.params);
- this._initCursor(this.params);
-
- this._changeNum = value.changeNum;
- this._path = value.path;
- this._patchRange = {
- patchNum: value.patchNum,
- basePatchNum: value.basePatchNum || PARENT,
- };
-
- // NOTE: This may be called before attachment (e.g. while parentElement is
- // null). Fire title-change in an async so that, if attachment to the DOM
- // has been queued, the event can bubble up to the handler in gr-app.
- this.async(() => {
- this.fire('title-change',
- {title: this.computeTruncatedPath(this._path)});
- });
-
- // When navigating away from the page, there is a possibility that the
- // patch number is no longer a part of the URL (say when navigating to
- // the top-level change info view) and therefore undefined in `params`.
- if (!this._patchRange.patchNum) {
- return;
- }
-
- const promises = [];
-
- promises.push(this._getDiffPreferences());
-
- promises.push(this._getPreferences().then(prefs => {
- this._userPrefs = prefs;
- }));
-
- promises.push(this._getChangeDetail(this._changeNum).then(change => {
- let commit;
- let baseCommit;
- if (change) {
- for (const commitSha in change.revisions) {
- if (!change.revisions.hasOwnProperty(commitSha)) continue;
- const revision = change.revisions[commitSha];
- const patchNum = revision._number.toString();
- if (patchNum === this._patchRange.patchNum) {
- commit = commitSha;
- const commitObj = revision.commit || {};
- const parents = commitObj.parents || [];
- if (this._patchRange.basePatchNum === PARENT && parents.length) {
- baseCommit = parents[parents.length - 1].commit;
- }
- } else if (patchNum === this._patchRange.basePatchNum) {
- baseCommit = commitSha;
+ promises.push(this._getChangeDetail(this._changeNum).then(change => {
+ let commit;
+ let baseCommit;
+ if (change) {
+ for (const commitSha in change.revisions) {
+ if (!change.revisions.hasOwnProperty(commitSha)) continue;
+ const revision = change.revisions[commitSha];
+ const patchNum = revision._number.toString();
+ if (patchNum === this._patchRange.patchNum) {
+ commit = commitSha;
+ const commitObj = revision.commit || {};
+ const parents = commitObj.parents || [];
+ if (this._patchRange.basePatchNum === PARENT && parents.length) {
+ baseCommit = parents[parents.length - 1].commit;
}
+ } else if (patchNum === this._patchRange.basePatchNum) {
+ baseCommit = commitSha;
}
- this._commitRange = {commit, baseCommit};
}
- }));
+ this._commitRange = {commit, baseCommit};
+ }
+ }));
- promises.push(this._loadComments());
+ promises.push(this._loadComments());
- promises.push(this._getChangeEdit(this._changeNum));
+ promises.push(this._getChangeEdit(this._changeNum));
- this._loading = true;
- return Promise.all(promises)
- .then(r => {
- const edit = r[4];
- if (edit) {
- this.set('_change.revisions.' + edit.commit.commit, {
- _number: this.EDIT_NAME,
- basePatchNum: edit.base_patch_set_number,
- commit: edit.commit,
- });
- }
- this._loading = false;
- this.$.diffHost.comments = this._commentsForDiff;
- return this.$.diffHost.reload(true);
- })
- .then(() => {
- this.$.reporting.diffViewFullyLoaded();
- // If diff view displayed has not ended yet, it ends here.
- this.$.reporting.diffViewDisplayed();
- });
- }
-
- _changeViewStateChanged(changeViewState) {
- if (changeViewState.diffMode === null) {
- // If screen size is small, always default to unified view.
- this.$.restAPI.getPreferences().then(prefs => {
- this.set('changeViewState.diffMode', prefs.default_diff_view);
+ this._loading = true;
+ return Promise.all(promises)
+ .then(r => {
+ const edit = r[4];
+ if (edit) {
+ this.set('_change.revisions.' + edit.commit.commit, {
+ _number: this.EDIT_NAME,
+ basePatchNum: edit.base_patch_set_number,
+ commit: edit.commit,
+ });
+ }
+ this._loading = false;
+ this.$.diffHost.comments = this._commentsForDiff;
+ return this.$.diffHost.reload(true);
+ })
+ .then(() => {
+ this.$.reporting.diffViewFullyLoaded();
+ // If diff view displayed has not ended yet, it ends here.
+ this.$.reporting.diffViewDisplayed();
});
- }
- }
+ }
- _setReviewedObserver(_loggedIn, paramsRecord, _prefs) {
- // Polymer 2: check for undefined
- if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) {
- return;
- }
-
- const params = paramsRecord.base || {};
- if (!_loggedIn) { return; }
-
- if (_prefs.manual_review) {
- // Checkbox state needs to be set explicitly only when manual_review
- // is specified.
- this._getReviewedStatus(this.editMode, this._changeNum,
- this._patchRange.patchNum, this._path).then(status => {
- this.$.reviewed.checked = status;
- });
- return;
- }
-
- if (params.view === Gerrit.Nav.View.DIFF) {
- this._setReviewed(true);
- }
- }
-
- /**
- * If the params specify a diff address then configure the diff cursor.
- */
- _initCursor(params) {
- if (params.lineNum === undefined) { return; }
- if (params.leftSide) {
- this.$.cursor.side = DiffSides.LEFT;
- } else {
- this.$.cursor.side = DiffSides.RIGHT;
- }
- this.$.cursor.initialLineNumber = params.lineNum;
- }
-
- _getLineOfInterest(params) {
- // If there is a line number specified, pass it along to the diff so that
- // it will not get collapsed.
- if (!params.lineNum) { return null; }
- return {number: params.lineNum, leftSide: params.leftSide};
- }
-
- _pathChanged(path) {
- if (path) {
- this.fire('title-change',
- {title: this.computeTruncatedPath(path)});
- }
-
- if (this._fileList.length == 0) { return; }
-
- this.set('changeViewState.selectedFileIndex',
- this._fileList.indexOf(path));
- }
-
- _getDiffUrl(change, patchRange, path) {
- if ([change, patchRange, path].some(arg => arg === undefined)) {
- return '';
- }
- return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
- patchRange.basePatchNum);
- }
-
- _patchRangeStr(patchRange) {
- let patchStr = patchRange.patchNum;
- if (patchRange.basePatchNum != null &&
- patchRange.basePatchNum != PARENT) {
- patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
- }
- return patchStr;
- }
-
- /**
- * When the latest patch of the change is selected (and there is no base
- * patch) then the patch range need not appear in the URL. Return a patch
- * range object with undefined values when a range is not needed.
- *
- * @param {!Object} patchRange
- * @param {!Object} revisions
- * @return {!Object}
- */
- _getChangeUrlRange(patchRange, revisions) {
- let patchNum = undefined;
- let basePatchNum = undefined;
- let latestPatchNum = -1;
- for (const rev of Object.values(revisions || {})) {
- latestPatchNum = Math.max(latestPatchNum, rev._number);
- }
- if (patchRange.basePatchNum !== PARENT ||
- parseInt(patchRange.patchNum, 10) !== latestPatchNum) {
- patchNum = patchRange.patchNum;
- basePatchNum = patchRange.basePatchNum;
- }
- return {patchNum, basePatchNum};
- }
-
- _getChangePath(change, patchRange, revisions) {
- if ([change, patchRange].some(arg => arg === undefined)) {
- return '';
- }
- const range = this._getChangeUrlRange(patchRange, revisions);
- return Gerrit.Nav.getUrlForChange(change, range.patchNum,
- range.basePatchNum);
- }
-
- _navigateToChange(change, patchRange, revisions) {
- const range = this._getChangeUrlRange(patchRange, revisions);
- Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum);
- }
-
- _computeChangePath(change, patchRangeRecord, revisions) {
- return this._getChangePath(change, patchRangeRecord.base, revisions);
- }
-
- _formatFilesForDropdown(files, patchNum, changeComments) {
- // Polymer 2: check for undefined
- if ([
- files,
- patchNum,
- changeComments,
- ].some(arg => arg === undefined)) {
- return;
- }
-
- if (!files) { return; }
- const dropdownContent = [];
- for (const path of files.sortedFileList) {
- dropdownContent.push({
- text: this.computeDisplayPath(path),
- mobileText: this.computeTruncatedPath(path),
- value: path,
- bottomText: this._computeCommentString(changeComments, patchNum,
- path, files.changeFilesByPath[path]),
- });
- }
- return dropdownContent;
- }
-
- _computeCommentString(changeComments, patchNum, path, changeFileInfo) {
- const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
- path);
- const commentCount = changeComments.computeCommentCount(patchNum, path);
- const commentString = GrCountStringFormatter.computePluralString(
- commentCount, 'comment');
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
-
- const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': '';
-
- return [
- unmodifiedString,
- commentString,
- unresolvedString]
- .filter(v => v && v.length > 0).join(', ');
- }
-
- _computePrefsButtonHidden(prefs, prefsDisabled) {
- return prefsDisabled || !prefs;
- }
-
- _handleFileChange(e) {
- // This is when it gets set initially.
- const path = e.detail.value;
- if (path === this._path) {
- return;
- }
-
- Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum,
- this._patchRange.basePatchNum);
- }
-
- _handleFileTap(e) {
- // async is needed so that that the click event is fired before the
- // dropdown closes (This was a bug for touch devices).
- this.async(() => {
- this.$.dropdown.close();
- }, 1);
- }
-
- _handlePatchChange(e) {
- const {basePatchNum, patchNum} = e.detail;
- if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
- this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; }
- Gerrit.Nav.navigateToDiff(
- this._change, this._path, patchNum, basePatchNum);
- }
-
- _handlePrefsTap(e) {
- e.preventDefault();
- this.$.diffPreferencesDialog.open();
- }
-
- /**
- * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
- * the current state.
- *
- * The expected behavior is to use the mode specified in the user's
- * preferences unless they have manually chosen the alternative view or they
- * are on a mobile device. If the user navigates up to the change view, it
- * should clear this choice and revert to the preference the next time a
- * diff is viewed.
- *
- * Use side-by-side if the user is not logged in.
- *
- * @return {string}
- */
- _getDiffViewMode() {
- if (this.changeViewState.diffMode) {
- return this.changeViewState.diffMode;
- } else if (this._userPrefs) {
- this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
- return this._userPrefs.default_diff_view;
- } else {
- return 'SIDE_BY_SIDE';
- }
- }
-
- _computeModeSelectHideClass(isImageDiff) {
- return isImageDiff ? 'hide' : '';
- }
-
- _onLineSelected(e, detail) {
- this.$.cursor.moveToLineNumber(detail.number, detail.side);
- if (!this._change) { return; }
- const cursorAddress = this.$.cursor.getAddress();
- const number = cursorAddress ? cursorAddress.number : undefined;
- const leftSide = cursorAddress ? cursorAddress.leftSide : undefined;
- const url = Gerrit.Nav.getUrlForDiffById(this._changeNum,
- this._change.project, this._path, this._patchRange.patchNum,
- this._patchRange.basePatchNum, number, leftSide);
- history.replaceState(null, '', url);
- }
-
- _computeDownloadDropdownLinks(
- project, changeNum, patchRange, path, diff) {
- if (!patchRange || !patchRange.patchNum) { return []; }
-
- const links = [
- {
- url: this._computeDownloadPatchLink(
- project, changeNum, patchRange, path),
- name: 'Patch',
- },
- ];
-
- if (diff && diff.meta_a) {
- let leftPath = path;
- if (diff.change_type === 'RENAMED') {
- leftPath = diff.meta_a.name;
- }
- links.push(
- {
- url: this._computeDownloadFileLink(
- project, changeNum, patchRange, leftPath, true),
- name: 'Left Content',
- }
- );
- }
-
- if (diff && diff.meta_b) {
- links.push(
- {
- url: this._computeDownloadFileLink(
- project, changeNum, patchRange, path, false),
- name: 'Right Content',
- }
- );
- }
-
- return links;
- }
-
- _computeDownloadFileLink(
- project, changeNum, patchRange, path, isBase) {
- let patchNum = patchRange.patchNum;
-
- const comparedAgainsParent = patchRange.basePatchNum === 'PARENT';
-
- if (isBase && !comparedAgainsParent) {
- patchNum = patchRange.basePatchNum;
- }
-
- let url = this.changeBaseURL(project, changeNum, patchNum) +
- `/files/${encodeURIComponent(path)}/download`;
-
- if (isBase && comparedAgainsParent) {
- url += '?parent=1';
- }
-
- return url;
- }
-
- _computeDownloadPatchLink(project, changeNum, patchRange, path) {
- let url = this.changeBaseURL(project, changeNum, patchRange.patchNum);
- url += '/patch?zip&path=' + encodeURIComponent(path);
- return url;
- }
-
- _loadComments() {
- return this.$.commentAPI.loadAll(this._changeNum).then(comments => {
- this._changeComments = comments;
- this._commentMap = this._getPaths(this._patchRange);
-
- this._commentsForDiff = this._getCommentsForPath(this._path,
- this._patchRange, this._projectConfig);
+ _changeViewStateChanged(changeViewState) {
+ if (changeViewState.diffMode === null) {
+ // If screen size is small, always default to unified view.
+ this.$.restAPI.getPreferences().then(prefs => {
+ this.set('changeViewState.diffMode', prefs.default_diff_view);
});
}
-
- _getPaths(patchRange) {
- return this._changeComments.getPaths(patchRange);
- }
-
- _getCommentsForPath(path, patchRange, projectConfig) {
- return this._changeComments.getCommentsBySideForPath(path, patchRange,
- projectConfig);
- }
-
- _getDiffDrafts() {
- return this.$.restAPI.getDiffDrafts(this._changeNum);
- }
-
- _computeCommentSkips(commentMap, fileList, path) {
- // Polymer 2: check for undefined
- if ([
- commentMap,
- fileList,
- path,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const skips = {previous: null, next: null};
- if (!fileList.length) { return skips; }
- const pathIndex = fileList.indexOf(path);
-
- // Scan backward for the previous file.
- for (let i = pathIndex - 1; i >= 0; i--) {
- if (commentMap[fileList[i]]) {
- skips.previous = fileList[i];
- break;
- }
- }
-
- // Scan forward for the next file.
- for (let i = pathIndex + 1; i < fileList.length; i++) {
- if (commentMap[fileList[i]]) {
- skips.next = fileList[i];
- break;
- }
- }
-
- return skips;
- }
-
- _computeDiffClass(panelFloatingDisabled) {
- if (panelFloatingDisabled) {
- return 'noOverflow';
- }
- }
-
- /**
- * @param {!Object} patchRangeRecord
- */
- _computeEditMode(patchRangeRecord) {
- const patchRange = patchRangeRecord.base || {};
- return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
- }
-
- /**
- * @param {boolean} editMode
- */
- _computeContainerClass(editMode) {
- return editMode ? 'editMode' : '';
- }
-
- _computeBlameToggleLabel(loaded, loading) {
- if (loaded) { return 'Hide blame'; }
- return 'Show blame';
- }
-
- /**
- * Load and display blame information if it has not already been loaded.
- * Otherwise hide it.
- */
- _toggleBlame() {
- if (this._isBlameLoaded) {
- this.$.diffHost.clearBlame();
- return;
- }
-
- this._isBlameLoading = true;
- this.fire('show-alert', {message: MSG_LOADING_BLAME});
- this.$.diffHost.loadBlame()
- .then(() => {
- this._isBlameLoading = false;
- this.fire('show-alert', {message: MSG_LOADED_BLAME});
- })
- .catch(() => {
- this._isBlameLoading = false;
- });
- }
-
- _handleToggleBlame(e) {
- if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
- this._toggleBlame();
- }
-
- _computeBlameLoaderClass(isImageDiff, path) {
- return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
- }
-
- _getRevisionInfo(change) {
- return new Gerrit.RevisionInfo(change);
- }
-
- _computeFileNum(file, files) {
- // Polymer 2: check for undefined
- if ([file, files].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return files.findIndex(({value}) => value === file) + 1;
- }
-
- /**
- * @param {number} fileNum
- * @param {!Array<string>} files
- * @return {string}
- */
- _computeFileNumClass(fileNum, files) {
- if (files && fileNum > 0) {
- return 'show';
- }
- return '';
- }
-
- _handleExpandAllDiffContext(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- this.$.diffHost.expandAllContext();
- }
-
- _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
- return disableDiffPrefs || !loggedIn;
- }
-
- _handleNextUnreviewedFile(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- this._setReviewed(true);
- // Ensure that the currently viewed file always appears in unreviewedFiles
- // so we resolve the right "next" file.
- const unreviewedFiles = this._fileList
- .filter(file =>
- (file === this._path || !this._reviewedFiles.has(file)));
- this._navToFile(this._path, unreviewedFiles, 1);
- }
-
- _handleReloadingDiffPreference() {
- this._getDiffPreferences();
- }
-
- _onChangeHeaderPanelHeightChanged(e) {
- this._scrollTopMargin = e.detail.value;
- }
-
- _computeIsLoggedIn(loggedIn) {
- return loggedIn ? true : false;
- }
}
- customElements.define(GrDiffView.is, GrDiffView);
-})();
+ _setReviewedObserver(_loggedIn, paramsRecord, _prefs) {
+ // Polymer 2: check for undefined
+ if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) {
+ return;
+ }
+
+ const params = paramsRecord.base || {};
+ if (!_loggedIn) { return; }
+
+ if (_prefs.manual_review) {
+ // Checkbox state needs to be set explicitly only when manual_review
+ // is specified.
+ this._getReviewedStatus(this.editMode, this._changeNum,
+ this._patchRange.patchNum, this._path).then(status => {
+ this.$.reviewed.checked = status;
+ });
+ return;
+ }
+
+ if (params.view === Gerrit.Nav.View.DIFF) {
+ this._setReviewed(true);
+ }
+ }
+
+ /**
+ * If the params specify a diff address then configure the diff cursor.
+ */
+ _initCursor(params) {
+ if (params.lineNum === undefined) { return; }
+ if (params.leftSide) {
+ this.$.cursor.side = DiffSides.LEFT;
+ } else {
+ this.$.cursor.side = DiffSides.RIGHT;
+ }
+ this.$.cursor.initialLineNumber = params.lineNum;
+ }
+
+ _getLineOfInterest(params) {
+ // If there is a line number specified, pass it along to the diff so that
+ // it will not get collapsed.
+ if (!params.lineNum) { return null; }
+ return {number: params.lineNum, leftSide: params.leftSide};
+ }
+
+ _pathChanged(path) {
+ if (path) {
+ this.fire('title-change',
+ {title: this.computeTruncatedPath(path)});
+ }
+
+ if (this._fileList.length == 0) { return; }
+
+ this.set('changeViewState.selectedFileIndex',
+ this._fileList.indexOf(path));
+ }
+
+ _getDiffUrl(change, patchRange, path) {
+ if ([change, patchRange, path].some(arg => arg === undefined)) {
+ return '';
+ }
+ return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum,
+ patchRange.basePatchNum);
+ }
+
+ _patchRangeStr(patchRange) {
+ let patchStr = patchRange.patchNum;
+ if (patchRange.basePatchNum != null &&
+ patchRange.basePatchNum != PARENT) {
+ patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum;
+ }
+ return patchStr;
+ }
+
+ /**
+ * When the latest patch of the change is selected (and there is no base
+ * patch) then the patch range need not appear in the URL. Return a patch
+ * range object with undefined values when a range is not needed.
+ *
+ * @param {!Object} patchRange
+ * @param {!Object} revisions
+ * @return {!Object}
+ */
+ _getChangeUrlRange(patchRange, revisions) {
+ let patchNum = undefined;
+ let basePatchNum = undefined;
+ let latestPatchNum = -1;
+ for (const rev of Object.values(revisions || {})) {
+ latestPatchNum = Math.max(latestPatchNum, rev._number);
+ }
+ if (patchRange.basePatchNum !== PARENT ||
+ parseInt(patchRange.patchNum, 10) !== latestPatchNum) {
+ patchNum = patchRange.patchNum;
+ basePatchNum = patchRange.basePatchNum;
+ }
+ return {patchNum, basePatchNum};
+ }
+
+ _getChangePath(change, patchRange, revisions) {
+ if ([change, patchRange].some(arg => arg === undefined)) {
+ return '';
+ }
+ const range = this._getChangeUrlRange(patchRange, revisions);
+ return Gerrit.Nav.getUrlForChange(change, range.patchNum,
+ range.basePatchNum);
+ }
+
+ _navigateToChange(change, patchRange, revisions) {
+ const range = this._getChangeUrlRange(patchRange, revisions);
+ Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum);
+ }
+
+ _computeChangePath(change, patchRangeRecord, revisions) {
+ return this._getChangePath(change, patchRangeRecord.base, revisions);
+ }
+
+ _formatFilesForDropdown(files, patchNum, changeComments) {
+ // Polymer 2: check for undefined
+ if ([
+ files,
+ patchNum,
+ changeComments,
+ ].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (!files) { return; }
+ const dropdownContent = [];
+ for (const path of files.sortedFileList) {
+ dropdownContent.push({
+ text: this.computeDisplayPath(path),
+ mobileText: this.computeTruncatedPath(path),
+ value: path,
+ bottomText: this._computeCommentString(changeComments, patchNum,
+ path, files.changeFilesByPath[path]),
+ });
+ }
+ return dropdownContent;
+ }
+
+ _computeCommentString(changeComments, patchNum, path, changeFileInfo) {
+ const unresolvedCount = changeComments.computeUnresolvedNum(patchNum,
+ path);
+ const commentCount = changeComments.computeCommentCount(patchNum, path);
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': '';
+
+ return [
+ unmodifiedString,
+ commentString,
+ unresolvedString]
+ .filter(v => v && v.length > 0).join(', ');
+ }
+
+ _computePrefsButtonHidden(prefs, prefsDisabled) {
+ return prefsDisabled || !prefs;
+ }
+
+ _handleFileChange(e) {
+ // This is when it gets set initially.
+ const path = e.detail.value;
+ if (path === this._path) {
+ return;
+ }
+
+ Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum,
+ this._patchRange.basePatchNum);
+ }
+
+ _handleFileTap(e) {
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(() => {
+ this.$.dropdown.close();
+ }, 1);
+ }
+
+ _handlePatchChange(e) {
+ const {basePatchNum, patchNum} = e.detail;
+ if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
+ this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; }
+ Gerrit.Nav.navigateToDiff(
+ this._change, this._path, patchNum, basePatchNum);
+ }
+
+ _handlePrefsTap(e) {
+ e.preventDefault();
+ this.$.diffPreferencesDialog.open();
+ }
+
+ /**
+ * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
+ * the current state.
+ *
+ * The expected behavior is to use the mode specified in the user's
+ * preferences unless they have manually chosen the alternative view or they
+ * are on a mobile device. If the user navigates up to the change view, it
+ * should clear this choice and revert to the preference the next time a
+ * diff is viewed.
+ *
+ * Use side-by-side if the user is not logged in.
+ *
+ * @return {string}
+ */
+ _getDiffViewMode() {
+ if (this.changeViewState.diffMode) {
+ return this.changeViewState.diffMode;
+ } else if (this._userPrefs) {
+ this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
+ return this._userPrefs.default_diff_view;
+ } else {
+ return 'SIDE_BY_SIDE';
+ }
+ }
+
+ _computeModeSelectHideClass(isImageDiff) {
+ return isImageDiff ? 'hide' : '';
+ }
+
+ _onLineSelected(e, detail) {
+ this.$.cursor.moveToLineNumber(detail.number, detail.side);
+ if (!this._change) { return; }
+ const cursorAddress = this.$.cursor.getAddress();
+ const number = cursorAddress ? cursorAddress.number : undefined;
+ const leftSide = cursorAddress ? cursorAddress.leftSide : undefined;
+ const url = Gerrit.Nav.getUrlForDiffById(this._changeNum,
+ this._change.project, this._path, this._patchRange.patchNum,
+ this._patchRange.basePatchNum, number, leftSide);
+ history.replaceState(null, '', url);
+ }
+
+ _computeDownloadDropdownLinks(
+ project, changeNum, patchRange, path, diff) {
+ if (!patchRange || !patchRange.patchNum) { return []; }
+
+ const links = [
+ {
+ url: this._computeDownloadPatchLink(
+ project, changeNum, patchRange, path),
+ name: 'Patch',
+ },
+ ];
+
+ if (diff && diff.meta_a) {
+ let leftPath = path;
+ if (diff.change_type === 'RENAMED') {
+ leftPath = diff.meta_a.name;
+ }
+ links.push(
+ {
+ url: this._computeDownloadFileLink(
+ project, changeNum, patchRange, leftPath, true),
+ name: 'Left Content',
+ }
+ );
+ }
+
+ if (diff && diff.meta_b) {
+ links.push(
+ {
+ url: this._computeDownloadFileLink(
+ project, changeNum, patchRange, path, false),
+ name: 'Right Content',
+ }
+ );
+ }
+
+ return links;
+ }
+
+ _computeDownloadFileLink(
+ project, changeNum, patchRange, path, isBase) {
+ let patchNum = patchRange.patchNum;
+
+ const comparedAgainsParent = patchRange.basePatchNum === 'PARENT';
+
+ if (isBase && !comparedAgainsParent) {
+ patchNum = patchRange.basePatchNum;
+ }
+
+ let url = this.changeBaseURL(project, changeNum, patchNum) +
+ `/files/${encodeURIComponent(path)}/download`;
+
+ if (isBase && comparedAgainsParent) {
+ url += '?parent=1';
+ }
+
+ return url;
+ }
+
+ _computeDownloadPatchLink(project, changeNum, patchRange, path) {
+ let url = this.changeBaseURL(project, changeNum, patchRange.patchNum);
+ url += '/patch?zip&path=' + encodeURIComponent(path);
+ return url;
+ }
+
+ _loadComments() {
+ return this.$.commentAPI.loadAll(this._changeNum).then(comments => {
+ this._changeComments = comments;
+ this._commentMap = this._getPaths(this._patchRange);
+
+ this._commentsForDiff = this._getCommentsForPath(this._path,
+ this._patchRange, this._projectConfig);
+ });
+ }
+
+ _getPaths(patchRange) {
+ return this._changeComments.getPaths(patchRange);
+ }
+
+ _getCommentsForPath(path, patchRange, projectConfig) {
+ return this._changeComments.getCommentsBySideForPath(path, patchRange,
+ projectConfig);
+ }
+
+ _getDiffDrafts() {
+ return this.$.restAPI.getDiffDrafts(this._changeNum);
+ }
+
+ _computeCommentSkips(commentMap, fileList, path) {
+ // Polymer 2: check for undefined
+ if ([
+ commentMap,
+ fileList,
+ path,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const skips = {previous: null, next: null};
+ if (!fileList.length) { return skips; }
+ const pathIndex = fileList.indexOf(path);
+
+ // Scan backward for the previous file.
+ for (let i = pathIndex - 1; i >= 0; i--) {
+ if (commentMap[fileList[i]]) {
+ skips.previous = fileList[i];
+ break;
+ }
+ }
+
+ // Scan forward for the next file.
+ for (let i = pathIndex + 1; i < fileList.length; i++) {
+ if (commentMap[fileList[i]]) {
+ skips.next = fileList[i];
+ break;
+ }
+ }
+
+ return skips;
+ }
+
+ _computeDiffClass(panelFloatingDisabled) {
+ if (panelFloatingDisabled) {
+ return 'noOverflow';
+ }
+ }
+
+ /**
+ * @param {!Object} patchRangeRecord
+ */
+ _computeEditMode(patchRangeRecord) {
+ const patchRange = patchRangeRecord.base || {};
+ return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
+ }
+
+ /**
+ * @param {boolean} editMode
+ */
+ _computeContainerClass(editMode) {
+ return editMode ? 'editMode' : '';
+ }
+
+ _computeBlameToggleLabel(loaded, loading) {
+ if (loaded) { return 'Hide blame'; }
+ return 'Show blame';
+ }
+
+ /**
+ * Load and display blame information if it has not already been loaded.
+ * Otherwise hide it.
+ */
+ _toggleBlame() {
+ if (this._isBlameLoaded) {
+ this.$.diffHost.clearBlame();
+ return;
+ }
+
+ this._isBlameLoading = true;
+ this.fire('show-alert', {message: MSG_LOADING_BLAME});
+ this.$.diffHost.loadBlame()
+ .then(() => {
+ this._isBlameLoading = false;
+ this.fire('show-alert', {message: MSG_LOADED_BLAME});
+ })
+ .catch(() => {
+ this._isBlameLoading = false;
+ });
+ }
+
+ _handleToggleBlame(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ this._toggleBlame();
+ }
+
+ _computeBlameLoaderClass(isImageDiff, path) {
+ return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
+ }
+
+ _getRevisionInfo(change) {
+ return new Gerrit.RevisionInfo(change);
+ }
+
+ _computeFileNum(file, files) {
+ // Polymer 2: check for undefined
+ if ([file, files].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return files.findIndex(({value}) => value === file) + 1;
+ }
+
+ /**
+ * @param {number} fileNum
+ * @param {!Array<string>} files
+ * @return {string}
+ */
+ _computeFileNumClass(fileNum, files) {
+ if (files && fileNum > 0) {
+ return 'show';
+ }
+ return '';
+ }
+
+ _handleExpandAllDiffContext(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ this.$.diffHost.expandAllContext();
+ }
+
+ _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
+ return disableDiffPrefs || !loggedIn;
+ }
+
+ _handleNextUnreviewedFile(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ this._setReviewed(true);
+ // Ensure that the currently viewed file always appears in unreviewedFiles
+ // so we resolve the right "next" file.
+ const unreviewedFiles = this._fileList
+ .filter(file =>
+ (file === this._path || !this._reviewedFiles.has(file)));
+ this._navToFile(this._path, unreviewedFiles, 1);
+ }
+
+ _handleReloadingDiffPreference() {
+ this._getDiffPreferences();
+ }
+
+ _onChangeHeaderPanelHeightChanged(e) {
+ this._scrollTopMargin = e.detail.value;
+ }
+
+ _computeIsLoggedIn(loggedIn) {
+ return loggedIn ? true : false;
+ }
+}
+
+customElements.define(GrDiffView.is, GrDiffView);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
index 947ccbd..cf4cf92 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
@@ -1,50 +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="/bower_components/polymer/polymer.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-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
-<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
-<link rel="import" href="../gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html">
-<link rel="import" href="../gr-apply-fix-dialog/gr-apply-fix-dialog.html">
-<link rel="import" href="../gr-diff-host/gr-diff-host.html">
-<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
-<link rel="import" href="../gr-diff-preferences-dialog/gr-diff-preferences-dialog.html">
-<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
-
-<dom-module id="gr-diff-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
background-color: var(--view-background-color);
@@ -225,78 +197,43 @@
}
}
</style>
- <gr-fixed-panel
- class$="[[_computeContainerClass(_editMode)]]"
- floating-disabled="[[_panelFloatingDisabled]]"
- keep-on-scroll
- ready-for-measure="[[!_loading]]"
- on-floating-height-changed="_onChangeHeaderPanelHeightChanged"
- >
+ <gr-fixed-panel class\$="[[_computeContainerClass(_editMode)]]" floating-disabled="[[_panelFloatingDisabled]]" keep-on-scroll="" ready-for-measure="[[!_loading]]" on-floating-height-changed="_onChangeHeaderPanelHeightChanged">
<header>
<div>
- <a href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">[[_changeNum]]</a><!--
+ <a href\$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">[[_changeNum]]</a><!--
--><span class="changeNumberColon">:</span>
<span class="headerSubject">[[_change.subject]]</span>
- <input id="reviewed"
- class="reviewed hideOnEdit"
- type="checkbox"
- on-change="_handleReviewedChange"
- hidden$="[[!_loggedIn]]" hidden><!--
+ <input id="reviewed" class="reviewed hideOnEdit" type="checkbox" on-change="_handleReviewedChange" hidden\$="[[!_loggedIn]]" hidden=""><!--
--><div class="jumpToFileContainer">
- <gr-dropdown-list
- id="dropdown"
- value="[[_path]]"
- on-value-change="_handleFileChange"
- items="[[_formattedFiles]]"
- initial-count="75">
+ <gr-dropdown-list id="dropdown" value="[[_path]]" on-value-change="_handleFileChange" items="[[_formattedFiles]]" initial-count="75">
</gr-dropdown-list>
</div>
</div>
<div class="navLinks desktop">
- <span class$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]">
+ <span class\$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]">
File [[_fileNum]] of [[_formattedFiles.length]]
<span class="separator"></span>
</span>
- <a class="navLink"
- title="[[createTitle(Shortcut.PREV_FILE,
- ShortcutSection.NAVIGATION)]]"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
+ <a class="navLink" title="[[createTitle(Shortcut.PREV_FILE,
+ ShortcutSection.NAVIGATION)]]" href\$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
Prev</a>
<span class="separator"></span>
- <a class="navLink"
- title="[[createTitle(Shortcut.UP_TO_CHANGE,
- ShortcutSection.NAVIGATION)]]"
- href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
+ <a class="navLink" title="[[createTitle(Shortcut.UP_TO_CHANGE,
+ ShortcutSection.NAVIGATION)]]" href\$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
Up</a>
<span class="separator"></span>
- <a class="navLink"
- title="[[createTitle(Shortcut.NEXT_FILE,
- ShortcutSection.NAVIGATION)]]"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
+ <a class="navLink" title="[[createTitle(Shortcut.NEXT_FILE,
+ ShortcutSection.NAVIGATION)]]" href\$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
Next</a>
</div>
</header>
<div class="subHeader">
<div class="patchRangeLeft">
- <gr-patch-range-select
- id="rangeSelect"
- change-num="[[_changeNum]]"
- change-comments="[[_changeComments]]"
- patch-num="[[_patchRange.patchNum]]"
- base-patch-num="[[_patchRange.basePatchNum]]"
- files-weblinks="[[_filesWeblinks]]"
- available-patches="[[_allPatchSets]]"
- revisions="[[_change.revisions]]"
- revision-info="[[_revisionInfo]]"
- on-patch-range-change="_handlePatchChange">
+ <gr-patch-range-select id="rangeSelect" change-num="[[_changeNum]]" change-comments="[[_changeComments]]" patch-num="[[_patchRange.patchNum]]" base-patch-num="[[_patchRange.basePatchNum]]" files-weblinks="[[_filesWeblinks]]" available-patches="[[_allPatchSets]]" revisions="[[_change.revisions]]" revision-info="[[_revisionInfo]]" on-patch-range-change="_handlePatchChange">
</gr-patch-range-select>
<span class="download desktop">
<span class="separator"></span>
- <gr-dropdown
- link
- down-arrow
- items="[[_computeDownloadDropdownLinks(_change.project, _changeNum, _patchRange, _path, _diff)]]"
- horizontal-align="left">
+ <gr-dropdown link="" down-arrow="" items="[[_computeDownloadDropdownLinks(_change.project, _changeNum, _patchRange, _path, _diff)]]" horizontal-align="left">
<span class="downloadTitle">
Download
</span>
@@ -304,99 +241,54 @@
</span>
</div>
<div class="rightControls">
- <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _path)]]">
- <gr-button
- link
- id='toggleBlame'
- title="[[createTitle(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS)]]"
- disabled="[[_isBlameLoading]]"
- on-click="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
+ <span class\$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _path)]]">
+ <gr-button link="" id="toggleBlame" title="[[createTitle(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS)]]" disabled="[[_isBlameLoading]]" on-click="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button>
</span>
<template is="dom-if" if="[[_computeIsLoggedIn(_loggedIn)]]">
<span class="separator"></span>
<span class="editButton">
- <gr-button
- link
- title="Edit current file"
- on-click="_goToEditFile">edit</gr-button>
+ <gr-button link="" title="Edit current file" on-click="_goToEditFile">edit</gr-button>
</span>
</template>
<span class="separator"></span>
- <div class$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]">
+ <div class\$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]">
<span>Diff view:</span>
- <gr-diff-mode-selector
- id="modeSelect"
- save-on-change="[[!_diffPrefsDisabled]]"
- mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
+ <gr-diff-mode-selector id="modeSelect" save-on-change="[[!_diffPrefsDisabled]]" mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
</div>
- <span id="diffPrefsContainer"
- hidden$="[[_computePrefsButtonHidden(_prefs, _diffPrefsDisabled)]]" hidden>
+ <span id="diffPrefsContainer" hidden\$="[[_computePrefsButtonHidden(_prefs, _diffPrefsDisabled)]]" hidden="">
<span class="preferences desktop">
- <gr-button
- link
- class="prefsButton"
- has-tooltip
- title="Diff preferences"
- on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
+ <gr-button link="" class="prefsButton" has-tooltip="" title="Diff preferences" on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button>
</span>
</span>
<gr-endpoint-decorator name="annotation-toggler">
- <span hidden id="annotation-span">
+ <span hidden="" id="annotation-span">
<label for="annotation-checkbox" id="annotation-label"></label>
- <iron-input type="checkbox" disabled>
- <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled>
+ <iron-input type="checkbox" disabled="">
+ <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled="">
</iron-input>
</span>
</gr-endpoint-decorator>
</div>
</div>
<div class="fileNav mobile">
- <a class="mobileNavLink"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
+ <a class="mobileNavLink" href\$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]">
<</a>
<div class="fullFileName mobile">[[computeDisplayPath(_path)]]
</div>
- <a class="mobileNavLink"
- href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
+ <a class="mobileNavLink" href\$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]">
></a>
</div>
</gr-fixed-panel>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <gr-diff-host
- id="diffHost"
- hidden
- hidden$="[[_loading]]"
- class$="[[_computeDiffClass(_panelFloatingDisabled)]]"
- is-image-diff="{{_isImageDiff}}"
- files-weblinks="{{_filesWeblinks}}"
- diff="{{_diff}}"
- change-num="[[_changeNum]]"
- commit-range="[[_commitRange]]"
- patch-range="[[_patchRange]]"
- path="[[_path]]"
- prefs="[[_prefs]]"
- project-name="[[_change.project]]"
- view-mode="[[_diffMode]]"
- is-blame-loaded="{{_isBlameLoaded}}"
- on-comment-anchor-tap="_onLineSelected"
- on-line-selected="_onLineSelected">
+ <div class="loading" hidden\$="[[!_loading]]">Loading...</div>
+ <gr-diff-host id="diffHost" hidden="" hidden\$="[[_loading]]" class\$="[[_computeDiffClass(_panelFloatingDisabled)]]" is-image-diff="{{_isImageDiff}}" files-weblinks="{{_filesWeblinks}}" diff="{{_diff}}" change-num="[[_changeNum]]" commit-range="[[_commitRange]]" patch-range="[[_patchRange]]" path="[[_path]]" prefs="[[_prefs]]" project-name="[[_change.project]]" view-mode="[[_diffMode]]" is-blame-loaded="{{_isBlameLoaded}}" on-comment-anchor-tap="_onLineSelected" on-line-selected="_onLineSelected">
</gr-diff-host>
- <gr-apply-fix-dialog
- id="applyFixDialog"
- prefs="[[_prefs]]"
- change="[[_change]]"
- change-num="[[_changeNum]]">
+ <gr-apply-fix-dialog id="applyFixDialog" prefs="[[_prefs]]" change="[[_change]]" change-num="[[_changeNum]]">
</gr-apply-fix-dialog>
- <gr-diff-preferences-dialog
- id="diffPreferencesDialog"
- diff-prefs="{{_prefs}}"
- on-reload-diff-preference="_handleReloadingDiffPreference">
+ <gr-diff-preferences-dialog id="diffPreferencesDialog" diff-prefs="{{_prefs}}" on-reload-diff-preference="_handleReloadingDiffPreference">
</gr-diff-preferences-dialog>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-storage id="storage"></gr-storage>
<gr-diff-cursor id="cursor" scroll-top-margin="[[_scrollTopMargin]]"></gr-diff-cursor>
<gr-comment-api id="commentAPI"></gr-comment-api>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-diff-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index a992a6e..35aa664 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_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-diff-view</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="../../../scripts/util.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>
+<script type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-diff-view.html">
+<script type="module" src="./gr-diff-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-diff-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -44,102 +50,533 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-view tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-diff-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff-view tests', () => {
+ suite('basic tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+ kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+ kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+ kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+ kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+ kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
+ kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
+ kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
+ kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+ kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+ kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+ kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+ kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
+ kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+ kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
+ kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
- suite('basic tests', async () => {
- const kb = window.Gerrit.KeyboardShortcutBinder;
- kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
- kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
- kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
- kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
- kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
- kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
- kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
- kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
- kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
- kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
- kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
- kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
- kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
- kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
- kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
- kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
- kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
- kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
- kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
+ let element;
+ let sandbox;
- let element;
- let sandbox;
+ const PARENT = 'PARENT';
- const PARENT = 'PARENT';
+ function getFilesFromFileList(fileList) {
+ const changeFilesByPath = fileList.reduce((files, path) => {
+ files[path] = {};
+ return files;
+ }, {});
+ return {
+ sortedFileList: fileList,
+ changeFilesByPath,
+ };
+ }
- function getFilesFromFileList(fileList) {
- const changeFilesByPath = fileList.reduce((files, path) => {
- files[path] = {};
- return files;
- }, {});
- return {
- sortedFileList: fileList,
- changeFilesByPath,
- };
- }
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({change: {}}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getProjectConfig() { return Promise.resolve({}); },
+ getDiffChangeDetail() { return Promise.resolve({}); },
+ getChangeFiles() { return Promise.resolve({}); },
+ saveFileReviewed() { return Promise.resolve(); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ getReviewedFiles() { return Promise.resolve([]); },
+ });
+ element = fixture('basic');
+ return element._loadComments();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('params change triggers diffViewDisplayed()', () => {
+ sandbox.stub(element.$.reporting, 'diffViewDisplayed');
+ sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
+ sandbox.spy(element, '_paramsChanged');
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+
+ return element._paramsChanged.returnValues[0].then(() => {
+ assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce);
+ });
+ });
+
+ test('toggle left diff with a hotkey', () => {
+ const toggleLeftDiffStub = sandbox.stub(
+ element.$.diffHost, 'toggleLeftDiff');
+ MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
+ assert.isTrue(toggleLeftDiffStub.calledOnce);
+ });
+
+ test('keyboard shortcuts', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '10',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 10, commit: {parents: []}},
+ },
+ };
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md']);
+ element._path = 'glados.txt';
+ element.changeViewState.selectedFileIndex = 1;
+ element._loggedIn = true;
+
+ const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert(changeNavStub.lastCall.calledWith(element._change),
+ 'Should navigate to /c/42/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
+ assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md',
+ '10', PARENT), 'Should navigate to /c/42/10/wheatley.md');
+ element._path = 'wheatley.md';
+ assert.equal(element.changeViewState.selectedFileIndex, 2);
+ assert.isTrue(element._loading);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt',
+ '10', PARENT), 'Should navigate to /c/42/10/glados.txt');
+ element._path = 'glados.txt';
+ assert.equal(element.changeViewState.selectedFileIndex, 1);
+ assert.isTrue(element._loading);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', '10',
+ PARENT), 'Should navigate to /c/42/10/chell.go');
+ element._path = 'chell.go';
+ assert.equal(element.changeViewState.selectedFileIndex, 0);
+ assert.isTrue(element._loading);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(changeNavStub.lastCall.calledWith(element._change),
+ 'Should navigate to /c/42/');
+ assert.equal(element.changeViewState.selectedFileIndex, 0);
+ assert.isTrue(element._loading);
+
+ const showPrefsStub =
+ sandbox.stub(element.$.diffPreferencesDialog, 'open',
+ () => Promise.resolve());
+
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert(showPrefsStub.calledOnce);
+
+ element.disableDiffPrefs = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert(showPrefsStub.calledOnce);
+
+ let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk');
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ assert(scrollStub.calledOnce);
+
+ scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk');
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+ assert(scrollStub.calledOnce);
+
+ scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread');
+ MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
+ assert(scrollStub.calledOnce);
+
+ scrollStub = sandbox.stub(element.$.cursor,
+ 'moveToPreviousCommentThread');
+ MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
+ assert(scrollStub.calledOnce);
+
+ const computeContainerClassStub = sandbox.stub(element.$.diffHost.$.diff,
+ '_computeContainerClass');
+ MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
+ assert(computeContainerClassStub.lastCall.calledWithExactly(
+ false, 'SIDE_BY_SIDE', true));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
+ assert(computeContainerClassStub.lastCall.calledWithExactly(
+ false, 'SIDE_BY_SIDE', false));
+
+ sandbox.stub(element, '_setReviewed');
+ element.$.reviewed.checked = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ assert.isFalse(element._setReviewed.called);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.isTrue(element._setReviewed.called);
+ assert.equal(element._setReviewed.lastCall.args[0], true);
+ });
+
+ test('shift+x shortcut expands all diff context', () => {
+ const expandStub = sandbox.stub(element.$.diffHost, 'expandAllContext');
+ MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x');
+ flushAsynchronousOperations();
+ assert.isTrue(expandStub.called);
+ });
+
+ test('keyboard shortcuts with patch range', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: '5',
+ patchNum: '10',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 10, commit: {parents: []}},
+ b: {_number: 5, commit: {parents: []}},
+ },
+ };
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md']);
+ element._path = 'glados.txt';
+
+ const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
+ 'should only work when the user is logged in.');
+ assert.isNull(window.sessionStorage.getItem(
+ 'changeView.showReplyDialog'));
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(element.changeViewState.showReplyDialog);
+
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
+ '5'), 'Should navigate to /c/42/5..10');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
+ '5'), 'Should navigate to /c/42/5..10');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
+ assert.isTrue(element._loading);
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'wheatley.md', '10', '5'),
+ 'Should navigate to /c/42/5..10/wheatley.md');
+ element._path = 'wheatley.md';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert.isTrue(element._loading);
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'glados.txt', '10', '5'),
+ 'Should navigate to /c/42/5..10/glados.txt');
+ element._path = 'glados.txt';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert.isTrue(element._loading);
+ assert(diffNavStub.lastCall.calledWithExactly(
+ element._change,
+ 'chell.go',
+ '10',
+ '5'),
+ 'Should navigate to /c/42/5..10/chell.go');
+ element._path = 'chell.go';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert.isTrue(element._loading);
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
+ '5'),
+ 'Should navigate to /c/42/5..10');
+ });
+
+ test('keyboard shortcuts with old patch number', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '1',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 1, commit: {parents: []}},
+ b: {_number: 2, commit: {parents: []}},
+ },
+ };
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md']);
+ element._path = 'glados.txt';
+
+ const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
+ 'should only work when the user is logged in.');
+ assert.isNull(window.sessionStorage.getItem(
+ 'changeView.showReplyDialog'));
+
+ element._loggedIn = true;
+ MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+ assert.isTrue(element.changeViewState.showReplyDialog);
+
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
+ PARENT), 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
+ PARENT), 'Should navigate to /c/42/1');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'wheatley.md', '1', PARENT),
+ 'Should navigate to /c/42/1/wheatley.md');
+ element._path = 'wheatley.md';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWithExactly(element._change,
+ 'glados.txt', '1', PARENT),
+ 'Should navigate to /c/42/1/glados.txt');
+ element._path = 'glados.txt';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(diffNavStub.lastCall.calledWithExactly(
+ element._change,
+ 'chell.go',
+ '1',
+ PARENT), 'Should navigate to /c/42/1/chell.go');
+ element._path = 'chell.go';
+
+ MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
+ PARENT), 'Should navigate to /c/42/1');
+ });
+
+ test('edit should redirect to edit page', done => {
+ element._loggedIn = true;
+ element._path = 't.txt';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '1',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 1, commit: {parents: []}},
+ b: {_number: 2, commit: {parents: []}},
+ },
+ };
+ const redirectStub = sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+ flush(() => {
+ const editBtn = element.shadowRoot
+ .querySelector('.editButton gr-button');
+ assert.isTrue(!!editBtn);
+ MockInteractions.tap(editBtn);
+ assert.isTrue(redirectStub.called);
+ done();
+ });
+ });
+
+ test('edit hidden when not logged in', done => {
+ element._loggedIn = false;
+ element._path = 't.txt';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '1',
+ };
+ element._change = {
+ _number: 42,
+ revisions: {
+ a: {_number: 1, commit: {parents: []}},
+ b: {_number: 2, commit: {parents: []}},
+ },
+ };
+ flush(() => {
+ const editBtn = element.shadowRoot
+ .querySelector('.editButton gr-button');
+ assert.isFalse(!!editBtn);
+ done();
+ });
+ });
+
+ suite('diff prefs hidden', () => {
+ test('when no prefs or logged out', () => {
+ element.disableDiffPrefs = false;
+ element._loggedIn = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = false;
+ element._prefs = {font_size: '12'};
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ });
+
+ test('when disableDiffPrefs is set', () => {
+ element._loggedIn = true;
+ element._prefs = {font_size: '12'};
+ element.disableDiffPrefs = false;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ element.disableDiffPrefs = true;
+ flushAsynchronousOperations();
+
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+ });
+ });
+
+ test('prefsButton opens gr-diff-preferences', () => {
+ const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
+ const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog,
+ 'open');
+ const prefsButton =
+ dom(element.root).querySelector('.prefsButton');
+
+ MockInteractions.tap(prefsButton);
+
+ assert.isTrue(handlePrefsTapSpy.called);
+ assert.isTrue(overlayOpenStub.called);
+ });
+
+ test('_computeCommentString', done => {
+ const path = '/test';
+ element.$.commentAPI.loadAll().then(comments => {
+ const commentCountStub =
+ sandbox.stub(comments, 'computeCommentCount');
+ const unresolvedCountStub =
+ sandbox.stub(comments, 'computeUnresolvedNum');
+ commentCountStub.withArgs(1, path).returns(0);
+ commentCountStub.withArgs(2, path).returns(1);
+ commentCountStub.withArgs(3, path).returns(2);
+ commentCountStub.withArgs(4, path).returns(0);
+ unresolvedCountStub.withArgs(1, path).returns(1);
+ unresolvedCountStub.withArgs(2, path).returns(0);
+ unresolvedCountStub.withArgs(3, path).returns(2);
+ unresolvedCountStub.withArgs(4, path).returns(0);
+
+ assert.equal(element._computeCommentString(comments, 1, path, {}),
+ '1 unresolved');
+ assert.equal(
+ element._computeCommentString(comments, 2, path, {status: 'M'}),
+ '1 comment');
+ assert.equal(
+ element._computeCommentString(comments, 2, path, {status: 'U'}),
+ 'no changes, 1 comment');
+ assert.equal(
+ element._computeCommentString(comments, 3, path, {status: 'A'}),
+ '2 comments, 2 unresolved');
+ assert.equal(
+ element._computeCommentString(
+ comments, 4, path, {status: 'M'}
+ ), '');
+ assert.equal(
+ element._computeCommentString(comments, 4, path, {status: 'U'}),
+ 'no changes');
+ done();
+ });
+ });
+
+ suite('url params', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
-
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({change: {}}); },
- getLoggedIn() { return Promise.resolve(false); },
- getProjectConfig() { return Promise.resolve({}); },
- getDiffChangeDetail() { return Promise.resolve({}); },
- getChangeFiles() { return Promise.resolve({}); },
- saveFileReviewed() { return Promise.resolve(); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- getReviewedFiles() { return Promise.resolve([]); },
- });
- element = fixture('basic');
- return element._loadComments();
+ sandbox.stub(
+ Gerrit.Nav,
+ 'getUrlForDiff',
+ (c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
+ sandbox.stub(
+ Gerrit.Nav
+ , 'getUrlForChange',
+ (c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
});
- teardown(() => {
- sandbox.restore();
- });
-
- test('params change triggers diffViewDisplayed()', () => {
- sandbox.stub(element.$.reporting, 'diffViewDisplayed');
- sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
- sandbox.spy(element, '_paramsChanged');
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
- path: '/COMMIT_MSG',
+ test('_formattedFiles', () => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: PARENT,
+ patchNum: '10',
};
+ element._change = {_number: 42};
+ element._files = getFilesFromFileList(
+ ['chell.go', 'glados.txt', 'wheatley.md',
+ '/COMMIT_MSG', '/MERGE_LIST']);
+ element._path = 'glados.txt';
+ const expectedFormattedFiles = [
+ {
+ text: 'chell.go',
+ mobileText: 'chell.go',
+ value: 'chell.go',
+ bottomText: '',
+ }, {
+ text: 'glados.txt',
+ mobileText: 'glados.txt',
+ value: 'glados.txt',
+ bottomText: '',
+ }, {
+ text: 'wheatley.md',
+ mobileText: 'wheatley.md',
+ value: 'wheatley.md',
+ bottomText: '',
+ },
+ {
+ text: 'Commit message',
+ mobileText: 'Commit message',
+ value: '/COMMIT_MSG',
+ bottomText: '',
+ },
+ {
+ text: 'Merge list',
+ mobileText: 'Merge list',
+ value: '/MERGE_LIST',
+ bottomText: '',
+ },
+ ];
- return element._paramsChanged.returnValues[0].then(() => {
- assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce);
- });
+ assert.deepEqual(element._formattedFiles, expectedFormattedFiles);
+ assert.equal(element._formattedFiles[1].value, element._path);
});
- test('toggle left diff with a hotkey', () => {
- const toggleLeftDiffStub = sandbox.stub(
- element.$.diffHost, 'toggleLeftDiff');
- MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
- assert.isTrue(toggleLeftDiffStub.calledOnce);
- });
-
- test('keyboard shortcuts', () => {
+ test('prev/up/next links', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: PARENT,
@@ -154,99 +591,34 @@
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
- element.changeViewState.selectedFileIndex = 1;
- element._loggedIn = true;
-
- const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWith(element._change),
- 'Should navigate to /c/42/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
- assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md',
- '10', PARENT), 'Should navigate to /c/42/10/wheatley.md');
- element._path = 'wheatley.md';
- assert.equal(element.changeViewState.selectedFileIndex, 2);
- assert.isTrue(element._loading);
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt',
- '10', PARENT), 'Should navigate to /c/42/10/glados.txt');
- element._path = 'glados.txt';
- assert.equal(element.changeViewState.selectedFileIndex, 1);
- assert.isTrue(element._loading);
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', '10',
- PARENT), 'Should navigate to /c/42/10/chell.go');
- element._path = 'chell.go';
- assert.equal(element.changeViewState.selectedFileIndex, 0);
- assert.isTrue(element._loading);
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(changeNavStub.lastCall.calledWith(element._change),
- 'Should navigate to /c/42/');
- assert.equal(element.changeViewState.selectedFileIndex, 0);
- assert.isTrue(element._loading);
-
- const showPrefsStub =
- sandbox.stub(element.$.diffPreferencesDialog, 'open',
- () => Promise.resolve());
-
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert(showPrefsStub.calledOnce);
-
- element.disableDiffPrefs = true;
- MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
- assert(showPrefsStub.calledOnce);
-
- let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk');
- MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
- assert(scrollStub.calledOnce);
-
- scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk');
- MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
- assert(scrollStub.calledOnce);
-
- scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread');
- MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
- assert(scrollStub.calledOnce);
-
- scrollStub = sandbox.stub(element.$.cursor,
- 'moveToPreviousCommentThread');
- MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
- assert(scrollStub.calledOnce);
-
- const computeContainerClassStub = sandbox.stub(element.$.diffHost.$.diff,
- '_computeContainerClass');
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', true));
-
- MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
- assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', false));
-
- sandbox.stub(element, '_setReviewed');
- element.$.reviewed.checked = false;
- MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
- assert.isFalse(element._setReviewed.called);
-
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.isTrue(element._setReviewed.called);
- assert.equal(element._setReviewed.lastCall.args[0], true);
- });
-
- test('shift+x shortcut expands all diff context', () => {
- const expandStub = sandbox.stub(element.$.diffHost, 'expandAllContext');
- MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x');
flushAsynchronousOperations();
- assert.isTrue(expandStub.called);
+ const linkEls = dom(element.root).querySelectorAll('.navLink');
+ assert.equal(linkEls.length, 3);
+ assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT');
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.equal(linkEls[2].getAttribute('href'),
+ '42-wheatley.md-10-PARENT');
+ element._path = 'wheatley.md';
+ flushAsynchronousOperations();
+ assert.equal(linkEls[0].getAttribute('href'),
+ '42-glados.txt-10-PARENT');
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.isFalse(linkEls[2].hasAttribute('href'));
+ element._path = 'chell.go';
+ flushAsynchronousOperations();
+ assert.isFalse(linkEls[0].hasAttribute('href'));
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.equal(linkEls[2].getAttribute('href'),
+ '42-glados.txt-10-PARENT');
+ element._path = 'not_a_real_file';
+ flushAsynchronousOperations();
+ assert.equal(linkEls[0].getAttribute('href'),
+ '42-wheatley.md-10-PARENT');
+ assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
+ assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT');
});
- test('keyboard shortcuts with patch range', () => {
+ test('prev/up/next links with patch range', () => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: '5',
@@ -255,1168 +627,805 @@
element._change = {
_number: 42,
revisions: {
- a: {_number: 10, commit: {parents: []}},
- b: {_number: 5, commit: {parents: []}},
+ a: {_number: 5, commit: {parents: []}},
+ b: {_number: 10, commit: {parents: []}},
},
};
element._files = getFilesFromFileList(
['chell.go', 'glados.txt', 'wheatley.md']);
element._path = 'glados.txt';
-
- const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
- 'should only work when the user is logged in.');
- assert.isNull(window.sessionStorage.getItem(
- 'changeView.showReplyDialog'));
-
- element._loggedIn = true;
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(element.changeViewState.showReplyDialog);
-
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
- '5'), 'Should navigate to /c/42/5..10');
-
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
- '5'), 'Should navigate to /c/42/5..10');
-
- MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
- assert.isTrue(element._loading);
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'wheatley.md', '10', '5'),
- 'Should navigate to /c/42/5..10/wheatley.md');
+ flushAsynchronousOperations();
+ const linkEls = dom(element.root).querySelectorAll('.navLink');
+ assert.equal(linkEls.length, 3);
+ assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5');
+ assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
+ assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5');
element._path = 'wheatley.md';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert.isTrue(element._loading);
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'glados.txt', '10', '5'),
- 'Should navigate to /c/42/5..10/glados.txt');
- element._path = 'glados.txt';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert.isTrue(element._loading);
- assert(diffNavStub.lastCall.calledWithExactly(
- element._change,
- 'chell.go',
- '10',
- '5'),
- 'Should navigate to /c/42/5..10/chell.go');
+ flushAsynchronousOperations();
+ assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5');
+ assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
+ assert.isFalse(linkEls[2].hasAttribute('href'));
element._path = 'chell.go';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert.isTrue(element._loading);
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '10',
- '5'),
- 'Should navigate to /c/42/5..10');
- });
-
- test('keyboard shortcuts with old patch number', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '1',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 1, commit: {parents: []}},
- b: {_number: 2, commit: {parents: []}},
- },
- };
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md']);
- element._path = 'glados.txt';
-
- const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
-
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
- 'should only work when the user is logged in.');
- assert.isNull(window.sessionStorage.getItem(
- 'changeView.showReplyDialog'));
-
- element._loggedIn = true;
- MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(element.changeViewState.showReplyDialog);
-
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
- PARENT), 'Should navigate to /c/42/1');
-
- MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
- PARENT), 'Should navigate to /c/42/1');
-
- MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'wheatley.md', '1', PARENT),
- 'Should navigate to /c/42/1/wheatley.md');
- element._path = 'wheatley.md';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWithExactly(element._change,
- 'glados.txt', '1', PARENT),
- 'Should navigate to /c/42/1/glados.txt');
- element._path = 'glados.txt';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(diffNavStub.lastCall.calledWithExactly(
- element._change,
- 'chell.go',
- '1',
- PARENT), 'Should navigate to /c/42/1/chell.go');
- element._path = 'chell.go';
-
- MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, '1',
- PARENT), 'Should navigate to /c/42/1');
- });
-
- test('edit should redirect to edit page', done => {
- element._loggedIn = true;
- element._path = 't.txt';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '1',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 1, commit: {parents: []}},
- b: {_number: 2, commit: {parents: []}},
- },
- };
- const redirectStub = sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
- flush(() => {
- const editBtn = element.shadowRoot
- .querySelector('.editButton gr-button');
- assert.isTrue(!!editBtn);
- MockInteractions.tap(editBtn);
- assert.isTrue(redirectStub.called);
- done();
- });
- });
-
- test('edit hidden when not logged in', done => {
- element._loggedIn = false;
- element._path = 't.txt';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '1',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 1, commit: {parents: []}},
- b: {_number: 2, commit: {parents: []}},
- },
- };
- flush(() => {
- const editBtn = element.shadowRoot
- .querySelector('.editButton gr-button');
- assert.isFalse(!!editBtn);
- done();
- });
- });
-
- suite('diff prefs hidden', () => {
- test('when no prefs or logged out', () => {
- element.disableDiffPrefs = false;
- element._loggedIn = false;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element._loggedIn = true;
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element._loggedIn = false;
- element._prefs = {font_size: '12'};
- flushAsynchronousOperations();
- assert.isTrue(element.$.diffPrefsContainer.hidden);
-
- element._loggedIn = true;
- flushAsynchronousOperations();
- assert.isFalse(element.$.diffPrefsContainer.hidden);
- });
-
- test('when disableDiffPrefs is set', () => {
- element._loggedIn = true;
- element._prefs = {font_size: '12'};
- element.disableDiffPrefs = false;
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.diffPrefsContainer.hidden);
- element.disableDiffPrefs = true;
- flushAsynchronousOperations();
-
- assert.isTrue(element.$.diffPrefsContainer.hidden);
- });
- });
-
- test('prefsButton opens gr-diff-preferences', () => {
- const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
- const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog,
- 'open');
- const prefsButton =
- Polymer.dom(element.root).querySelector('.prefsButton');
-
- MockInteractions.tap(prefsButton);
-
- assert.isTrue(handlePrefsTapSpy.called);
- assert.isTrue(overlayOpenStub.called);
- });
-
- test('_computeCommentString', done => {
- const path = '/test';
- element.$.commentAPI.loadAll().then(comments => {
- const commentCountStub =
- sandbox.stub(comments, 'computeCommentCount');
- const unresolvedCountStub =
- sandbox.stub(comments, 'computeUnresolvedNum');
- commentCountStub.withArgs(1, path).returns(0);
- commentCountStub.withArgs(2, path).returns(1);
- commentCountStub.withArgs(3, path).returns(2);
- commentCountStub.withArgs(4, path).returns(0);
- unresolvedCountStub.withArgs(1, path).returns(1);
- unresolvedCountStub.withArgs(2, path).returns(0);
- unresolvedCountStub.withArgs(3, path).returns(2);
- unresolvedCountStub.withArgs(4, path).returns(0);
-
- assert.equal(element._computeCommentString(comments, 1, path, {}),
- '1 unresolved');
- assert.equal(
- element._computeCommentString(comments, 2, path, {status: 'M'}),
- '1 comment');
- assert.equal(
- element._computeCommentString(comments, 2, path, {status: 'U'}),
- 'no changes, 1 comment');
- assert.equal(
- element._computeCommentString(comments, 3, path, {status: 'A'}),
- '2 comments, 2 unresolved');
- assert.equal(
- element._computeCommentString(
- comments, 4, path, {status: 'M'}
- ), '');
- assert.equal(
- element._computeCommentString(comments, 4, path, {status: 'U'}),
- 'no changes');
- done();
- });
- });
-
- suite('url params', () => {
- setup(() => {
- sandbox.stub(
- Gerrit.Nav,
- 'getUrlForDiff',
- (c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`);
- sandbox.stub(
- Gerrit.Nav
- , 'getUrlForChange',
- (c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
- });
-
- test('_formattedFiles', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '10',
- };
- element._change = {_number: 42};
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md',
- '/COMMIT_MSG', '/MERGE_LIST']);
- element._path = 'glados.txt';
- const expectedFormattedFiles = [
- {
- text: 'chell.go',
- mobileText: 'chell.go',
- value: 'chell.go',
- bottomText: '',
- }, {
- text: 'glados.txt',
- mobileText: 'glados.txt',
- value: 'glados.txt',
- bottomText: '',
- }, {
- text: 'wheatley.md',
- mobileText: 'wheatley.md',
- value: 'wheatley.md',
- bottomText: '',
- },
- {
- text: 'Commit message',
- mobileText: 'Commit message',
- value: '/COMMIT_MSG',
- bottomText: '',
- },
- {
- text: 'Merge list',
- mobileText: 'Merge list',
- value: '/MERGE_LIST',
- bottomText: '',
- },
- ];
-
- assert.deepEqual(element._formattedFiles, expectedFormattedFiles);
- assert.equal(element._formattedFiles[1].value, element._path);
- });
-
- test('prev/up/next links', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: PARENT,
- patchNum: '10',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 10, commit: {parents: []}},
- },
- };
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md']);
- element._path = 'glados.txt';
- flushAsynchronousOperations();
- const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
- assert.equal(linkEls.length, 3);
- assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT');
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.equal(linkEls[2].getAttribute('href'),
- '42-wheatley.md-10-PARENT');
- element._path = 'wheatley.md';
- flushAsynchronousOperations();
- assert.equal(linkEls[0].getAttribute('href'),
- '42-glados.txt-10-PARENT');
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.isFalse(linkEls[2].hasAttribute('href'));
- element._path = 'chell.go';
- flushAsynchronousOperations();
- assert.isFalse(linkEls[0].hasAttribute('href'));
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.equal(linkEls[2].getAttribute('href'),
- '42-glados.txt-10-PARENT');
- element._path = 'not_a_real_file';
- flushAsynchronousOperations();
- assert.equal(linkEls[0].getAttribute('href'),
- '42-wheatley.md-10-PARENT');
- assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined');
- assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT');
- });
-
- test('prev/up/next links with patch range', () => {
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: '5',
- patchNum: '10',
- };
- element._change = {
- _number: 42,
- revisions: {
- a: {_number: 5, commit: {parents: []}},
- b: {_number: 10, commit: {parents: []}},
- },
- };
- element._files = getFilesFromFileList(
- ['chell.go', 'glados.txt', 'wheatley.md']);
- element._path = 'glados.txt';
- flushAsynchronousOperations();
- const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
- assert.equal(linkEls.length, 3);
- assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5');
- assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
- assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5');
- element._path = 'wheatley.md';
- flushAsynchronousOperations();
- assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5');
- assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
- assert.isFalse(linkEls[2].hasAttribute('href'));
- element._path = 'chell.go';
- flushAsynchronousOperations();
- assert.isFalse(linkEls[0].hasAttribute('href'));
- assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
- assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5');
- });
- });
-
- test('_handlePatchChange calls navigateToDiff correctly', () => {
- const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- element._change = {_number: 321, project: 'foo/bar'};
- element._path = 'path/to/file.txt';
-
- element._patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '3',
- };
-
- const detail = {
- basePatchNum: 'PARENT',
- patchNum: '1',
- };
-
- element.$.rangeSelect.dispatchEvent(
- new CustomEvent('patch-range-change', {detail, bubbles: false}));
-
- assert(navigateStub.lastCall.calledWithExactly(element._change,
- element._path, '1', 'PARENT'));
- });
-
- test('_prefs.manual_review is respected', () => {
- const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
- () => Promise.resolve());
- const getReviewedStub = sandbox.stub(element, '_getReviewedStatus',
- () => Promise.resolve());
-
- sandbox.stub(element.$.diffHost, 'reload');
- element._loggedIn = true;
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
- path: '/COMMIT_MSG',
- };
- element._prefs = {manual_review: true};
flushAsynchronousOperations();
-
- assert.isFalse(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.called);
-
- element._prefs = {};
- flushAsynchronousOperations();
-
- assert.isTrue(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.calledOnce);
+ assert.isFalse(linkEls[0].hasAttribute('href'));
+ assert.equal(linkEls[1].getAttribute('href'), '42-10-5');
+ assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5');
});
+ });
- test('file review status', () => {
- const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
- () => Promise.resolve());
- sandbox.stub(element.$.diffHost, 'reload');
+ test('_handlePatchChange calls navigateToDiff correctly', () => {
+ const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ element._change = {_number: 321, project: 'foo/bar'};
+ element._path = 'path/to/file.txt';
- element._loggedIn = true;
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
- path: '/COMMIT_MSG',
- };
- element._prefs = {};
- flushAsynchronousOperations();
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
- const commitMsg = Polymer.dom(element.root).querySelector(
- 'input[type="checkbox"]');
+ const detail = {
+ basePatchNum: 'PARENT',
+ patchNum: '1',
+ };
- assert.isTrue(commitMsg.checked);
- MockInteractions.tap(commitMsg);
- assert.isFalse(commitMsg.checked);
- assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false));
+ element.$.rangeSelect.dispatchEvent(
+ new CustomEvent('patch-range-change', {detail, bubbles: false}));
- MockInteractions.tap(commitMsg);
- assert.isTrue(commitMsg.checked);
- assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true));
- const callCount = saveReviewedStub.callCount;
+ assert(navigateStub.lastCall.calledWithExactly(element._change,
+ element._path, '1', 'PARENT'));
+ });
- element.set('params.view', Gerrit.Nav.View.CHANGE);
- flushAsynchronousOperations();
+ test('_prefs.manual_review is respected', () => {
+ const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
+ () => Promise.resolve());
+ const getReviewedStub = sandbox.stub(element, '_getReviewedStatus',
+ () => Promise.resolve());
- // saveReviewedState observer observes params, but should not fire when
- // view !== Gerrit.Nav.View.DIFF.
- assert.equal(saveReviewedStub.callCount, callCount);
+ sandbox.stub(element.$.diffHost, 'reload');
+ element._loggedIn = true;
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+ element._prefs = {manual_review: true};
+ flushAsynchronousOperations();
+
+ assert.isFalse(saveReviewedStub.called);
+ assert.isTrue(getReviewedStub.called);
+
+ element._prefs = {};
+ flushAsynchronousOperations();
+
+ assert.isTrue(saveReviewedStub.called);
+ assert.isTrue(getReviewedStub.calledOnce);
+ });
+
+ test('file review status', () => {
+ const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
+ () => Promise.resolve());
+ sandbox.stub(element.$.diffHost, 'reload');
+
+ element._loggedIn = true;
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+ element._prefs = {};
+ flushAsynchronousOperations();
+
+ const commitMsg = dom(element.root).querySelector(
+ 'input[type="checkbox"]');
+
+ assert.isTrue(commitMsg.checked);
+ MockInteractions.tap(commitMsg);
+ assert.isFalse(commitMsg.checked);
+ assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false));
+
+ MockInteractions.tap(commitMsg);
+ assert.isTrue(commitMsg.checked);
+ assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true));
+ const callCount = saveReviewedStub.callCount;
+
+ element.set('params.view', Gerrit.Nav.View.CHANGE);
+ flushAsynchronousOperations();
+
+ // saveReviewedState observer observes params, but should not fire when
+ // view !== Gerrit.Nav.View.DIFF.
+ assert.equal(saveReviewedStub.callCount, callCount);
+ });
+
+ test('file review status with edit loaded', () => {
+ const saveReviewedStub = sandbox.stub(element, '_saveReviewedState');
+
+ element._patchRange = {patchNum: element.EDIT_NAME};
+ flushAsynchronousOperations();
+
+ assert.isTrue(element._editMode);
+ element._setReviewed();
+ assert.isFalse(saveReviewedStub.called);
+ });
+
+ test('hash is determined from params', done => {
+ sandbox.stub(element.$.diffHost, 'reload');
+ sandbox.stub(element, '_initCursor');
+
+ element._loggedIn = true;
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ hash: 10,
+ };
+
+ flush(() => {
+ assert.isTrue(element._initCursor.calledOnce);
+ done();
});
+ });
- test('file review status with edit loaded', () => {
- const saveReviewedStub = sandbox.stub(element, '_saveReviewedState');
+ test('diff mode selector correctly toggles the diff', () => {
+ const select = element.$.modeSelect;
+ const diffDisplay = element.$.diffHost;
+ element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
- element._patchRange = {patchNum: element.EDIT_NAME};
- flushAsynchronousOperations();
+ // The mode selected in the view state reflects the selected option.
+ assert.equal(element._getDiffViewMode(), select.mode);
- assert.isTrue(element._editMode);
- element._setReviewed();
- assert.isFalse(saveReviewedStub.called);
+ // The mode selected in the view state reflects the view rednered in the
+ // diff.
+ assert.equal(select.mode, diffDisplay.viewMode);
+
+ // We will simulate a user change of the selected mode.
+ const newMode = 'UNIFIED_DIFF';
+
+ // Set the mode, and simulate the change event.
+ element.set('changeViewState.diffMode', newMode);
+
+ // Make sure the handler was called and the state is still coherent.
+ assert.equal(element._getDiffViewMode(), newMode);
+ assert.equal(element._getDiffViewMode(), select.mode);
+ assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
+ });
+
+ test('diff mode selector initializes from preferences', () => {
+ let resolvePrefs;
+ const prefsPromise = new Promise(resolve => {
+ resolvePrefs = resolve;
});
+ sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise);
- test('hash is determined from params', done => {
+ // Attach a new gr-diff-view so we can intercept the preferences fetch.
+ const view = document.createElement('gr-diff-view');
+ fixture('blank').appendChild(view);
+ flushAsynchronousOperations();
+
+ // At this point the diff mode doesn't yet have the user's preference.
+ assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
+
+ // Receive the overriding preference.
+ resolvePrefs({default_diff_view: 'UNIFIED'});
+ flushAsynchronousOperations();
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ });
+
+ suite('_commitRange', () => {
+ setup(() => {
sandbox.stub(element.$.diffHost, 'reload');
sandbox.stub(element, '_initCursor');
+ sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve({
+ _number: 42,
+ revisions: {
+ 'commit-sha-1': {
+ _number: 1,
+ commit: {
+ parents: [{commit: 'sha-1-parent'}],
+ },
+ },
+ 'commit-sha-2': {_number: 2},
+ 'commit-sha-3': {_number: 3},
+ 'commit-sha-4': {_number: 4},
+ 'commit-sha-5': {
+ _number: 5,
+ commit: {
+ parents: [{commit: 'sha-5-parent'}],
+ },
+ },
+ },
+ }));
+ });
- element._loggedIn = true;
+ test('uses the patchNum and basePatchNum ', done => {
element.params = {
view: Gerrit.Nav.View.DIFF,
changeNum: '42',
- patchNum: '2',
- basePatchNum: '1',
+ patchNum: '4',
+ basePatchNum: '2',
path: '/COMMIT_MSG',
- hash: 10,
};
-
flush(() => {
- assert.isTrue(element._initCursor.calledOnce);
+ assert.deepEqual(element._commitRange, {
+ baseCommit: 'commit-sha-2',
+ commit: 'commit-sha-4',
+ });
done();
});
});
- test('diff mode selector correctly toggles the diff', () => {
- const select = element.$.modeSelect;
- const diffDisplay = element.$.diffHost;
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
-
- // The mode selected in the view state reflects the selected option.
- assert.equal(element._getDiffViewMode(), select.mode);
-
- // The mode selected in the view state reflects the view rednered in the
- // diff.
- assert.equal(select.mode, diffDisplay.viewMode);
-
- // We will simulate a user change of the selected mode.
- const newMode = 'UNIFIED_DIFF';
-
- // Set the mode, and simulate the change event.
- element.set('changeViewState.diffMode', newMode);
-
- // Make sure the handler was called and the state is still coherent.
- assert.equal(element._getDiffViewMode(), newMode);
- assert.equal(element._getDiffViewMode(), select.mode);
- assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
- });
-
- test('diff mode selector initializes from preferences', () => {
- let resolvePrefs;
- const prefsPromise = new Promise(resolve => {
- resolvePrefs = resolve;
- });
- sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise);
-
- // Attach a new gr-diff-view so we can intercept the preferences fetch.
- const view = document.createElement('gr-diff-view');
- fixture('blank').appendChild(view);
- flushAsynchronousOperations();
-
- // At this point the diff mode doesn't yet have the user's preference.
- assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // Receive the overriding preference.
- resolvePrefs({default_diff_view: 'UNIFIED'});
- flushAsynchronousOperations();
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
- suite('_commitRange', () => {
- setup(() => {
- sandbox.stub(element.$.diffHost, 'reload');
- sandbox.stub(element, '_initCursor');
- sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve({
- _number: 42,
- revisions: {
- 'commit-sha-1': {
- _number: 1,
- commit: {
- parents: [{commit: 'sha-1-parent'}],
- },
- },
- 'commit-sha-2': {_number: 2},
- 'commit-sha-3': {_number: 3},
- 'commit-sha-4': {_number: 4},
- 'commit-sha-5': {
- _number: 5,
- commit: {
- parents: [{commit: 'sha-5-parent'}],
- },
- },
- },
- }));
- });
-
- test('uses the patchNum and basePatchNum ', done => {
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '4',
- basePatchNum: '2',
- path: '/COMMIT_MSG',
- };
- flush(() => {
- assert.deepEqual(element._commitRange, {
- baseCommit: 'commit-sha-2',
- commit: 'commit-sha-4',
- });
- done();
+ test('uses the parent when there is no base patch num ', done => {
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '5',
+ path: '/COMMIT_MSG',
+ };
+ flush(() => {
+ assert.deepEqual(element._commitRange, {
+ commit: 'commit-sha-5',
+ baseCommit: 'sha-5-parent',
});
+ done();
});
+ });
+ });
- test('uses the parent when there is no base patch num ', done => {
- element.params = {
- view: Gerrit.Nav.View.DIFF,
- changeNum: '42',
- patchNum: '5',
- path: '/COMMIT_MSG',
- };
- flush(() => {
- assert.deepEqual(element._commitRange, {
- commit: 'commit-sha-5',
- baseCommit: 'sha-5-parent',
- });
- done();
- });
+ test('_initCursor', () => {
+ assert.isNotOk(element.$.cursor.initialLineNumber);
+
+ // Does nothing when params specify no cursor address:
+ element._initCursor({});
+ assert.isNotOk(element.$.cursor.initialLineNumber);
+
+ // Does nothing when params specify side but no number:
+ element._initCursor({leftSide: true});
+ assert.isNotOk(element.$.cursor.initialLineNumber);
+
+ // Revision hash: specifies lineNum but not side.
+ element._initCursor({lineNum: 234});
+ assert.equal(element.$.cursor.initialLineNumber, 234);
+ assert.equal(element.$.cursor.side, 'right');
+
+ // Base hash: specifies lineNum and side.
+ element._initCursor({leftSide: true, lineNum: 345});
+ assert.equal(element.$.cursor.initialLineNumber, 345);
+ assert.equal(element.$.cursor.side, 'left');
+
+ // Specifies right side:
+ element._initCursor({leftSide: false, lineNum: 123});
+ assert.equal(element.$.cursor.initialLineNumber, 123);
+ assert.equal(element.$.cursor.side, 'right');
+ });
+
+ test('_getLineOfInterest', () => {
+ assert.isNull(element._getLineOfInterest({}));
+
+ let result = element._getLineOfInterest({lineNum: 12});
+ assert.equal(result.number, 12);
+ assert.isNotOk(result.leftSide);
+
+ result = element._getLineOfInterest({lineNum: 12, leftSide: true});
+ assert.equal(result.number, 12);
+ assert.isOk(result.leftSide);
+ });
+
+ test('_onLineSelected', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ const replaceStateStub = sandbox.stub(history, 'replaceState');
+ const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber');
+ sandbox.stub(element.$.cursor, 'getAddress')
+ .returns({number: 123, isLeftSide: false});
+
+ element._changeNum = 321;
+ element._change = {_number: 321, project: 'foo/bar'};
+ element._patchRange = {
+ basePatchNum: '3',
+ patchNum: '5',
+ };
+ const e = {};
+ const detail = {number: 123, side: 'right'};
+
+ element._onLineSelected(e, detail);
+
+ assert.isTrue(moveStub.called);
+ assert.equal(moveStub.lastCall.args[0], detail.number);
+ assert.equal(moveStub.lastCall.args[1], detail.side);
+
+ assert.isTrue(replaceStateStub.called);
+ assert.isTrue(getUrlStub.called);
+ });
+
+ test('_onLineSelected w/o line address', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ sandbox.stub(history, 'replaceState');
+ sandbox.stub(element.$.cursor, 'moveToLineNumber');
+ sandbox.stub(element.$.cursor, 'getAddress').returns(null);
+ element._changeNum = 321;
+ element._change = {_number: 321, project: 'foo/bar'};
+ element._patchRange = {basePatchNum: '3', patchNum: '5'};
+ element._onLineSelected({}, {number: 123, side: 'right'});
+ assert.isTrue(getUrlStub.calledOnce);
+ assert.isUndefined(getUrlStub.lastCall.args[5]);
+ assert.isUndefined(getUrlStub.lastCall.args[6]);
+ });
+
+ test('_getDiffViewMode', () => {
+ // No user prefs or change view state set.
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+
+ // User prefs but no change view state set.
+ element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
+ assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+
+ // User prefs and change view state set.
+ element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ });
+
+ test('_handleToggleDiffMode', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ const e = {preventDefault: () => {}};
+ // Initial state.
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+
+ element._handleToggleDiffMode(e);
+ assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+
+ element._handleToggleDiffMode(e);
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ });
+
+ suite('_loadComments', () => {
+ test('empty', done => {
+ element._loadComments().then(() => {
+ assert.equal(Object.keys(element._commentMap).length, 0);
+ done();
});
});
- test('_initCursor', () => {
- assert.isNotOk(element.$.cursor.initialLineNumber);
-
- // Does nothing when params specify no cursor address:
- element._initCursor({});
- assert.isNotOk(element.$.cursor.initialLineNumber);
-
- // Does nothing when params specify side but no number:
- element._initCursor({leftSide: true});
- assert.isNotOk(element.$.cursor.initialLineNumber);
-
- // Revision hash: specifies lineNum but not side.
- element._initCursor({lineNum: 234});
- assert.equal(element.$.cursor.initialLineNumber, 234);
- assert.equal(element.$.cursor.side, 'right');
-
- // Base hash: specifies lineNum and side.
- element._initCursor({leftSide: true, lineNum: 345});
- assert.equal(element.$.cursor.initialLineNumber, 345);
- assert.equal(element.$.cursor.side, 'left');
-
- // Specifies right side:
- element._initCursor({leftSide: false, lineNum: 123});
- assert.equal(element.$.cursor.initialLineNumber, 123);
- assert.equal(element.$.cursor.side, 'right');
- });
-
- test('_getLineOfInterest', () => {
- assert.isNull(element._getLineOfInterest({}));
-
- let result = element._getLineOfInterest({lineNum: 12});
- assert.equal(result.number, 12);
- assert.isNotOk(result.leftSide);
-
- result = element._getLineOfInterest({lineNum: 12, leftSide: true});
- assert.equal(result.number, 12);
- assert.isOk(result.leftSide);
- });
-
- test('_onLineSelected', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- const replaceStateStub = sandbox.stub(history, 'replaceState');
- const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber');
- sandbox.stub(element.$.cursor, 'getAddress')
- .returns({number: 123, isLeftSide: false});
-
- element._changeNum = 321;
- element._change = {_number: 321, project: 'foo/bar'};
+ test('has paths', done => {
+ sandbox.stub(element, '_getPaths').returns({
+ 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
+ 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}],
+ });
+ sandbox.stub(element, '_getCommentsForPath').returns({meta: {}});
+ element._changeNum = '42';
element._patchRange = {
basePatchNum: '3',
patchNum: '5',
};
- const e = {};
- const detail = {number: 123, side: 'right'};
-
- element._onLineSelected(e, detail);
-
- assert.isTrue(moveStub.called);
- assert.equal(moveStub.lastCall.args[0], detail.number);
- assert.equal(moveStub.lastCall.args[1], detail.side);
-
- assert.isTrue(replaceStateStub.called);
- assert.isTrue(getUrlStub.called);
- });
-
- test('_onLineSelected w/o line address', () => {
- const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- sandbox.stub(history, 'replaceState');
- sandbox.stub(element.$.cursor, 'moveToLineNumber');
- sandbox.stub(element.$.cursor, 'getAddress').returns(null);
- element._changeNum = 321;
- element._change = {_number: 321, project: 'foo/bar'};
- element._patchRange = {basePatchNum: '3', patchNum: '5'};
- element._onLineSelected({}, {number: 123, side: 'right'});
- assert.isTrue(getUrlStub.calledOnce);
- assert.isUndefined(getUrlStub.lastCall.args[5]);
- assert.isUndefined(getUrlStub.lastCall.args[6]);
- });
-
- test('_getDiffViewMode', () => {
- // No user prefs or change view state set.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // User prefs but no change view state set.
- element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
-
- // User prefs and change view state set.
- element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
- test('_handleToggleDiffMode', () => {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- const e = {preventDefault: () => {}};
- // Initial state.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
-
- element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
- suite('_loadComments', () => {
- test('empty', done => {
- element._loadComments().then(() => {
- assert.equal(Object.keys(element._commentMap).length, 0);
- done();
- });
- });
-
- test('has paths', done => {
- sandbox.stub(element, '_getPaths').returns({
- 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}],
- 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}],
- });
- sandbox.stub(element, '_getCommentsForPath').returns({meta: {}});
- element._changeNum = '42';
- element._patchRange = {
- basePatchNum: '3',
- patchNum: '5',
- };
- element._loadComments().then(() => {
- assert.deepEqual(Object.keys(element._commentMap),
- ['path/to/file/one.cpp', 'path-to/file/two.py']);
- done();
- });
+ element._loadComments().then(() => {
+ assert.deepEqual(Object.keys(element._commentMap),
+ ['path/to/file/one.cpp', 'path-to/file/two.py']);
+ done();
});
});
+ });
- suite('_computeCommentSkips', () => {
- test('empty file list', () => {
- const commentMap = {
- 'path/one.jpg': true,
- 'path/three.wav': true,
- };
- const path = 'path/two.m4v';
- const fileList = [];
- const result = element._computeCommentSkips(commentMap, fileList, path);
- assert.isNull(result.previous);
- assert.isNull(result.next);
- });
-
- test('finds skips', () => {
- const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
- let path = fileList[1];
- const commentMap = {};
- commentMap[fileList[0]] = true;
- commentMap[fileList[1]] = false;
- commentMap[fileList[2]] = true;
-
- let result = element._computeCommentSkips(commentMap, fileList, path);
- assert.equal(result.previous, fileList[0]);
- assert.equal(result.next, fileList[2]);
-
- commentMap[fileList[1]] = true;
-
- result = element._computeCommentSkips(commentMap, fileList, path);
- assert.equal(result.previous, fileList[0]);
- assert.equal(result.next, fileList[2]);
-
- path = fileList[0];
-
- result = element._computeCommentSkips(commentMap, fileList, path);
- assert.isNull(result.previous);
- assert.equal(result.next, fileList[1]);
-
- path = fileList[2];
-
- result = element._computeCommentSkips(commentMap, fileList, path);
- assert.equal(result.previous, fileList[1]);
- assert.isNull(result.next);
- });
-
- suite('skip next/previous', () => {
- let navToChangeStub;
- let navToDiffStub;
-
- setup(() => {
- navToChangeStub = sandbox.stub(element, '_navToChangeView');
- navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
- element._files = getFilesFromFileList([
- 'path/one.jpg', 'path/two.m4v', 'path/three.wav',
- ]);
- element._patchRange = {patchNum: '2', basePatchNum: '1'};
- });
-
- suite('_moveToPreviousFileWithComment', () => {
- test('no skips', () => {
- element._moveToPreviousFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('no previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = false;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = true;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToPreviousFileWithComment();
- assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('w/ previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = true;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = true;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToPreviousFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isTrue(navToDiffStub.calledOnce);
- });
- });
-
- suite('_moveToNextFileWithComment', () => {
- test('no skips', () => {
- element._moveToNextFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('no previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = true;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = false;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToNextFileWithComment();
- assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(navToDiffStub.called);
- });
-
- test('w/ previous', () => {
- const commentMap = {};
- commentMap[element._fileList[0]] = true;
- commentMap[element._fileList[1]] = false;
- commentMap[element._fileList[2]] = true;
- element._commentMap = commentMap;
- element._path = element._fileList[1];
-
- element._moveToNextFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isTrue(navToDiffStub.calledOnce);
- });
- });
- });
- });
-
- test('_computeEditMode', () => {
- const callCompute = range => element._computeEditMode({base: range});
- assert.isFalse(callCompute({}));
- assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}));
- assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}));
- assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}));
- });
-
- test('_computeFileNum', () => {
- assert.equal(element._computeFileNum('/foo',
- [{value: '/foo'}, {value: '/bar'}]), 1);
- assert.equal(element._computeFileNum('/bar',
- [{value: '/foo'}, {value: '/bar'}]), 2);
- });
-
- test('_computeFileNumClass', () => {
- assert.equal(element._computeFileNumClass(0, []), '');
- assert.equal(element._computeFileNumClass(1,
- [{value: '/foo'}, {value: '/bar'}]), 'show');
- });
-
- test('_getReviewedStatus', () => {
- const promises = [];
- element.$.restAPI.getReviewedFiles.restore();
-
- sandbox.stub(element.$.restAPI, 'getReviewedFiles')
- .returns(Promise.resolve(['path']));
-
- promises.push(element._getReviewedStatus(true, null, null, 'path')
- .then(reviewed => assert.isFalse(reviewed)));
-
- promises.push(element._getReviewedStatus(false, null, null, 'otherPath')
- .then(reviewed => assert.isFalse(reviewed)));
-
- promises.push(element._getReviewedStatus(false, null, null, 'path')
- .then(reviewed => assert.isTrue(reviewed)));
-
- return Promise.all(promises);
- });
-
- suite('blame', () => {
- test('toggle blame with button', () => {
- const toggleBlame = sandbox.stub(
- element.$.diffHost, 'loadBlame', () => Promise.resolve());
- MockInteractions.tap(element.$.toggleBlame);
- assert.isTrue(toggleBlame.calledOnce);
- });
- test('toggle blame with shortcut', () => {
- const toggleBlame = sandbox.stub(
- element.$.diffHost, 'loadBlame', () => Promise.resolve());
- MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
- assert.isTrue(toggleBlame.calledOnce);
- });
- });
-
- suite('editMode behavior', () => {
- setup(() => {
- element._loggedIn = true;
- });
-
- const isVisible = el => {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ suite('_computeCommentSkips', () => {
+ test('empty file list', () => {
+ const commentMap = {
+ 'path/one.jpg': true,
+ 'path/three.wav': true,
};
-
- test('reviewed checkbox', () => {
- sandbox.stub(element, '_handlePatchChange');
- element._patchRange = {patchNum: '1'};
- // Reviewed checkbox should be shown.
- assert.isTrue(isVisible(element.$.reviewed));
- element.set('_patchRange.patchNum', element.EDIT_NAME);
- flushAsynchronousOperations();
-
- assert.isFalse(isVisible(element.$.reviewed));
- });
+ const path = 'path/two.m4v';
+ const fileList = [];
+ const result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.isNull(result.previous);
+ assert.isNull(result.next);
});
- test('_paramsChanged sets in projectLookup', () => {
- sandbox.stub(element, '_getLineOfInterest');
- sandbox.stub(element, '_initCursor');
- const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
- element._paramsChanged({
- view: Gerrit.Nav.View.DIFF,
- changeNum: 101,
- project: 'test-project',
- path: '',
- });
- assert.isTrue(setStub.calledOnce);
- assert.isTrue(setStub.calledWith(101, 'test-project'));
+ test('finds skips', () => {
+ const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
+ let path = fileList[1];
+ const commentMap = {};
+ commentMap[fileList[0]] = true;
+ commentMap[fileList[1]] = false;
+ commentMap[fileList[2]] = true;
+
+ let result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.equal(result.previous, fileList[0]);
+ assert.equal(result.next, fileList[2]);
+
+ commentMap[fileList[1]] = true;
+
+ result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.equal(result.previous, fileList[0]);
+ assert.equal(result.next, fileList[2]);
+
+ path = fileList[0];
+
+ result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.isNull(result.previous);
+ assert.equal(result.next, fileList[1]);
+
+ path = fileList[2];
+
+ result = element._computeCommentSkips(commentMap, fileList, path);
+ assert.equal(result.previous, fileList[1]);
+ assert.isNull(result.next);
});
- test('shift+m navigates to next unreviewed file', () => {
- element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file1', 'file2']);
- element._path = 'file1';
- const reviewedStub = sandbox.stub(element, '_setReviewed');
- const navStub = sandbox.stub(element, '_navToFile');
- MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm');
+ suite('skip next/previous', () => {
+ let navToChangeStub;
+ let navToDiffStub;
+
+ setup(() => {
+ navToChangeStub = sandbox.stub(element, '_navToChangeView');
+ navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+ element._files = getFilesFromFileList([
+ 'path/one.jpg', 'path/two.m4v', 'path/three.wav',
+ ]);
+ element._patchRange = {patchNum: '2', basePatchNum: '1'};
+ });
+
+ suite('_moveToPreviousFileWithComment', () => {
+ test('no skips', () => {
+ element._moveToPreviousFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('no previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = false;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = true;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToPreviousFileWithComment();
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('w/ previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = true;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = true;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToPreviousFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isTrue(navToDiffStub.calledOnce);
+ });
+ });
+
+ suite('_moveToNextFileWithComment', () => {
+ test('no skips', () => {
+ element._moveToNextFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('no previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = true;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = false;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToNextFileWithComment();
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.isFalse(navToDiffStub.called);
+ });
+
+ test('w/ previous', () => {
+ const commentMap = {};
+ commentMap[element._fileList[0]] = true;
+ commentMap[element._fileList[1]] = false;
+ commentMap[element._fileList[2]] = true;
+ element._commentMap = commentMap;
+ element._path = element._fileList[1];
+
+ element._moveToNextFileWithComment();
+ assert.isFalse(navToChangeStub.called);
+ assert.isTrue(navToDiffStub.calledOnce);
+ });
+ });
+ });
+ });
+
+ test('_computeEditMode', () => {
+ const callCompute = range => element._computeEditMode({base: range});
+ assert.isFalse(callCompute({}));
+ assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}));
+ assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}));
+ assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}));
+ });
+
+ test('_computeFileNum', () => {
+ assert.equal(element._computeFileNum('/foo',
+ [{value: '/foo'}, {value: '/bar'}]), 1);
+ assert.equal(element._computeFileNum('/bar',
+ [{value: '/foo'}, {value: '/bar'}]), 2);
+ });
+
+ test('_computeFileNumClass', () => {
+ assert.equal(element._computeFileNumClass(0, []), '');
+ assert.equal(element._computeFileNumClass(1,
+ [{value: '/foo'}, {value: '/bar'}]), 'show');
+ });
+
+ test('_getReviewedStatus', () => {
+ const promises = [];
+ element.$.restAPI.getReviewedFiles.restore();
+
+ sandbox.stub(element.$.restAPI, 'getReviewedFiles')
+ .returns(Promise.resolve(['path']));
+
+ promises.push(element._getReviewedStatus(true, null, null, 'path')
+ .then(reviewed => assert.isFalse(reviewed)));
+
+ promises.push(element._getReviewedStatus(false, null, null, 'otherPath')
+ .then(reviewed => assert.isFalse(reviewed)));
+
+ promises.push(element._getReviewedStatus(false, null, null, 'path')
+ .then(reviewed => assert.isTrue(reviewed)));
+
+ return Promise.all(promises);
+ });
+
+ suite('blame', () => {
+ test('toggle blame with button', () => {
+ const toggleBlame = sandbox.stub(
+ element.$.diffHost, 'loadBlame', () => Promise.resolve());
+ MockInteractions.tap(element.$.toggleBlame);
+ assert.isTrue(toggleBlame.calledOnce);
+ });
+ test('toggle blame with shortcut', () => {
+ const toggleBlame = sandbox.stub(
+ element.$.diffHost, 'loadBlame', () => Promise.resolve());
+ MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b');
+ assert.isTrue(toggleBlame.calledOnce);
+ });
+ });
+
+ suite('editMode behavior', () => {
+ setup(() => {
+ element._loggedIn = true;
+ });
+
+ const isVisible = el => {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ };
+
+ test('reviewed checkbox', () => {
+ sandbox.stub(element, '_handlePatchChange');
+ element._patchRange = {patchNum: '1'};
+ // Reviewed checkbox should be shown.
+ assert.isTrue(isVisible(element.$.reviewed));
+ element.set('_patchRange.patchNum', element.EDIT_NAME);
flushAsynchronousOperations();
- assert.isTrue(reviewedStub.lastCall.args[0]);
- assert.deepEqual(navStub.lastCall.args, [
- 'file1',
- ['file1', 'file3'],
- 1,
- ]);
- });
-
- test('File change should trigger navigateToDiff once', () => {
- element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- sandbox.stub(element, '_getLineOfInterest');
- sandbox.stub(element, '_initCursor');
- sandbox.stub(Gerrit.Nav, 'navigateToDiff');
-
- // Load file1
- element._paramsChanged({
- view: Gerrit.Nav.View.DIFF,
- patchNum: 1,
- changeNum: 101,
- project: 'test-project',
- path: 'file1',
- });
- assert.isTrue(Gerrit.Nav.navigateToDiff.notCalled);
-
- // Switch to file2
- element.$.dropdown.value = 'file2';
- assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
-
- // This is to mock the param change triggered by above navigate
- element._paramsChanged({
- view: Gerrit.Nav.View.DIFF,
- patchNum: 1,
- changeNum: 101,
- project: 'test-project',
- path: 'file2',
- });
-
- // No extra call
- assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
- });
-
- test('_computeDownloadDropdownLinks', () => {
- const downloadLinks = [
- {
- url: '/changes/test~12/revisions/1/patch?zip&path=index.php',
- name: 'Patch',
- },
- {
- url: '/changes/test~12/revisions/1' +
- '/files/index.php/download?parent=1',
- name: 'Left Content',
- },
- {
- url: '/changes/test~12/revisions/1' +
- '/files/index.php/download',
- name: 'Right Content',
- },
- ];
-
- const side = {
- meta_a: true,
- meta_b: true,
- };
-
- const base = {
- patchNum: 1,
- basePatchNum: 'PARENT',
- };
-
- assert.deepEqual(
- element._computeDownloadDropdownLinks(
- 'test', 12, base, 'index.php', side),
- downloadLinks);
- });
-
- test('_computeDownloadDropdownLinks diff returns renamed', () => {
- const downloadLinks = [
- {
- url: '/changes/test~12/revisions/3/patch?zip&path=index.php',
- name: 'Patch',
- },
- {
- url: '/changes/test~12/revisions/2' +
- '/files/index2.php/download',
- name: 'Left Content',
- },
- {
- url: '/changes/test~12/revisions/3' +
- '/files/index.php/download',
- name: 'Right Content',
- },
- ];
-
- const side = {
- change_type: 'RENAMED',
- meta_a: {
- name: 'index2.php',
- },
- meta_b: true,
- };
-
- const base = {
- patchNum: 3,
- basePatchNum: 2,
- };
-
- assert.deepEqual(
- element._computeDownloadDropdownLinks(
- 'test', 12, base, 'index.php', side),
- downloadLinks);
- });
-
- test('_computeDownloadFileLink', () => {
- const base = {
- patchNum: 1,
- basePatchNum: 'PARENT',
- };
-
- assert.equal(
- element._computeDownloadFileLink(
- 'test', 12, base, 'index.php', true),
- '/changes/test~12/revisions/1/files/index.php/download?parent=1');
-
- assert.equal(
- element._computeDownloadFileLink(
- 'test', 12, base, 'index.php', false),
- '/changes/test~12/revisions/1/files/index.php/download');
- });
-
- test('_computeDownloadPatchLink', () => {
- assert.equal(
- element._computeDownloadPatchLink(
- 'test', 12, {patchNum: 1}, 'index.php'),
- '/changes/test~12/revisions/1/patch?zip&path=index.php');
+ assert.isFalse(isVisible(element.$.reviewed));
});
});
- suite('gr-diff-view tests unmodified files with comments', () => {
- let sandbox;
- let element;
- setup(() => {
- sandbox = sinon.sandbox.create();
- const changedFiles = {
- 'file1.txt': {},
- 'a/b/test.c': {},
- };
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({change: {}}); },
- getLoggedIn() { return Promise.resolve(false); },
- getProjectConfig() { return Promise.resolve({}); },
- getDiffChangeDetail() { return Promise.resolve({}); },
- getChangeFiles() { return Promise.resolve(changedFiles); },
- saveFileReviewed() { return Promise.resolve(); },
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
- getReviewedFiles() { return Promise.resolve([]); },
- });
- element = fixture('basic');
- return element._loadComments();
+ test('_paramsChanged sets in projectLookup', () => {
+ sandbox.stub(element, '_getLineOfInterest');
+ sandbox.stub(element, '_initCursor');
+ const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: 101,
+ project: 'test-project',
+ path: '',
+ });
+ assert.isTrue(setStub.calledOnce);
+ assert.isTrue(setStub.calledWith(101, 'test-project'));
+ });
+
+ test('shift+m navigates to next unreviewed file', () => {
+ element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
+ element._reviewedFiles = new Set(['file1', 'file2']);
+ element._path = 'file1';
+ const reviewedStub = sandbox.stub(element, '_setReviewed');
+ const navStub = sandbox.stub(element, '_navToFile');
+ MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm');
+ flushAsynchronousOperations();
+
+ assert.isTrue(reviewedStub.lastCall.args[0]);
+ assert.deepEqual(navStub.lastCall.args, [
+ 'file1',
+ ['file1', 'file3'],
+ 1,
+ ]);
+ });
+
+ test('File change should trigger navigateToDiff once', () => {
+ element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
+ sandbox.stub(element, '_getLineOfInterest');
+ sandbox.stub(element, '_initCursor');
+ sandbox.stub(Gerrit.Nav, 'navigateToDiff');
+
+ // Load file1
+ element._paramsChanged({
+ view: Gerrit.Nav.View.DIFF,
+ patchNum: 1,
+ changeNum: 101,
+ project: 'test-project',
+ path: 'file1',
+ });
+ assert.isTrue(Gerrit.Nav.navigateToDiff.notCalled);
+
+ // Switch to file2
+ element.$.dropdown.value = 'file2';
+ assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
+
+ // This is to mock the param change triggered by above navigate
+ element._paramsChanged({
+ view: Gerrit.Nav.View.DIFF,
+ patchNum: 1,
+ changeNum: 101,
+ project: 'test-project',
+ path: 'file2',
});
- teardown(() => {
- sandbox.restore();
- });
+ // No extra call
+ assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce);
+ });
- test('_getFiles add files with comments without changes', () => {
- const patchChangeRecord = {
- base: {
- basePatchNum: '5',
- patchNum: '10',
- },
- };
- const changeComments = {
- getPaths: sandbox.stub().returns({
- 'file2.txt': {},
- 'file1.txt': {},
- }),
- };
- return element._getFiles(23, patchChangeRecord, changeComments)
- .then(() => {
- assert.deepEqual(element._files, {
- sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
- changeFilesByPath: {
- 'file1.txt': {},
- 'file2.txt': {status: 'U'},
- 'a/b/test.c': {},
- },
- });
- });
- });
+ test('_computeDownloadDropdownLinks', () => {
+ const downloadLinks = [
+ {
+ url: '/changes/test~12/revisions/1/patch?zip&path=index.php',
+ name: 'Patch',
+ },
+ {
+ url: '/changes/test~12/revisions/1' +
+ '/files/index.php/download?parent=1',
+ name: 'Left Content',
+ },
+ {
+ url: '/changes/test~12/revisions/1' +
+ '/files/index.php/download',
+ name: 'Right Content',
+ },
+ ];
+
+ const side = {
+ meta_a: true,
+ meta_b: true,
+ };
+
+ const base = {
+ patchNum: 1,
+ basePatchNum: 'PARENT',
+ };
+
+ assert.deepEqual(
+ element._computeDownloadDropdownLinks(
+ 'test', 12, base, 'index.php', side),
+ downloadLinks);
+ });
+
+ test('_computeDownloadDropdownLinks diff returns renamed', () => {
+ const downloadLinks = [
+ {
+ url: '/changes/test~12/revisions/3/patch?zip&path=index.php',
+ name: 'Patch',
+ },
+ {
+ url: '/changes/test~12/revisions/2' +
+ '/files/index2.php/download',
+ name: 'Left Content',
+ },
+ {
+ url: '/changes/test~12/revisions/3' +
+ '/files/index.php/download',
+ name: 'Right Content',
+ },
+ ];
+
+ const side = {
+ change_type: 'RENAMED',
+ meta_a: {
+ name: 'index2.php',
+ },
+ meta_b: true,
+ };
+
+ const base = {
+ patchNum: 3,
+ basePatchNum: 2,
+ };
+
+ assert.deepEqual(
+ element._computeDownloadDropdownLinks(
+ 'test', 12, base, 'index.php', side),
+ downloadLinks);
+ });
+
+ test('_computeDownloadFileLink', () => {
+ const base = {
+ patchNum: 1,
+ basePatchNum: 'PARENT',
+ };
+
+ assert.equal(
+ element._computeDownloadFileLink(
+ 'test', 12, base, 'index.php', true),
+ '/changes/test~12/revisions/1/files/index.php/download?parent=1');
+
+ assert.equal(
+ element._computeDownloadFileLink(
+ 'test', 12, base, 'index.php', false),
+ '/changes/test~12/revisions/1/files/index.php/download');
+ });
+
+ test('_computeDownloadPatchLink', () => {
+ assert.equal(
+ element._computeDownloadPatchLink(
+ 'test', 12, {patchNum: 1}, 'index.php'),
+ '/changes/test~12/revisions/1/patch?zip&path=index.php');
});
});
+
+ suite('gr-diff-view tests unmodified files with comments', () => {
+ let sandbox;
+ let element;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ const changedFiles = {
+ 'file1.txt': {},
+ 'a/b/test.c': {},
+ };
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({change: {}}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ getProjectConfig() { return Promise.resolve({}); },
+ getDiffChangeDetail() { return Promise.resolve({}); },
+ getChangeFiles() { return Promise.resolve(changedFiles); },
+ saveFileReviewed() { return Promise.resolve(); },
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ getReviewedFiles() { return Promise.resolve([]); },
+ });
+ element = fixture('basic');
+ return element._loadComments();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_getFiles add files with comments without changes', () => {
+ const patchChangeRecord = {
+ base: {
+ basePatchNum: '5',
+ patchNum: '10',
+ },
+ };
+ const changeComments = {
+ getPaths: sandbox.stub().returns({
+ 'file2.txt': {},
+ 'file1.txt': {},
+ }),
+ };
+ return element._getFiles(23, patchChangeRecord, changeComments)
+ .then(() => {
+ assert.deepEqual(element._files, {
+ sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
+ changeFilesByPath: {
+ 'file1.txt': {},
+ 'file2.txt': {status: 'U'},
+ 'a/b/test.c': {},
+ },
+ });
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
index 00907d6..c461e93 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -19,193 +19,195 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-group</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/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="gr-diff-line.js"></script>
-<script src="gr-diff-group.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-diff-line.js"></script>
+<script type="module" src="./gr-diff-group.js"></script>
-<script>
- suite('gr-diff-group tests', async () => {
- await readyToTest();
- test('delta line pairs', () => {
- let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
- const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128);
- const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129);
- const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0);
- group.addLine(l1);
- group.addLine(l2);
- group.addLine(l3);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, [l1, l2]);
- assert.deepEqual(group.removes, [l3]);
- assert.deepEqual(group.lineRange, {
- left: {start: 64, end: 64},
- right: {start: 128, end: 129},
- });
-
- let pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l3, right: l1},
- {left: GrDiffLine.BLANK_LINE, right: l2},
- ]);
-
- group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, [l1, l2]);
- assert.deepEqual(group.removes, [l3]);
-
- pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l3, right: l1},
- {left: GrDiffLine.BLANK_LINE, right: l2},
- ]);
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-line.js';
+import './gr-diff-group.js';
+suite('gr-diff-group tests', () => {
+ test('delta line pairs', () => {
+ let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128);
+ const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129);
+ const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0);
+ group.addLine(l1);
+ group.addLine(l2);
+ group.addLine(l3);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, [l1, l2]);
+ assert.deepEqual(group.removes, [l3]);
+ assert.deepEqual(group.lineRange, {
+ left: {start: 64, end: 64},
+ right: {start: 128, end: 129},
});
- test('group/header line pairs', () => {
- const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128);
- const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129);
- const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130);
+ let pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l3, right: l1},
+ {left: GrDiffLine.BLANK_LINE, right: l2},
+ ]);
- let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
+ group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, [l1, l2]);
+ assert.deepEqual(group.removes, [l3]);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, []);
- assert.deepEqual(group.removes, []);
-
- assert.deepEqual(group.lineRange, {
- left: {start: 64, end: 66},
- right: {start: 128, end: 130},
- });
-
- let pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l1, right: l1},
- {left: l2, right: l2},
- {left: l3, right: l3},
- ]);
-
- group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, []);
- assert.deepEqual(group.removes, []);
-
- pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l1, right: l1},
- {left: l2, right: l2},
- {left: l3, right: l3},
- ]);
- });
-
- test('adding delta lines to non-delta group', () => {
- const l1 = new GrDiffLine(GrDiffLine.Type.ADD);
- const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE);
- const l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
-
- let group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
- assert.throws(group.addLine.bind(group, l1));
- assert.throws(group.addLine.bind(group, l2));
- assert.doesNotThrow(group.addLine.bind(group, l3));
-
- group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.throws(group.addLine.bind(group, l1));
- assert.throws(group.addLine.bind(group, l2));
- assert.doesNotThrow(group.addLine.bind(group, l3));
- });
-
- suite('hideInContextControl', () => {
- let groups;
- setup(() => {
- groups = [
- new GrDiffGroup(GrDiffGroup.Type.BOTH, [
- new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7),
- new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8),
- new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9),
- ]),
- new GrDiffGroup(GrDiffGroup.Type.DELTA, [
- new GrDiffLine(GrDiffLine.Type.REMOVE, 8),
- new GrDiffLine(GrDiffLine.Type.ADD, 0, 10),
- new GrDiffLine(GrDiffLine.Type.REMOVE, 9),
- new GrDiffLine(GrDiffLine.Type.ADD, 0, 11),
- new GrDiffLine(GrDiffLine.Type.REMOVE, 10),
- new GrDiffLine(GrDiffLine.Type.ADD, 0, 12),
- ]),
- new GrDiffGroup(GrDiffGroup.Type.BOTH, [
- new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13),
- new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14),
- new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15),
- ]),
- ];
- });
-
- test('hides hidden groups in context control', () => {
- const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6);
- assert.equal(collapsedGroups.length, 3);
-
- assert.equal(collapsedGroups[0], groups[0]);
-
- assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.equal(collapsedGroups[1].lines.length, 1);
- assert.equal(
- collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
- assert.equal(
- collapsedGroups[1].lines[0].contextGroups.length, 1);
- assert.equal(
- collapsedGroups[1].lines[0].contextGroups[0], groups[1]);
-
- assert.equal(collapsedGroups[2], groups[2]);
- });
-
- test('splits partially hidden groups', () => {
- const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7);
- assert.equal(collapsedGroups.length, 4);
- assert.equal(collapsedGroups[0], groups[0]);
-
- assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA);
- assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
- assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
-
- assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
- assert.equal(collapsedGroups[2].lines.length, 1);
- assert.equal(
- collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
- assert.equal(
- collapsedGroups[2].lines[0].contextGroups.length, 2);
-
- assert.equal(
- collapsedGroups[2].lines[0].contextGroups[0].type,
- GrDiffGroup.Type.DELTA);
- assert.deepEqual(
- collapsedGroups[2].lines[0].contextGroups[0].adds,
- groups[1].adds.slice(1));
- assert.deepEqual(
- collapsedGroups[2].lines[0].contextGroups[0].removes,
- groups[1].removes.slice(1));
-
- assert.equal(
- collapsedGroups[2].lines[0].contextGroups[1].type,
- GrDiffGroup.Type.BOTH);
- assert.deepEqual(
- collapsedGroups[2].lines[0].contextGroups[1].lines,
- [groups[2].lines[0]]);
-
- assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH);
- assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
- });
-
- test('groups unchanged if the hidden range is empty', () => {
- assert.deepEqual(
- GrDiffGroup.hideInContextControl(groups, 0, 0), groups);
- });
-
- test('groups unchanged if there is only 1 line to hide', () => {
- assert.deepEqual(
- GrDiffGroup.hideInContextControl(groups, 3, 4), groups);
- });
- });
+ pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l3, right: l1},
+ {left: GrDiffLine.BLANK_LINE, right: l2},
+ ]);
});
+ test('group/header line pairs', () => {
+ const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128);
+ const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129);
+ const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130);
+
+ let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
+
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, []);
+ assert.deepEqual(group.removes, []);
+
+ assert.deepEqual(group.lineRange, {
+ left: {start: 64, end: 66},
+ right: {start: 128, end: 130},
+ });
+
+ let pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l1, right: l1},
+ {left: l2, right: l2},
+ {left: l3, right: l3},
+ ]);
+
+ group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, []);
+ assert.deepEqual(group.removes, []);
+
+ pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l1, right: l1},
+ {left: l2, right: l2},
+ {left: l3, right: l3},
+ ]);
+ });
+
+ test('adding delta lines to non-delta group', () => {
+ const l1 = new GrDiffLine(GrDiffLine.Type.ADD);
+ const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ const l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
+
+ let group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
+ assert.throws(group.addLine.bind(group, l1));
+ assert.throws(group.addLine.bind(group, l2));
+ assert.doesNotThrow(group.addLine.bind(group, l3));
+
+ group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.throws(group.addLine.bind(group, l1));
+ assert.throws(group.addLine.bind(group, l2));
+ assert.doesNotThrow(group.addLine.bind(group, l3));
+ });
+
+ suite('hideInContextControl', () => {
+ let groups;
+ setup(() => {
+ groups = [
+ new GrDiffGroup(GrDiffGroup.Type.BOTH, [
+ new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9),
+ ]),
+ new GrDiffGroup(GrDiffGroup.Type.DELTA, [
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 8),
+ new GrDiffLine(GrDiffLine.Type.ADD, 0, 10),
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 9),
+ new GrDiffLine(GrDiffLine.Type.ADD, 0, 11),
+ new GrDiffLine(GrDiffLine.Type.REMOVE, 10),
+ new GrDiffLine(GrDiffLine.Type.ADD, 0, 12),
+ ]),
+ new GrDiffGroup(GrDiffGroup.Type.BOTH, [
+ new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14),
+ new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15),
+ ]),
+ ];
+ });
+
+ test('hides hidden groups in context control', () => {
+ const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6);
+ assert.equal(collapsedGroups.length, 3);
+
+ assert.equal(collapsedGroups[0], groups[0]);
+
+ assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(collapsedGroups[1].lines.length, 1);
+ assert.equal(
+ collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
+ assert.equal(
+ collapsedGroups[1].lines[0].contextGroups.length, 1);
+ assert.equal(
+ collapsedGroups[1].lines[0].contextGroups[0], groups[1]);
+
+ assert.equal(collapsedGroups[2], groups[2]);
+ });
+
+ test('splits partially hidden groups', () => {
+ const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7);
+ assert.equal(collapsedGroups.length, 4);
+ assert.equal(collapsedGroups[0], groups[0]);
+
+ assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA);
+ assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
+ assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
+
+ assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(collapsedGroups[2].lines.length, 1);
+ assert.equal(
+ collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
+ assert.equal(
+ collapsedGroups[2].lines[0].contextGroups.length, 2);
+
+ assert.equal(
+ collapsedGroups[2].lines[0].contextGroups[0].type,
+ GrDiffGroup.Type.DELTA);
+ assert.deepEqual(
+ collapsedGroups[2].lines[0].contextGroups[0].adds,
+ groups[1].adds.slice(1));
+ assert.deepEqual(
+ collapsedGroups[2].lines[0].contextGroups[0].removes,
+ groups[1].removes.slice(1));
+
+ assert.equal(
+ collapsedGroups[2].lines[0].contextGroups[1].type,
+ GrDiffGroup.Type.BOTH);
+ assert.deepEqual(
+ collapsedGroups[2].lines[0].contextGroups[1].lines,
+ [groups[2].lines[0]]);
+
+ assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH);
+ assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
+ });
+
+ test('groups unchanged if the hidden range is empty', () => {
+ assert.deepEqual(
+ GrDiffGroup.hideInContextControl(groups, 0, 0), groups);
+ });
+
+ test('groups unchanged if there is only 1 line to hide', () => {
+ assert.deepEqual(
+ GrDiffGroup.hideInContextControl(groups, 3, 4), groups);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 8e96147..5bca7be 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.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,964 +14,983 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
- const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
- 'of an edit.';
- const ERR_INVALID_LINE = 'Invalid line number: ';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../gr-diff-builder/gr-diff-builder-element.js';
+import '../gr-diff-highlight/gr-diff-highlight.js';
+import '../gr-diff-selection/gr-diff-selection.js';
+import '../gr-syntax-themes/gr-syntax-theme.js';
+import '../gr-ranged-comment-themes/gr-ranged-comment-theme.js';
+import '../../../scripts/hiddenscroll.js';
+import './gr-diff-line.js';
+import './gr-diff-group.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.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 {htmlTemplate} from './gr-diff_html.js';
- const NO_NEWLINE_BASE = 'No newline at end of base file.';
- const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
+const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
+const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
+ 'of an edit.';
+const ERR_INVALID_LINE = 'Invalid line number: ';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
+const NO_NEWLINE_BASE = 'No newline at end of base file.';
+const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
- const DiffSide = {
- LEFT: 'left',
- RIGHT: 'right',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
- const LARGE_DIFF_THRESHOLD_LINES = 10000;
- const FULL_CONTEXT = -1;
- const LIMITED_CONTEXT = 10;
+const DiffSide = {
+ LEFT: 'left',
+ RIGHT: 'right',
+};
+
+const LARGE_DIFF_THRESHOLD_LINES = 10000;
+const FULL_CONTEXT = -1;
+const LIMITED_CONTEXT = 10;
+
+/**
+ * Compare two ranges. Either argument may be falsy, but will only return
+ * true if both are falsy or if neither are falsy and have the same position
+ * values.
+ *
+ * @param {Gerrit.Range=} a range 1
+ * @param {Gerrit.Range=} b range 2
+ * @return {boolean}
+ */
+Gerrit.rangesEqual = function(a, b) {
+ if (!a && !b) { return true; }
+ if (!a || !b) { return false; }
+ return a.start_line === b.start_line &&
+ a.start_character === b.start_character &&
+ a.end_line === b.end_line &&
+ a.end_character === b.end_character;
+};
+
+function isThreadEl(node) {
+ return node.nodeType === Node.ELEMENT_NODE &&
+ node.classList.contains('comment-thread');
+}
+
+/**
+ * Turn a slot element into the corresponding content element.
+ * Slots are only fully supported in Polymer 2 - in Polymer 1, they are
+ * replaced with content elements during template parsing. This conversion is
+ * not applied for imperatively created slot elements, so this method
+ * implements the same behavior as the template parsing for imperative slots.
+ */
+Gerrit.slotToContent = function(slot) {
+ if (PolymerElement) {
+ return slot;
+ }
+ const content = document.createElement('content');
+ content.name = slot.name;
+ content.setAttribute('select', `[slot='${slot.name}']`);
+ return content;
+};
+
+const COMMIT_MSG_PATH = '/COMMIT_MSG';
+/**
+ * 72 is the inofficial length standard for git commit messages.
+ * Derived from the fact that git log/show appends 4 ws in the beginning of
+ * each line when displaying commit messages. To center the commit message
+ * in an 80 char terminal a 4 ws border is added to the rightmost side:
+ * 4 + 72 + 4
+ */
+const COMMIT_MSG_LINE_LENGTH = 72;
+
+const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrDiff extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-diff'; }
+ /**
+ * Fired when the user selects a line.
+ *
+ * @event line-selected
+ */
/**
- * Compare two ranges. Either argument may be falsy, but will only return
- * true if both are falsy or if neither are falsy and have the same position
- * values.
+ * Fired if being logged in is required.
*
- * @param {Gerrit.Range=} a range 1
- * @param {Gerrit.Range=} b range 2
- * @return {boolean}
+ * @event show-auth-required
*/
- Gerrit.rangesEqual = function(a, b) {
- if (!a && !b) { return true; }
- if (!a || !b) { return false; }
- return a.start_line === b.start_line &&
- a.start_character === b.start_character &&
- a.end_line === b.end_line &&
- a.end_character === b.end_character;
- };
- function isThreadEl(node) {
- return node.nodeType === Node.ELEMENT_NODE &&
- node.classList.contains('comment-thread');
+ /**
+ * Fired when a comment is created
+ *
+ * @event create-comment
+ */
+
+ /**
+ * Fired when rendering, including syntax highlighting, is done. Also fired
+ * when no rendering can be done because required preferences are not set.
+ *
+ * @event render
+ */
+
+ /**
+ * Fired for interaction reporting when a diff context is expanded.
+ * Contains an event.detail with numLines about the number of lines that
+ * were expanded.
+ *
+ * @event diff-context-expanded
+ */
+
+ static get properties() {
+ return {
+ changeNum: String,
+ noAutoRender: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ patchRange: Object,
+ path: {
+ type: String,
+ observer: '_pathObserver',
+ },
+ prefs: {
+ type: Object,
+ observer: '_prefsObserver',
+ },
+ projectName: String,
+ displayLine: {
+ type: Boolean,
+ value: false,
+ },
+ isImageDiff: {
+ type: Boolean,
+ },
+ commitRange: Object,
+ hidden: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ noRenderOnPrefsChange: Boolean,
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ _commentRanges: {
+ type: Array,
+ value: () => [],
+ },
+ /** @type {!Array<!Gerrit.CoverageRange>} */
+ coverageRanges: {
+ type: Array,
+ value: () => [],
+ },
+ lineWrapping: {
+ type: Boolean,
+ value: false,
+ observer: '_lineWrappingObserver',
+ },
+ viewMode: {
+ type: String,
+ value: DiffViewMode.SIDE_BY_SIDE,
+ observer: '_viewModeObserver',
+ },
+
+ /** @type {?Gerrit.LineOfInterest} */
+ lineOfInterest: Object,
+
+ loading: {
+ type: Boolean,
+ value: false,
+ observer: '_loadingChanged',
+ },
+
+ loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ diff: {
+ type: Object,
+ observer: '_diffChanged',
+ },
+ _diffHeaderItems: {
+ type: Array,
+ value: [],
+ computed: '_computeDiffHeaderItems(diff.*)',
+ },
+ _diffTableClass: {
+ type: String,
+ value: '',
+ },
+ /** @type {?Object} */
+ baseImage: Object,
+ /** @type {?Object} */
+ revisionImage: Object,
+
+ /**
+ * Whether the safety check for large diffs when whole-file is set has
+ * been bypassed. If the value is null, then the safety has not been
+ * bypassed. If the value is a number, then that number represents the
+ * context preference to use when rendering the bypassed diff.
+ *
+ * @type {number|null}
+ */
+ _safetyBypass: {
+ type: Number,
+ value: null,
+ },
+
+ _showWarning: Boolean,
+
+ /** @type {?string} */
+ errorMessage: {
+ type: String,
+ value: null,
+ },
+
+ /** @type {?Object} */
+ blame: {
+ type: Object,
+ value: null,
+ observer: '_blameChanged',
+ },
+
+ parentIndex: Number,
+
+ showNewlineWarningLeft: {
+ type: Boolean,
+ value: false,
+ },
+ showNewlineWarningRight: {
+ type: Boolean,
+ value: false,
+ },
+
+ _newlineWarning: {
+ type: String,
+ computed: '_computeNewlineWarning(' +
+ 'showNewlineWarningLeft, showNewlineWarningRight)',
+ },
+
+ _diffLength: Number,
+
+ /**
+ * Observes comment nodes added or removed after the initial render.
+ * Can be used to unregister when the entire diff is (re-)rendered or upon
+ * detachment.
+ *
+ * @type {?PolymerDomApi.ObserveHandle}
+ */
+ _incrementalNodeObserver: Object,
+
+ /**
+ * Observes comment nodes added or removed at any point.
+ * Can be used to unregister upon detachment.
+ *
+ * @type {?PolymerDomApi.ObserveHandle}
+ */
+ _nodeObserver: Object,
+
+ /** Set by Polymer. */
+ isAttached: Boolean,
+ layers: Array,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_enableSelectionObserver(loggedIn, isAttached)',
+ ];
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('create-range-comment',
+ e => this._handleCreateRangeComment(e));
+ this.addEventListener('render-content',
+ () => this._handleRenderContent());
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._observeNodes();
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this._unobserveIncrementalNodes();
+ this._unobserveNodes();
+ }
+
+ showNoChangeMessage(loading, prefs, diffLength) {
+ return !loading &&
+ prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
+ diffLength === 0;
+ }
+
+ _enableSelectionObserver(loggedIn, isAttached) {
+ // Polymer 2: check for undefined
+ if ([loggedIn, isAttached].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (loggedIn && isAttached) {
+ this.listen(document, 'selectionchange', '_handleSelectionChange');
+ this.listen(document, 'mouseup', '_handleMouseUp');
+ } else {
+ this.unlisten(document, 'selectionchange', '_handleSelectionChange');
+ this.unlisten(document, 'mouseup', '_handleMouseUp');
+ }
+ }
+
+ _handleSelectionChange() {
+ // Because of shadow DOM selections, we handle the selectionchange here,
+ // and pass the shadow DOM selection into gr-diff-highlight, where the
+ // corresponding range is determined and normalized.
+ const selection = this._getShadowOrDocumentSelection();
+ this.$.highlights.handleSelectionChange(selection, false);
+ }
+
+ _handleMouseUp(e) {
+ // To handle double-click outside of text creating comments, we check on
+ // mouse-up if there's a selection that just covers a line change. We
+ // can't do that on selection change since the user may still be dragging.
+ const selection = this._getShadowOrDocumentSelection();
+ this.$.highlights.handleSelectionChange(selection, true);
+ }
+
+ /** Gets the current selection, preferring the shadow DOM selection. */
+ _getShadowOrDocumentSelection() {
+ // When using native shadow DOM, the selection returned by
+ // document.getSelection() cannot reference the actual DOM elements making
+ // up the diff, because they are in the shadow DOM of the gr-diff element.
+ // This takes the shadow DOM selection if one exists.
+ return this.root.getSelection ?
+ this.root.getSelection() :
+ document.getSelection();
+ }
+
+ _observeNodes() {
+ this._nodeObserver = dom(this).observeNodes(info => {
+ const addedThreadEls = info.addedNodes.filter(isThreadEl);
+ const removedThreadEls = info.removedNodes.filter(isThreadEl);
+ this._updateRanges(addedThreadEls, removedThreadEls);
+ this._redispatchHoverEvents(addedThreadEls);
+ });
+ }
+
+ _updateRanges(addedThreadEls, removedThreadEls) {
+ function commentRangeFromThreadEl(threadEl) {
+ const side = threadEl.getAttribute('comment-side');
+ const range = JSON.parse(threadEl.getAttribute('range'));
+ return {side, range, hovering: false};
+ }
+
+ const addedCommentRanges = addedThreadEls
+ .map(commentRangeFromThreadEl)
+ .filter(({range}) => range);
+ const removedCommentRanges = removedThreadEls
+ .map(commentRangeFromThreadEl)
+ .filter(({range}) => range);
+ for (const removedCommentRange of removedCommentRanges) {
+ const i = this._commentRanges
+ .findIndex(
+ cr => cr.side === removedCommentRange.side &&
+ Gerrit.rangesEqual(cr.range, removedCommentRange.range)
+ );
+ this.splice('_commentRanges', i, 1);
+ }
+
+ if (addedCommentRanges && addedCommentRanges.length) {
+ this.push('_commentRanges', ...addedCommentRanges);
+ }
}
/**
- * Turn a slot element into the corresponding content element.
- * Slots are only fully supported in Polymer 2 - in Polymer 1, they are
- * replaced with content elements during template parsing. This conversion is
- * not applied for imperatively created slot elements, so this method
- * implements the same behavior as the template parsing for imperative slots.
+ * The key locations based on the comments and line of interests,
+ * where lines should not be collapsed.
+ *
+ * @return {{left: Object<(string|number), boolean>,
+ * right: Object<(string|number), boolean>}}
*/
- Gerrit.slotToContent = function(slot) {
- if (Polymer.Element) {
- return slot;
+ _computeKeyLocations() {
+ const keyLocations = {left: {}, right: {}};
+ if (this.lineOfInterest) {
+ const side = this.lineOfInterest.leftSide ? 'left' : 'right';
+ keyLocations[side][this.lineOfInterest.number] = true;
}
- const content = document.createElement('content');
- content.name = slot.name;
- content.setAttribute('select', `[slot='${slot.name}']`);
- return content;
- };
+ const threadEls = dom(this).getEffectiveChildNodes()
+ .filter(isThreadEl);
- const COMMIT_MSG_PATH = '/COMMIT_MSG';
- /**
- * 72 is the inofficial length standard for git commit messages.
- * Derived from the fact that git log/show appends 4 ws in the beginning of
- * each line when displaying commit messages. To center the commit message
- * in an 80 char terminal a 4 ws border is added to the rightmost side:
- * 4 + 72 + 4
- */
- const COMMIT_MSG_LINE_LENGTH = 72;
+ for (const threadEl of threadEls) {
+ const commentSide = threadEl.getAttribute('comment-side');
+ const lineNum = Number(threadEl.getAttribute('line-num')) ||
+ GrDiffLine.FILE;
+ const commentRange = threadEl.range || {};
+ keyLocations[commentSide][lineNum] = true;
+ // Add start_line as well if exists,
+ // the being and end of the range should not be collapsed.
+ if (commentRange.start_line) {
+ keyLocations[commentSide][commentRange.start_line] = true;
+ }
+ }
+ return keyLocations;
+ }
- const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
+ // Dispatch events that are handled by the gr-diff-highlight.
+ _redispatchHoverEvents(addedThreadEls) {
+ for (const threadEl of addedThreadEls) {
+ threadEl.addEventListener('mouseenter', () => {
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseenter', {bubbles: true, composed: true}));
+ });
+ threadEl.addEventListener('mouseleave', () => {
+ threadEl.dispatchEvent(new CustomEvent(
+ 'comment-thread-mouseleave', {bubbles: true, composed: true}));
+ });
+ }
+ }
- /**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrDiff extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-diff'; }
- /**
- * Fired when the user selects a line.
- *
- * @event line-selected
- */
+ /** Cancel any remaining diff builder rendering work. */
+ cancel() {
+ this.$.diffBuilder.cancel();
+ this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
+ }
- /**
- * Fired if being logged in is required.
- *
- * @event show-auth-required
- */
-
- /**
- * Fired when a comment is created
- *
- * @event create-comment
- */
-
- /**
- * Fired when rendering, including syntax highlighting, is done. Also fired
- * when no rendering can be done because required preferences are not set.
- *
- * @event render
- */
-
- /**
- * Fired for interaction reporting when a diff context is expanded.
- * Contains an event.detail with numLines about the number of lines that
- * were expanded.
- *
- * @event diff-context-expanded
- */
-
- static get properties() {
- return {
- changeNum: String,
- noAutoRender: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- patchRange: Object,
- path: {
- type: String,
- observer: '_pathObserver',
- },
- prefs: {
- type: Object,
- observer: '_prefsObserver',
- },
- projectName: String,
- displayLine: {
- type: Boolean,
- value: false,
- },
- isImageDiff: {
- type: Boolean,
- },
- commitRange: Object,
- hidden: {
- type: Boolean,
- reflectToAttribute: true,
- },
- noRenderOnPrefsChange: Boolean,
- /** @type {!Array<!Gerrit.HoveredRange>} */
- _commentRanges: {
- type: Array,
- value: () => [],
- },
- /** @type {!Array<!Gerrit.CoverageRange>} */
- coverageRanges: {
- type: Array,
- value: () => [],
- },
- lineWrapping: {
- type: Boolean,
- value: false,
- observer: '_lineWrappingObserver',
- },
- viewMode: {
- type: String,
- value: DiffViewMode.SIDE_BY_SIDE,
- observer: '_viewModeObserver',
- },
-
- /** @type {?Gerrit.LineOfInterest} */
- lineOfInterest: Object,
-
- loading: {
- type: Boolean,
- value: false,
- observer: '_loadingChanged',
- },
-
- loggedIn: {
- type: Boolean,
- value: false,
- },
- diff: {
- type: Object,
- observer: '_diffChanged',
- },
- _diffHeaderItems: {
- type: Array,
- value: [],
- computed: '_computeDiffHeaderItems(diff.*)',
- },
- _diffTableClass: {
- type: String,
- value: '',
- },
- /** @type {?Object} */
- baseImage: Object,
- /** @type {?Object} */
- revisionImage: Object,
-
- /**
- * Whether the safety check for large diffs when whole-file is set has
- * been bypassed. If the value is null, then the safety has not been
- * bypassed. If the value is a number, then that number represents the
- * context preference to use when rendering the bypassed diff.
- *
- * @type {number|null}
- */
- _safetyBypass: {
- type: Number,
- value: null,
- },
-
- _showWarning: Boolean,
-
- /** @type {?string} */
- errorMessage: {
- type: String,
- value: null,
- },
-
- /** @type {?Object} */
- blame: {
- type: Object,
- value: null,
- observer: '_blameChanged',
- },
-
- parentIndex: Number,
-
- showNewlineWarningLeft: {
- type: Boolean,
- value: false,
- },
- showNewlineWarningRight: {
- type: Boolean,
- value: false,
- },
-
- _newlineWarning: {
- type: String,
- computed: '_computeNewlineWarning(' +
- 'showNewlineWarningLeft, showNewlineWarningRight)',
- },
-
- _diffLength: Number,
-
- /**
- * Observes comment nodes added or removed after the initial render.
- * Can be used to unregister when the entire diff is (re-)rendered or upon
- * detachment.
- *
- * @type {?PolymerDomApi.ObserveHandle}
- */
- _incrementalNodeObserver: Object,
-
- /**
- * Observes comment nodes added or removed at any point.
- * Can be used to unregister upon detachment.
- *
- * @type {?PolymerDomApi.ObserveHandle}
- */
- _nodeObserver: Object,
-
- /** Set by Polymer. */
- isAttached: Boolean,
- layers: Array,
- };
+ /** @return {!Array<!HTMLElement>} */
+ getCursorStops() {
+ if (this.hidden && this.noAutoRender) {
+ return [];
}
- static get observers() {
- return [
- '_enableSelectionObserver(loggedIn, isAttached)',
- ];
- }
+ return Array.from(
+ dom(this.root).querySelectorAll('.diff-row'));
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('create-range-comment',
- e => this._handleCreateRangeComment(e));
- this.addEventListener('render-content',
- () => this._handleRenderContent());
- }
+ /** @return {boolean} */
+ isRangeSelected() {
+ return !!this.$.highlights.selectedRange;
+ }
- /** @override */
- attached() {
- super.attached();
- this._observeNodes();
- }
+ toggleLeftDiff() {
+ this.toggleClass('no-left');
+ }
- /** @override */
- detached() {
- super.detached();
- this._unobserveIncrementalNodes();
- this._unobserveNodes();
+ _blameChanged(newValue) {
+ this.$.diffBuilder.setBlame(newValue);
+ if (newValue) {
+ this.classList.add('showBlame');
+ } else {
+ this.classList.remove('showBlame');
}
+ }
- showNoChangeMessage(loading, prefs, diffLength) {
- return !loading &&
- prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
- diffLength === 0;
+ /** @return {string} */
+ _computeContainerClass(loggedIn, viewMode, displayLine) {
+ const classes = ['diffContainer'];
+ switch (viewMode) {
+ case DiffViewMode.UNIFIED:
+ classes.push('unified');
+ break;
+ case DiffViewMode.SIDE_BY_SIDE:
+ classes.push('sideBySide');
+ break;
+ default:
+ throw Error('Invalid view mode: ', viewMode);
}
+ if (Gerrit.hiddenscroll) {
+ classes.push('hiddenscroll');
+ }
+ if (loggedIn) {
+ classes.push('canComment');
+ }
+ if (displayLine) {
+ classes.push('displayLine');
+ }
+ return classes.join(' ');
+ }
- _enableSelectionObserver(loggedIn, isAttached) {
- // Polymer 2: check for undefined
- if ([loggedIn, isAttached].some(arg => arg === undefined)) {
+ _handleTap(e) {
+ const el = dom(e).localTarget;
+
+ if (el.classList.contains('showContext')) {
+ this.fire('diff-context-expanded', {
+ numLines: e.detail.numLines,
+ });
+ this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
+ } else if (el.classList.contains('lineNum')) {
+ this.addDraftAtLine(el);
+ } else if (el.tagName === 'HL' ||
+ el.classList.contains('content') ||
+ el.classList.contains('contentText')) {
+ const target = this.$.diffBuilder.getLineElByChild(el);
+ if (target) { this._selectLine(target); }
+ }
+ }
+
+ _selectLine(el) {
+ this.fire('line-selected', {
+ side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT,
+ number: el.getAttribute('data-value'),
+ path: this.path,
+ });
+ }
+
+ addDraftAtLine(el) {
+ this._selectLine(el);
+ if (!this._isValidElForComment(el)) { return; }
+
+ const value = el.getAttribute('data-value');
+ let lineNum;
+ if (value !== GrDiffLine.FILE) {
+ lineNum = parseInt(value, 10);
+ if (isNaN(lineNum)) {
+ this.fire('show-alert', {message: ERR_INVALID_LINE + value});
return;
}
-
- if (loggedIn && isAttached) {
- this.listen(document, 'selectionchange', '_handleSelectionChange');
- this.listen(document, 'mouseup', '_handleMouseUp');
- } else {
- this.unlisten(document, 'selectionchange', '_handleSelectionChange');
- this.unlisten(document, 'mouseup', '_handleMouseUp');
- }
}
+ this._createComment(el, lineNum);
+ }
- _handleSelectionChange() {
- // Because of shadow DOM selections, we handle the selectionchange here,
- // and pass the shadow DOM selection into gr-diff-highlight, where the
- // corresponding range is determined and normalized.
- const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, false);
+ createRangeComment() {
+ if (!this.isRangeSelected()) {
+ throw Error('Selection is needed for new range comment');
}
+ const {side, range} = this.$.highlights.selectedRange;
+ this._createCommentForSelection(side, range);
+ }
- _handleMouseUp(e) {
- // To handle double-click outside of text creating comments, we check on
- // mouse-up if there's a selection that just covers a line change. We
- // can't do that on selection change since the user may still be dragging.
- const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, true);
+ _createCommentForSelection(side, range) {
+ const lineNum = range.end_line;
+ const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
+ if (this._isValidElForComment(lineEl)) {
+ this._createComment(lineEl, lineNum, side, range);
}
+ }
- /** Gets the current selection, preferring the shadow DOM selection. */
- _getShadowOrDocumentSelection() {
- // When using native shadow DOM, the selection returned by
- // document.getSelection() cannot reference the actual DOM elements making
- // up the diff, because they are in the shadow DOM of the gr-diff element.
- // This takes the shadow DOM selection if one exists.
- return this.root.getSelection ?
- this.root.getSelection() :
- document.getSelection();
- }
+ _handleCreateRangeComment(e) {
+ const range = e.detail.range;
+ const side = e.detail.side;
+ this._createCommentForSelection(side, range);
+ }
- _observeNodes() {
- this._nodeObserver = Polymer.dom(this).observeNodes(info => {
- const addedThreadEls = info.addedNodes.filter(isThreadEl);
- const removedThreadEls = info.removedNodes.filter(isThreadEl);
- this._updateRanges(addedThreadEls, removedThreadEls);
- this._redispatchHoverEvents(addedThreadEls);
- });
- }
-
- _updateRanges(addedThreadEls, removedThreadEls) {
- function commentRangeFromThreadEl(threadEl) {
- const side = threadEl.getAttribute('comment-side');
- const range = JSON.parse(threadEl.getAttribute('range'));
- return {side, range, hovering: false};
- }
-
- const addedCommentRanges = addedThreadEls
- .map(commentRangeFromThreadEl)
- .filter(({range}) => range);
- const removedCommentRanges = removedThreadEls
- .map(commentRangeFromThreadEl)
- .filter(({range}) => range);
- for (const removedCommentRange of removedCommentRanges) {
- const i = this._commentRanges
- .findIndex(
- cr => cr.side === removedCommentRange.side &&
- Gerrit.rangesEqual(cr.range, removedCommentRange.range)
- );
- this.splice('_commentRanges', i, 1);
- }
-
- if (addedCommentRanges && addedCommentRanges.length) {
- this.push('_commentRanges', ...addedCommentRanges);
- }
- }
-
- /**
- * The key locations based on the comments and line of interests,
- * where lines should not be collapsed.
- *
- * @return {{left: Object<(string|number), boolean>,
- * right: Object<(string|number), boolean>}}
- */
- _computeKeyLocations() {
- const keyLocations = {left: {}, right: {}};
- if (this.lineOfInterest) {
- const side = this.lineOfInterest.leftSide ? 'left' : 'right';
- keyLocations[side][this.lineOfInterest.number] = true;
- }
- const threadEls = Polymer.dom(this).getEffectiveChildNodes()
- .filter(isThreadEl);
-
- for (const threadEl of threadEls) {
- const commentSide = threadEl.getAttribute('comment-side');
- const lineNum = Number(threadEl.getAttribute('line-num')) ||
- GrDiffLine.FILE;
- const commentRange = threadEl.range || {};
- keyLocations[commentSide][lineNum] = true;
- // Add start_line as well if exists,
- // the being and end of the range should not be collapsed.
- if (commentRange.start_line) {
- keyLocations[commentSide][commentRange.start_line] = true;
- }
- }
- return keyLocations;
- }
-
- // Dispatch events that are handled by the gr-diff-highlight.
- _redispatchHoverEvents(addedThreadEls) {
- for (const threadEl of addedThreadEls) {
- threadEl.addEventListener('mouseenter', () => {
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- });
- threadEl.addEventListener('mouseleave', () => {
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseleave', {bubbles: true, composed: true}));
- });
- }
- }
-
- /** Cancel any remaining diff builder rendering work. */
- cancel() {
- this.$.diffBuilder.cancel();
- this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
- }
-
- /** @return {!Array<!HTMLElement>} */
- getCursorStops() {
- if (this.hidden && this.noAutoRender) {
- return [];
- }
-
- return Array.from(
- Polymer.dom(this.root).querySelectorAll('.diff-row'));
- }
-
- /** @return {boolean} */
- isRangeSelected() {
- return !!this.$.highlights.selectedRange;
- }
-
- toggleLeftDiff() {
- this.toggleClass('no-left');
- }
-
- _blameChanged(newValue) {
- this.$.diffBuilder.setBlame(newValue);
- if (newValue) {
- this.classList.add('showBlame');
- } else {
- this.classList.remove('showBlame');
- }
- }
-
- /** @return {string} */
- _computeContainerClass(loggedIn, viewMode, displayLine) {
- const classes = ['diffContainer'];
- switch (viewMode) {
- case DiffViewMode.UNIFIED:
- classes.push('unified');
- break;
- case DiffViewMode.SIDE_BY_SIDE:
- classes.push('sideBySide');
- break;
- default:
- throw Error('Invalid view mode: ', viewMode);
- }
- if (Gerrit.hiddenscroll) {
- classes.push('hiddenscroll');
- }
- if (loggedIn) {
- classes.push('canComment');
- }
- if (displayLine) {
- classes.push('displayLine');
- }
- return classes.join(' ');
- }
-
- _handleTap(e) {
- const el = Polymer.dom(e).localTarget;
-
- if (el.classList.contains('showContext')) {
- this.fire('diff-context-expanded', {
- numLines: e.detail.numLines,
- });
- this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
- } else if (el.classList.contains('lineNum')) {
- this.addDraftAtLine(el);
- } else if (el.tagName === 'HL' ||
- el.classList.contains('content') ||
- el.classList.contains('contentText')) {
- const target = this.$.diffBuilder.getLineElByChild(el);
- if (target) { this._selectLine(target); }
- }
- }
-
- _selectLine(el) {
- this.fire('line-selected', {
- side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT,
- number: el.getAttribute('data-value'),
- path: this.path,
- });
- }
-
- addDraftAtLine(el) {
- this._selectLine(el);
- if (!this._isValidElForComment(el)) { return; }
-
- const value = el.getAttribute('data-value');
- let lineNum;
- if (value !== GrDiffLine.FILE) {
- lineNum = parseInt(value, 10);
- if (isNaN(lineNum)) {
- this.fire('show-alert', {message: ERR_INVALID_LINE + value});
- return;
- }
- }
- this._createComment(el, lineNum);
- }
-
- createRangeComment() {
- if (!this.isRangeSelected()) {
- throw Error('Selection is needed for new range comment');
- }
- const {side, range} = this.$.highlights.selectedRange;
- this._createCommentForSelection(side, range);
- }
-
- _createCommentForSelection(side, range) {
- const lineNum = range.end_line;
- const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
- if (this._isValidElForComment(lineEl)) {
- this._createComment(lineEl, lineNum, side, range);
- }
- }
-
- _handleCreateRangeComment(e) {
- const range = e.detail.range;
- const side = e.detail.side;
- this._createCommentForSelection(side, range);
- }
-
- /** @return {boolean} */
- _isValidElForComment(el) {
- if (!this.loggedIn) {
- this.fire('show-auth-required');
- return false;
- }
- const patchNum = el.classList.contains(DiffSide.LEFT) ?
- this.patchRange.basePatchNum :
- this.patchRange.patchNum;
-
- const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME);
- const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) &&
- this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME);
-
- if (isEdit) {
- this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT});
- return false;
- } else if (isEditBase) {
- this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE});
- return false;
- }
- return true;
- }
-
- /**
- * @param {!Object} lineEl
- * @param {number=} lineNum
- * @param {string=} side
- * @param {!Object=} range
- */
- _createComment(lineEl, lineNum, side, range) {
- const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
- const contentEl = contentText.parentElement;
- side = side ||
- this._getCommentSideByLineAndContent(lineEl, contentEl);
- const patchForNewThreads = this._getPatchNumByLineAndContent(
- lineEl, contentEl);
- const isOnParent =
- this._getIsParentCommentByLineAndContent(lineEl, contentEl);
- this.dispatchEvent(new CustomEvent('create-comment', {
- bubbles: true,
- composed: true,
- detail: {
- lineNum,
- side,
- patchNum: patchForNewThreads,
- isOnParent,
- range,
- },
- }));
- }
-
- _getThreadGroupForLine(contentEl) {
- return contentEl.querySelector('.thread-group');
- }
-
- /**
- * Gets or creates a comment thread group for a specific line and side on a
- * diff.
- *
- * @param {!Object} contentEl
- * @param {!Gerrit.DiffSide} commentSide
- * @return {!Node}
- */
- _getOrCreateThreadGroup(contentEl, commentSide) {
- // Check if thread group exists.
- let threadGroupEl = this._getThreadGroupForLine(contentEl);
- if (!threadGroupEl) {
- threadGroupEl = document.createElement('div');
- threadGroupEl.className = 'thread-group';
- threadGroupEl.setAttribute('data-side', commentSide);
- contentEl.appendChild(threadGroupEl);
- }
- return threadGroupEl;
- }
-
- /**
- * The value to be used for the patch number of new comments created at the
- * given line and content elements.
- *
- * In two cases of creating a comment on the left side, the patch number to
- * be used should actually be right side of the patch range:
- * - When the patch range is against the parent comment of a normal change.
- * Such comments declare themmselves to be on the left using side=PARENT.
- * - If the patch range is against the indexed parent of a merge change.
- * Such comments declare themselves to be on the given parent by
- * specifying the parent index via parent=i.
- *
- * @return {number}
- */
- _getPatchNumByLineAndContent(lineEl, contentEl) {
- let patchNum = this.patchRange.patchNum;
-
- if ((lineEl.classList.contains(DiffSide.LEFT) ||
- contentEl.classList.contains('remove')) &&
- this.patchRange.basePatchNum !== 'PARENT' &&
- !this.isMergeParent(this.patchRange.basePatchNum)) {
- patchNum = this.patchRange.basePatchNum;
- }
- return patchNum;
- }
-
- /** @return {boolean} */
- _getIsParentCommentByLineAndContent(lineEl, contentEl) {
- if ((lineEl.classList.contains(DiffSide.LEFT) ||
- contentEl.classList.contains('remove')) &&
- (this.patchRange.basePatchNum === 'PARENT' ||
- this.isMergeParent(this.patchRange.basePatchNum))) {
- return true;
- }
+ /** @return {boolean} */
+ _isValidElForComment(el) {
+ if (!this.loggedIn) {
+ this.fire('show-auth-required');
return false;
}
+ const patchNum = el.classList.contains(DiffSide.LEFT) ?
+ this.patchRange.basePatchNum :
+ this.patchRange.patchNum;
- /** @return {string} */
- _getCommentSideByLineAndContent(lineEl, contentEl) {
- let side = 'right';
- if (lineEl.classList.contains(DiffSide.LEFT) ||
- contentEl.classList.contains('remove')) {
- side = 'left';
- }
- return side;
+ const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME);
+ const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) &&
+ this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME);
+
+ if (isEdit) {
+ this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT});
+ return false;
+ } else if (isEditBase) {
+ this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE});
+ return false;
}
+ return true;
+ }
- _prefsObserver(newPrefs, oldPrefs) {
- // Scan the preference objects one level deep to see if they differ.
- let differ = !oldPrefs;
- if (newPrefs && oldPrefs) {
- for (const key in newPrefs) {
- if (newPrefs[key] !== oldPrefs[key]) {
- differ = true;
- }
+ /**
+ * @param {!Object} lineEl
+ * @param {number=} lineNum
+ * @param {string=} side
+ * @param {!Object=} range
+ */
+ _createComment(lineEl, lineNum, side, range) {
+ const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+ const contentEl = contentText.parentElement;
+ side = side ||
+ this._getCommentSideByLineAndContent(lineEl, contentEl);
+ const patchForNewThreads = this._getPatchNumByLineAndContent(
+ lineEl, contentEl);
+ const isOnParent =
+ this._getIsParentCommentByLineAndContent(lineEl, contentEl);
+ this.dispatchEvent(new CustomEvent('create-comment', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ lineNum,
+ side,
+ patchNum: patchForNewThreads,
+ isOnParent,
+ range,
+ },
+ }));
+ }
+
+ _getThreadGroupForLine(contentEl) {
+ return contentEl.querySelector('.thread-group');
+ }
+
+ /**
+ * Gets or creates a comment thread group for a specific line and side on a
+ * diff.
+ *
+ * @param {!Object} contentEl
+ * @param {!Gerrit.DiffSide} commentSide
+ * @return {!Node}
+ */
+ _getOrCreateThreadGroup(contentEl, commentSide) {
+ // Check if thread group exists.
+ let threadGroupEl = this._getThreadGroupForLine(contentEl);
+ if (!threadGroupEl) {
+ threadGroupEl = document.createElement('div');
+ threadGroupEl.className = 'thread-group';
+ threadGroupEl.setAttribute('data-side', commentSide);
+ contentEl.appendChild(threadGroupEl);
+ }
+ return threadGroupEl;
+ }
+
+ /**
+ * The value to be used for the patch number of new comments created at the
+ * given line and content elements.
+ *
+ * In two cases of creating a comment on the left side, the patch number to
+ * be used should actually be right side of the patch range:
+ * - When the patch range is against the parent comment of a normal change.
+ * Such comments declare themmselves to be on the left using side=PARENT.
+ * - If the patch range is against the indexed parent of a merge change.
+ * Such comments declare themselves to be on the given parent by
+ * specifying the parent index via parent=i.
+ *
+ * @return {number}
+ */
+ _getPatchNumByLineAndContent(lineEl, contentEl) {
+ let patchNum = this.patchRange.patchNum;
+
+ if ((lineEl.classList.contains(DiffSide.LEFT) ||
+ contentEl.classList.contains('remove')) &&
+ this.patchRange.basePatchNum !== 'PARENT' &&
+ !this.isMergeParent(this.patchRange.basePatchNum)) {
+ patchNum = this.patchRange.basePatchNum;
+ }
+ return patchNum;
+ }
+
+ /** @return {boolean} */
+ _getIsParentCommentByLineAndContent(lineEl, contentEl) {
+ if ((lineEl.classList.contains(DiffSide.LEFT) ||
+ contentEl.classList.contains('remove')) &&
+ (this.patchRange.basePatchNum === 'PARENT' ||
+ this.isMergeParent(this.patchRange.basePatchNum))) {
+ return true;
+ }
+ return false;
+ }
+
+ /** @return {string} */
+ _getCommentSideByLineAndContent(lineEl, contentEl) {
+ let side = 'right';
+ if (lineEl.classList.contains(DiffSide.LEFT) ||
+ contentEl.classList.contains('remove')) {
+ side = 'left';
+ }
+ return side;
+ }
+
+ _prefsObserver(newPrefs, oldPrefs) {
+ // Scan the preference objects one level deep to see if they differ.
+ let differ = !oldPrefs;
+ if (newPrefs && oldPrefs) {
+ for (const key in newPrefs) {
+ if (newPrefs[key] !== oldPrefs[key]) {
+ differ = true;
}
}
-
- if (differ) {
- this._prefsChanged(newPrefs);
- }
}
- _pathObserver() {
- // Call _prefsChanged(), because line-limit style value depends on path.
- this._prefsChanged(this.prefs);
- }
-
- _viewModeObserver() {
- this._prefsChanged(this.prefs);
- }
-
- /** @param {boolean} newValue */
- _loadingChanged(newValue) {
- if (newValue) {
- this.cancel();
- this._blame = null;
- this._safetyBypass = null;
- this._showWarning = false;
- this.clearDiffContent();
- }
- }
-
- _lineWrappingObserver() {
- this._prefsChanged(this.prefs);
- }
-
- _prefsChanged(prefs) {
- if (!prefs) { return; }
-
- this._blame = null;
-
- const lineLength = this.path === COMMIT_MSG_PATH ?
- COMMIT_MSG_LINE_LENGTH : prefs.line_length;
- const stylesToUpdate = {};
-
- if (prefs.line_wrapping) {
- this._diffTableClass = 'full-width';
- if (this.viewMode === 'SIDE_BY_SIDE') {
- stylesToUpdate['--content-width'] = 'none';
- stylesToUpdate['--line-limit'] = lineLength + 'ch';
- }
- } else {
- this._diffTableClass = '';
- stylesToUpdate['--content-width'] = lineLength + 'ch';
- }
-
- if (prefs.font_size) {
- stylesToUpdate['--font-size'] = prefs.font_size + 'px';
- }
-
- this.updateStyles(stylesToUpdate);
-
- if (this.diff && !this.noRenderOnPrefsChange) {
- this._debounceRenderDiffTable();
- }
- }
-
- _diffChanged(newValue) {
- if (newValue) {
- this._diffLength = this.getDiffLength(newValue);
- this._debounceRenderDiffTable();
- }
- }
-
- /**
- * When called multiple times from the same microtask, will call
- * _renderDiffTable only once, in the next microtask, unless it is cancelled
- * before that microtask runs.
- *
- * This should be used instead of calling _renderDiffTable directly to
- * render the diff in response to an input change, because there may be
- * multiple inputs changing in the same microtask, but we only want to
- * render once.
- */
- _debounceRenderDiffTable() {
- this.debounce(
- RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable());
- }
-
- _renderDiffTable() {
- this._unobserveIncrementalNodes();
- if (!this.prefs) {
- this.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return;
- }
- if (this.prefs.context === -1 &&
- this._diffLength >= LARGE_DIFF_THRESHOLD_LINES &&
- this._safetyBypass === null) {
- this._showWarning = true;
- this.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return;
- }
-
- this._showWarning = false;
-
- const keyLocations = this._computeKeyLocations();
- this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
- .then(() => {
- this.dispatchEvent(
- new CustomEvent('render', {
- bubbles: true,
- composed: true,
- detail: {contentRendered: true},
- }));
- });
- }
-
- _handleRenderContent() {
- this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => {
- const addedThreadEls = info.addedNodes.filter(isThreadEl);
- // Removed nodes do not need to be handled because all this code does is
- // adding a slot for the added thread elements, and the extra slots do
- // not hurt. It's probably a bigger performance cost to remove them than
- // to keep them around. Medium term we can even consider to add one slot
- // for each line from the start.
- let lastEl;
- for (const threadEl of addedThreadEls) {
- const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
- const commentSide = threadEl.getAttribute('comment-side');
- const lineEl = this.$.diffBuilder.getLineElByNumber(
- lineNumString, commentSide);
- const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
- const contentEl = contentText.parentElement;
- const threadGroupEl = this._getOrCreateThreadGroup(
- contentEl, commentSide);
- // Create a slot for the thread and attach it to the thread group.
- // The Polyfill has some bugs and this only works if the slot is
- // attached to the group after the group is attached to the DOM.
- // The thread group may already have a slot with the right name, but
- // that is okay because the first matching slot is used and the rest
- // are ignored.
- const slot = document.createElement('slot');
- slot.name = threadEl.getAttribute('slot');
- Polymer.dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot));
- lastEl = threadEl;
- }
-
- // Safari is not binding newly created comment-thread
- // with the slot somehow, replace itself will rebind it
- // @see Issue 11182
- if (lastEl && lastEl.replaceWith) {
- lastEl.replaceWith(lastEl);
- }
- });
- }
-
- _unobserveIncrementalNodes() {
- if (this._incrementalNodeObserver) {
- Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver);
- }
- }
-
- _unobserveNodes() {
- if (this._nodeObserver) {
- Polymer.dom(this).unobserveNodes(this._nodeObserver);
- }
- }
-
- /**
- * Get the preferences object including the safety bypass context (if any).
- */
- _getBypassPrefs() {
- if (this._safetyBypass !== null) {
- return Object.assign({}, this.prefs, {context: this._safetyBypass});
- }
- return this.prefs;
- }
-
- clearDiffContent() {
- this._unobserveIncrementalNodes();
- this.$.diffTable.innerHTML = null;
- }
-
- /** @return {!Array} */
- _computeDiffHeaderItems(diffInfoRecord) {
- const diffInfo = diffInfoRecord.base;
- if (!diffInfo || !diffInfo.diff_header) { return []; }
- return diffInfo.diff_header
- .filter(item => !(item.startsWith('diff --git ') ||
- item.startsWith('index ') ||
- item.startsWith('+++ ') ||
- item.startsWith('--- ') ||
- item === 'Binary files differ'));
- }
-
- /** @return {boolean} */
- _computeDiffHeaderHidden(items) {
- return items.length === 0;
- }
-
- _handleFullBypass() {
- this._safetyBypass = FULL_CONTEXT;
- this._debounceRenderDiffTable();
- }
-
- _handleLimitedBypass() {
- this._safetyBypass = LIMITED_CONTEXT;
- this._debounceRenderDiffTable();
- }
-
- /** @return {string} */
- _computeWarningClass(showWarning) {
- return showWarning ? 'warn' : '';
- }
-
- /**
- * @param {string} errorMessage
- * @return {string}
- */
- _computeErrorClass(errorMessage) {
- return errorMessage ? 'showError' : '';
- }
-
- expandAllContext() {
- this._handleFullBypass();
- }
-
- /**
- * @param {!boolean} warnLeft
- * @param {!boolean} warnRight
- * @return {string|null}
- */
- _computeNewlineWarning(warnLeft, warnRight) {
- const messages = [];
- if (warnLeft) {
- messages.push(NO_NEWLINE_BASE);
- }
- if (warnRight) {
- messages.push(NO_NEWLINE_REVISION);
- }
- if (!messages.length) { return null; }
- return messages.join(' — ');
- }
-
- /**
- * @param {string} warning
- * @param {boolean} loading
- * @return {string}
- */
- _computeNewlineWarningClass(warning, loading) {
- if (loading || !warning) { return 'newlineWarning hidden'; }
- return 'newlineWarning';
- }
-
- /**
- * Get the approximate length of the diff as the sum of the maximum
- * length of the chunks.
- *
- * @param {Object} diff object
- * @return {number}
- */
- getDiffLength(diff) {
- if (!diff) return 0;
- return diff.content.reduce((sum, sec) => {
- if (sec.hasOwnProperty('ab')) {
- return sum + sec.ab.length;
- } else {
- return sum + Math.max(
- sec.hasOwnProperty('a') ? sec.a.length : 0,
- sec.hasOwnProperty('b') ? sec.b.length : 0);
- }
- }, 0);
+ if (differ) {
+ this._prefsChanged(newPrefs);
}
}
- customElements.define(GrDiff.is, GrDiff);
-})();
+ _pathObserver() {
+ // Call _prefsChanged(), because line-limit style value depends on path.
+ this._prefsChanged(this.prefs);
+ }
+
+ _viewModeObserver() {
+ this._prefsChanged(this.prefs);
+ }
+
+ /** @param {boolean} newValue */
+ _loadingChanged(newValue) {
+ if (newValue) {
+ this.cancel();
+ this._blame = null;
+ this._safetyBypass = null;
+ this._showWarning = false;
+ this.clearDiffContent();
+ }
+ }
+
+ _lineWrappingObserver() {
+ this._prefsChanged(this.prefs);
+ }
+
+ _prefsChanged(prefs) {
+ if (!prefs) { return; }
+
+ this._blame = null;
+
+ const lineLength = this.path === COMMIT_MSG_PATH ?
+ COMMIT_MSG_LINE_LENGTH : prefs.line_length;
+ const stylesToUpdate = {};
+
+ if (prefs.line_wrapping) {
+ this._diffTableClass = 'full-width';
+ if (this.viewMode === 'SIDE_BY_SIDE') {
+ stylesToUpdate['--content-width'] = 'none';
+ stylesToUpdate['--line-limit'] = lineLength + 'ch';
+ }
+ } else {
+ this._diffTableClass = '';
+ stylesToUpdate['--content-width'] = lineLength + 'ch';
+ }
+
+ if (prefs.font_size) {
+ stylesToUpdate['--font-size'] = prefs.font_size + 'px';
+ }
+
+ this.updateStyles(stylesToUpdate);
+
+ if (this.diff && !this.noRenderOnPrefsChange) {
+ this._debounceRenderDiffTable();
+ }
+ }
+
+ _diffChanged(newValue) {
+ if (newValue) {
+ this._diffLength = this.getDiffLength(newValue);
+ this._debounceRenderDiffTable();
+ }
+ }
+
+ /**
+ * When called multiple times from the same microtask, will call
+ * _renderDiffTable only once, in the next microtask, unless it is cancelled
+ * before that microtask runs.
+ *
+ * This should be used instead of calling _renderDiffTable directly to
+ * render the diff in response to an input change, because there may be
+ * multiple inputs changing in the same microtask, but we only want to
+ * render once.
+ */
+ _debounceRenderDiffTable() {
+ this.debounce(
+ RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable());
+ }
+
+ _renderDiffTable() {
+ this._unobserveIncrementalNodes();
+ if (!this.prefs) {
+ this.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ return;
+ }
+ if (this.prefs.context === -1 &&
+ this._diffLength >= LARGE_DIFF_THRESHOLD_LINES &&
+ this._safetyBypass === null) {
+ this._showWarning = true;
+ this.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ return;
+ }
+
+ this._showWarning = false;
+
+ const keyLocations = this._computeKeyLocations();
+ this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
+ .then(() => {
+ this.dispatchEvent(
+ new CustomEvent('render', {
+ bubbles: true,
+ composed: true,
+ detail: {contentRendered: true},
+ }));
+ });
+ }
+
+ _handleRenderContent() {
+ this._incrementalNodeObserver = dom(this).observeNodes(info => {
+ const addedThreadEls = info.addedNodes.filter(isThreadEl);
+ // Removed nodes do not need to be handled because all this code does is
+ // adding a slot for the added thread elements, and the extra slots do
+ // not hurt. It's probably a bigger performance cost to remove them than
+ // to keep them around. Medium term we can even consider to add one slot
+ // for each line from the start.
+ let lastEl;
+ for (const threadEl of addedThreadEls) {
+ const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
+ const commentSide = threadEl.getAttribute('comment-side');
+ const lineEl = this.$.diffBuilder.getLineElByNumber(
+ lineNumString, commentSide);
+ const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+ const contentEl = contentText.parentElement;
+ const threadGroupEl = this._getOrCreateThreadGroup(
+ contentEl, commentSide);
+ // Create a slot for the thread and attach it to the thread group.
+ // The Polyfill has some bugs and this only works if the slot is
+ // attached to the group after the group is attached to the DOM.
+ // The thread group may already have a slot with the right name, but
+ // that is okay because the first matching slot is used and the rest
+ // are ignored.
+ const slot = document.createElement('slot');
+ slot.name = threadEl.getAttribute('slot');
+ dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot));
+ lastEl = threadEl;
+ }
+
+ // Safari is not binding newly created comment-thread
+ // with the slot somehow, replace itself will rebind it
+ // @see Issue 11182
+ if (lastEl && lastEl.replaceWith) {
+ lastEl.replaceWith(lastEl);
+ }
+ });
+ }
+
+ _unobserveIncrementalNodes() {
+ if (this._incrementalNodeObserver) {
+ dom(this).unobserveNodes(this._incrementalNodeObserver);
+ }
+ }
+
+ _unobserveNodes() {
+ if (this._nodeObserver) {
+ dom(this).unobserveNodes(this._nodeObserver);
+ }
+ }
+
+ /**
+ * Get the preferences object including the safety bypass context (if any).
+ */
+ _getBypassPrefs() {
+ if (this._safetyBypass !== null) {
+ return Object.assign({}, this.prefs, {context: this._safetyBypass});
+ }
+ return this.prefs;
+ }
+
+ clearDiffContent() {
+ this._unobserveIncrementalNodes();
+ this.$.diffTable.innerHTML = null;
+ }
+
+ /** @return {!Array} */
+ _computeDiffHeaderItems(diffInfoRecord) {
+ const diffInfo = diffInfoRecord.base;
+ if (!diffInfo || !diffInfo.diff_header) { return []; }
+ return diffInfo.diff_header
+ .filter(item => !(item.startsWith('diff --git ') ||
+ item.startsWith('index ') ||
+ item.startsWith('+++ ') ||
+ item.startsWith('--- ') ||
+ item === 'Binary files differ'));
+ }
+
+ /** @return {boolean} */
+ _computeDiffHeaderHidden(items) {
+ return items.length === 0;
+ }
+
+ _handleFullBypass() {
+ this._safetyBypass = FULL_CONTEXT;
+ this._debounceRenderDiffTable();
+ }
+
+ _handleLimitedBypass() {
+ this._safetyBypass = LIMITED_CONTEXT;
+ this._debounceRenderDiffTable();
+ }
+
+ /** @return {string} */
+ _computeWarningClass(showWarning) {
+ return showWarning ? 'warn' : '';
+ }
+
+ /**
+ * @param {string} errorMessage
+ * @return {string}
+ */
+ _computeErrorClass(errorMessage) {
+ return errorMessage ? 'showError' : '';
+ }
+
+ expandAllContext() {
+ this._handleFullBypass();
+ }
+
+ /**
+ * @param {!boolean} warnLeft
+ * @param {!boolean} warnRight
+ * @return {string|null}
+ */
+ _computeNewlineWarning(warnLeft, warnRight) {
+ const messages = [];
+ if (warnLeft) {
+ messages.push(NO_NEWLINE_BASE);
+ }
+ if (warnRight) {
+ messages.push(NO_NEWLINE_REVISION);
+ }
+ if (!messages.length) { return null; }
+ return messages.join(' — ');
+ }
+
+ /**
+ * @param {string} warning
+ * @param {boolean} loading
+ * @return {string}
+ */
+ _computeNewlineWarningClass(warning, loading) {
+ if (loading || !warning) { return 'newlineWarning hidden'; }
+ return 'newlineWarning';
+ }
+
+ /**
+ * Get the approximate length of the diff as the sum of the maximum
+ * length of the chunks.
+ *
+ * @param {Object} diff object
+ * @return {number}
+ */
+ getDiffLength(diff) {
+ if (!diff) return 0;
+ return diff.content.reduce((sum, sec) => {
+ if (sec.hasOwnProperty('ab')) {
+ return sum + sec.ab.length;
+ } else {
+ return sum + Math.max(
+ sec.hasOwnProperty('a') ? sec.a.length : 0,
+ sec.hasOwnProperty('b') ? sec.b.length : 0);
+ }
+ }, 0);
+ }
+}
+
+customElements.define(GrDiff.is, GrDiff);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
index 38f6265..c6a7e98 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
@@ -1,37 +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="/bower_components/polymer/polymer.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="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../gr-diff-builder/gr-diff-builder-element.html">
-<link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html">
-<link rel="import" href="../gr-diff-selection/gr-diff-selection.html">
-<link rel="import" href="../gr-syntax-themes/gr-syntax-theme.html">
-<link rel="import" href="../gr-ranged-comment-themes/gr-ranged-comment-theme.html">
-
-<script src="../../../scripts/hiddenscroll.js"></script>
-<script src="gr-diff-line.js"></script>
-<script src="gr-diff-group.js"></script>
-
-<dom-module id="gr-diff">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host(.no-left) .sideBySide .left,
:host(.no-left) .sideBySide .left + td,
@@ -184,7 +169,7 @@
.content .contentText:empty:after {
/* Newline, to ensure empty lines are one line-height tall. */
- content: '\A';
+ content: '\\A';
}
.contextControl {
background-color: var(--diff-context-control-background-color);
@@ -214,7 +199,7 @@
}
.br:after {
/* Line feed */
- content: '\A';
+ content: '\\A';
}
.tab {
display: inline-block;
@@ -222,7 +207,7 @@
.tab-indicator:before {
color: var(--diff-tab-indicator-color);
/* >> character */
- content: '\00BB';
+ content: '\\00BB';
position: absolute;
}
/* Is defined after other background-colors, such that this
@@ -363,38 +348,16 @@
<style include="gr-ranged-comment-theme">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
- <template
- is="dom-repeat"
- items="[[_diffHeaderItems]]">
+ <div id="diffHeader" hidden\$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
+ <template is="dom-repeat" items="[[_diffHeaderItems]]">
<div>[[item]]</div>
</template>
</div>
- <div class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
- on-tap="_handleTap">
+ <div class\$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]" on-tap="_handleTap">
<gr-diff-selection diff="[[diff]]">
- <gr-diff-highlight
- id="highlights"
- logged-in="[[loggedIn]]"
- comment-ranges="{{_commentRanges}}">
- <gr-diff-builder
- id="diffBuilder"
- comment-ranges="[[_commentRanges]]"
- coverage-ranges="[[coverageRanges]]"
- project-name="[[projectName]]"
- diff="[[diff]]"
- path="[[path]]"
- change-num="[[changeNum]]"
- patch-num="[[patchRange.patchNum]]"
- view-mode="[[viewMode]]"
- is-image-diff="[[isImageDiff]]"
- base-image="[[baseImage]]"
- layers="[[layers]]"
- revision-image="[[revisionImage]]">
- <table
- id="diffTable"
- class$="[[_diffTableClass]]"
- role="presentation"></table>
+ <gr-diff-highlight id="highlights" logged-in="[[loggedIn]]" comment-ranges="{{_commentRanges}}">
+ <gr-diff-builder id="diffBuilder" comment-ranges="[[_commentRanges]]" coverage-ranges="[[coverageRanges]]" project-name="[[projectName]]" diff="[[diff]]" path="[[path]]" change-num="[[changeNum]]" patch-num="[[patchRange.patchNum]]" view-mode="[[viewMode]]" is-image-diff="[[isImageDiff]]" base-image="[[baseImage]]" layers="[[layers]]" revision-image="[[revisionImage]]">
+ <table id="diffTable" class\$="[[_diffTableClass]]" role="presentation"></table>
<template is="dom-if" if="[[showNoChangeMessage(loading, prefs, _diffLength)]]">
<div class="whitespace-change-only-message">
@@ -406,13 +369,13 @@
</gr-diff-highlight>
</gr-diff-selection>
</div>
- <div class$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]">
+ <div class\$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]">
[[_newlineWarning]]
</div>
- <div id="loadingError" class$="[[_computeErrorClass(errorMessage)]]">
+ <div id="loadingError" class\$="[[_computeErrorClass(errorMessage)]]">
[[errorMessage]]
</div>
- <div id="sizeWarning" class$="[[_computeWarningClass(_showWarning)]]">
+ <div id="sizeWarning" class\$="[[_computeWarningClass(_showWarning)]]">
<p>
Prevented render because "Whole file" is enabled and this diff is very
large (about [[_diffLength]] lines).
@@ -424,6 +387,4 @@
Render anyway (may be slow)
</gr-button>
</div>
- </template>
- <script src="gr-diff.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index ba46549..884c729 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -19,20 +19,28 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</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>
+<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 src="/components/web-component-tester/data/a11ySuite.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="../../../scripts/util.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-diff.html">
+<script type="module" src="../../shared/gr-rest-api-interface/gr-rest-api-interface.js"></script>
+<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script>
+<script type="module" src="./gr-diff.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-diff.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -40,932 +48,248 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-diff.js';
+import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-diff tests', () => {
+ let element;
+ let sandbox;
- const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
+ const MINIMAL_PREFS = {tab_size: 2, line_length: 80};
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('selectionchange event handling', () => {
+ const emulateSelection = function() {
+ document.dispatchEvent(new CustomEvent('selectionchange'));
+ };
setup(() => {
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- suite('selectionchange event handling', () => {
- const emulateSelection = function() {
- document.dispatchEvent(new CustomEvent('selectionchange'));
- };
-
- setup(() => {
- element = fixture('basic');
- sandbox.stub(element.$.highlights, 'handleSelectionChange');
- });
-
- test('enabled if logged in', () => {
- element.loggedIn = true;
- emulateSelection();
- assert.isTrue(element.$.highlights.handleSelectionChange.called);
- });
-
- test('ignored if logged out', () => {
- element.loggedIn = false;
- emulateSelection();
- assert.isFalse(element.$.highlights.handleSelectionChange.called);
- });
- });
-
- test('cancel', () => {
element = fixture('basic');
- const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
- element.cancel();
- assert.isTrue(cancelStub.calledOnce);
+ sandbox.stub(element.$.highlights, 'handleSelectionChange');
});
- test('line limit with line_wrapping', () => {
+ test('enabled if logged in', () => {
+ element.loggedIn = true;
+ emulateSelection();
+ assert.isTrue(element.$.highlights.handleSelectionChange.called);
+ });
+
+ test('ignored if logged out', () => {
+ element.loggedIn = false;
+ emulateSelection();
+ assert.isFalse(element.$.highlights.handleSelectionChange.called);
+ });
+ });
+
+ test('cancel', () => {
+ element = fixture('basic');
+ const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
+ element.cancel();
+ assert.isTrue(cancelStub.calledOnce);
+ });
+
+ test('line limit with line_wrapping', () => {
+ element = fixture('basic');
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
+ flushAsynchronousOperations();
+ assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
+ });
+
+ test('line limit without line_wrapping', () => {
+ element = fixture('basic');
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
+ flushAsynchronousOperations();
+ assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
+ });
+
+ suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
+ let lineEl;
+ let contentEl;
+
+ setup(() => {
element = fixture('basic');
- element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
- flushAsynchronousOperations();
- assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
+ lineEl = document.createElement('td');
+ contentEl = document.createElement('span');
});
- test('line limit without line_wrapping', () => {
- element = fixture('basic');
- element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
- flushAsynchronousOperations();
- assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
- });
-
- suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
- let lineEl;
- let contentEl;
-
- setup(() => {
- element = fixture('basic');
- lineEl = document.createElement('td');
- contentEl = document.createElement('span');
+ suite('_getPatchNumByLineAndContent', () => {
+ test('right side', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('right');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
});
- suite('_getPatchNumByLineAndContent', () => {
- test('right side', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('right');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side parent by linenum', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('left');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side parent by content', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- contentEl.classList.add('remove');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side merge parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: -2};
- contentEl.classList.add('remove');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 4);
- });
-
- test('left side non parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 3};
- contentEl.classList.add('remove');
- assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
- 3);
- });
+ test('left side parent by linenum', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('left');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
});
- suite('_getIsParentCommentByLineAndContent', () => {
- test('right side', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('right');
- assert.isFalse(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
+ test('left side parent by content', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ contentEl.classList.add('remove');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
+ });
- test('left side parent by linenum', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- lineEl.classList.add('left');
- assert.isTrue(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
+ test('left side merge parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: -2};
+ contentEl.classList.add('remove');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 4);
+ });
- test('left side parent by content', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
- contentEl.classList.add('remove');
- assert.isTrue(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
-
- test('left side merge parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: -2};
- contentEl.classList.add('remove');
- assert.isTrue(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
-
- test('left side non parent', () => {
- element.patchRange = {patchNum: 4, basePatchNum: 3};
- contentEl.classList.add('remove');
- assert.isFalse(
- element._getIsParentCommentByLineAndContent(lineEl, contentEl));
- });
+ test('left side non parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 3};
+ contentEl.classList.add('remove');
+ assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl),
+ 3);
});
});
- suite('not logged in', () => {
- setup(() => {
- const getLoggedInPromise = Promise.resolve(false);
- stub('gr-rest-api-interface', {
- getLoggedIn() { return getLoggedInPromise; },
- });
- element = fixture('basic');
- return getLoggedInPromise;
- });
-
- test('toggleLeftDiff', () => {
- element.toggleLeftDiff();
- assert.isTrue(element.classList.contains('no-left'));
- element.toggleLeftDiff();
- assert.isFalse(element.classList.contains('no-left'));
- });
-
- test('addDraftAtLine', () => {
- sandbox.stub(element, '_selectLine');
- const loggedInErrorSpy = sandbox.spy();
- element.addEventListener('show-auth-required', loggedInErrorSpy);
- element.addDraftAtLine();
- assert.isTrue(loggedInErrorSpy.called);
- });
-
- test('view does not start with displayLine classList', () => {
+ suite('_getIsParentCommentByLineAndContent', () => {
+ test('right side', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('right');
assert.isFalse(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- test('displayLine class added called when displayLine is true', () => {
- const spy = sandbox.spy(element, '_computeContainerClass');
- element.displayLine = true;
- assert.isTrue(spy.called);
+ test('left side parent by linenum', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ lineEl.classList.add('left');
assert.isTrue(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- test('thread groups', () => {
- const contentEl = document.createElement('div');
-
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
- element.path = 'file.txt';
-
- const mock = document.createElement('mock-diff-response');
- element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
- mock.diffResponse, Object.assign({}, MINIMAL_PREFS));
-
- // No thread groups.
- assert.isNotOk(element._getThreadGroupForLine(contentEl));
-
- // A thread group gets created.
- const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
- assert.isOk(threadGroupEl);
-
- // The new thread group can be fetched.
- assert.isOk(element._getThreadGroupForLine(contentEl));
-
- assert.equal(contentEl.querySelectorAll('.thread-group').length, 1);
+ test('left side parent by content', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'};
+ contentEl.classList.add('remove');
+ assert.isTrue(
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
- setup(() => {
- mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
- type: 'image/bmp',
- };
- mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
- type: 'image/bmp',
- };
-
- element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
- element.isImageDiff = true;
- element.prefs = {
- auto_hide_diff_table_header: true,
- context: 10,
- cursor_blink_rate: 0,
- font_size: 12,
- ignore_whitespace: 'IGNORE_NONE',
- intraline_difference: true,
- line_length: 100,
- line_wrapping: false,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- };
- });
-
- test('renders image diffs with same file name', done => {
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isNotOk(rightLabelName);
- assert.isNotOk(leftLabelName);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.revisionImage = mockFile2;
- element.diff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- });
-
- test('renders image diffs with a different file name', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot2.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot2.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
-
- const rendered = () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isOk(rightLabelName);
- assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
-
- let leftLoaded = false;
- let rightLoaded = false;
-
- leftImage.addEventListener('load', () => {
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- leftLoaded = true;
- if (rightLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
-
- rightImage.addEventListener('load', () => {
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
-
- rightLoaded = true;
- if (leftLoaded) {
- element.removeEventListener('render', rendered);
- done();
- }
- });
- };
-
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.baseImage._name = mockDiff.meta_a.name;
- element.revisionImage = mockFile2;
- element.revisionImage._name = mockDiff.meta_b.name;
- element.diff = mockDiff;
- });
-
- test('renders added image', done => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'ADDED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 0000000..f9c2f2c 100644',
- '--- /dev/null',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
-
- function rendered() {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
- assert.isNotOk(leftImage);
- assert.isOk(rightImage);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
-
- element.revisionImage = mockFile2;
- element.diff = mockDiff;
- });
-
- test('renders removed image', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
-
- function rendered() {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
- assert.isOk(leftImage);
- assert.isNotOk(rightImage);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.diff = mockDiff;
- });
-
- test('does not render disallowed image type', done => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- mockFile1.type = 'image/jpeg-evil';
-
- function rendered() {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- assert.isNotOk(leftImage);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
-
- element.baseImage = mockFile1;
- element.diff = mockDiff;
- });
+ test('left side merge parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: -2};
+ contentEl.classList.add('remove');
+ assert.isTrue(
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
- test('_handleTap lineNum', done => {
- const addDraftStub = sandbox.stub(element, 'addDraftAtLine');
- const el = document.createElement('div');
- el.className = 'lineNum';
- el.addEventListener('click', e => {
- element._handleTap(e);
- assert.isTrue(addDraftStub.called);
- assert.equal(addDraftStub.lastCall.args[0], el);
- done();
- });
- el.click();
+ test('left side non parent', () => {
+ element.patchRange = {patchNum: 4, basePatchNum: 3};
+ contentEl.classList.add('remove');
+ assert.isFalse(
+ element._getIsParentCommentByLineAndContent(lineEl, contentEl));
});
+ });
+ });
- test('_handleTap context', done => {
- const showContextStub =
- sandbox.stub(element.$.diffBuilder, 'showContext');
- const el = document.createElement('div');
- el.className = 'showContext';
- el.addEventListener('click', e => {
- element._handleTap(e);
- assert.isTrue(showContextStub.called);
- done();
- });
- el.click();
+ suite('not logged in', () => {
+ setup(() => {
+ const getLoggedInPromise = Promise.resolve(false);
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return getLoggedInPromise; },
});
+ element = fixture('basic');
+ return getLoggedInPromise;
+ });
- test('_handleTap content', done => {
- const content = document.createElement('div');
- const lineEl = document.createElement('div');
+ test('toggleLeftDiff', () => {
+ element.toggleLeftDiff();
+ assert.isTrue(element.classList.contains('no-left'));
+ element.toggleLeftDiff();
+ assert.isFalse(element.classList.contains('no-left'));
+ });
- const selectStub = sandbox.stub(element, '_selectLine');
- sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl);
+ test('addDraftAtLine', () => {
+ sandbox.stub(element, '_selectLine');
+ const loggedInErrorSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
+ element.addDraftAtLine();
+ assert.isTrue(loggedInErrorSpy.called);
+ });
- content.className = 'content';
- content.addEventListener('click', e => {
- element._handleTap(e);
- assert.isTrue(selectStub.called);
- assert.equal(selectStub.lastCall.args[0], lineEl);
- done();
- });
- content.click();
- });
+ test('view does not start with displayLine classList', () => {
+ assert.isFalse(
+ element.shadowRoot
+ .querySelector('.diffContainer')
+ .classList
+ .contains('displayLine'));
+ });
- suite('getCursorStops', () => {
- const setupDiff = function() {
- const mock = document.createElement('mock-diff-response');
- element.diff = mock.diffResponse;
- element.prefs = {
- context: 10,
- tab_size: 8,
- font_size: 12,
- line_length: 100,
- cursor_blink_rate: 0,
- line_wrapping: false,
- intraline_difference: true,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- auto_hide_diff_table_header: true,
- theme: 'DEFAULT',
- ignore_whitespace: 'IGNORE_NONE',
- };
+ test('displayLine class added called when displayLine is true', () => {
+ const spy = sandbox.spy(element, '_computeContainerClass');
+ element.displayLine = true;
+ assert.isTrue(spy.called);
+ assert.isTrue(
+ element.shadowRoot
+ .querySelector('.diffContainer')
+ .classList
+ .contains('displayLine'));
+ });
- element._renderDiffTable();
- flushAsynchronousOperations();
+ test('thread groups', () => {
+ const contentEl = document.createElement('div');
+
+ element.changeNum = 123;
+ element.patchRange = {basePatchNum: 1, patchNum: 2};
+ element.path = 'file.txt';
+
+ const mock = document.createElement('mock-diff-response');
+ element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
+ mock.diffResponse, Object.assign({}, MINIMAL_PREFS));
+
+ // No thread groups.
+ assert.isNotOk(element._getThreadGroupForLine(contentEl));
+
+ // A thread group gets created.
+ const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
+ assert.isOk(threadGroupEl);
+
+ // The new thread group can be fetched.
+ assert.isOk(element._getThreadGroupForLine(contentEl));
+
+ assert.equal(contentEl.querySelectorAll('.thread-group').length, 1);
+ });
+
+ suite('image diffs', () => {
+ let mockFile1;
+ let mockFile2;
+ setup(() => {
+ mockFile1 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
+ type: 'image/bmp',
+ };
+ mockFile2 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
+ type: 'image/bmp',
};
- test('getCursorStops returns [] when hidden and noAutoRender', () => {
- element.noAutoRender = true;
- setupDiff();
- element.hidden = true;
- assert.equal(element.getCursorStops().length, 0);
- });
-
- test('getCursorStops', () => {
- setupDiff();
- assert.equal(element.getCursorStops().length, 50);
- });
- });
-
- test('adds .hiddenscroll', () => {
- Gerrit.hiddenscroll = true;
- element.displayLine = true;
- assert.include(element.shadowRoot
- .querySelector('.diffContainer').className, 'hiddenscroll');
- });
- });
-
- suite('logged in', () => {
- let fakeLineEl;
- setup(() => {
- element = fixture('basic');
- element.loggedIn = true;
- element.patchRange = {};
-
- fakeLineEl = {
- getAttribute: sandbox.stub().returns(42),
- classList: {
- contains: sandbox.stub().returns(true),
- },
- };
- });
-
- test('addDraftAtLine', () => {
- sandbox.stub(element, '_selectLine');
- sandbox.stub(element, '_createComment');
- element.addDraftAtLine(fakeLineEl);
- assert.isTrue(element._createComment
- .calledWithExactly(fakeLineEl, 42));
- });
-
- test('addDraftAtLine on an edit', () => {
- element.patchRange.basePatchNum = element.EDIT_NAME;
- sandbox.stub(element, '_selectLine');
- sandbox.stub(element, '_createComment');
- const alertSpy = sandbox.spy();
- element.addEventListener('show-alert', alertSpy);
- element.addDraftAtLine(fakeLineEl);
- assert.isTrue(alertSpy.called);
- assert.isFalse(element._createComment.called);
- });
-
- test('addDraftAtLine on an edit base', () => {
- element.patchRange.patchNum = element.EDIT_NAME;
- element.patchRange.basePatchNum = element.PARENT_NAME;
- sandbox.stub(element, '_selectLine');
- sandbox.stub(element, '_createComment');
- const alertSpy = sandbox.spy();
- element.addEventListener('show-alert', alertSpy);
- element.addDraftAtLine(fakeLineEl);
- assert.isTrue(alertSpy.called);
- assert.isFalse(element._createComment.called);
- });
-
- suite('change in preferences', () => {
- setup(() => {
- element.diff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- diff_header: [],
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- content: [{skip: 66}],
- };
- element.flushDebouncer('renderDiffTable');
- });
-
- test('change in preferences re-renders diff', () => {
- sandbox.stub(element, '_renderDiffTable');
- element.prefs = Object.assign(
- {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
- element.flushDebouncer('renderDiffTable');
- assert.isTrue(element._renderDiffTable.called);
- });
-
- test('change in preferences does not re-renders diff with ' +
- 'noRenderOnPrefsChange', () => {
- sandbox.stub(element, '_renderDiffTable');
- element.noRenderOnPrefsChange = true;
- element.prefs = Object.assign(
- {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
- element.flushDebouncer('renderDiffTable');
- assert.isFalse(element._renderDiffTable.called);
- });
- });
- });
-
- suite('diff header', () => {
- setup(() => {
- element = fixture('basic');
- element.diff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- diff_header: [],
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- content: [{skip: 66}],
- };
- });
-
- test('hidden', () => {
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'index 2adc47d..f9c2f2c 100644');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', '--- a/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', '+++ b/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'test');
- assert.equal(element._diffHeaderItems.length, 1);
- flushAsynchronousOperations();
-
- assert.equal(element.$.diffHeader.textContent.trim(), 'test');
- });
-
- test('binary files', () => {
- element.diff.binary = true;
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
- assert.equal(element._diffHeaderItems.length, 0);
- element.push('diff.diff_header', 'test');
- assert.equal(element._diffHeaderItems.length, 1);
- element.push('diff.diff_header', 'Binary files differ');
- assert.equal(element._diffHeaderItems.length, 1);
- });
- });
-
- suite('safety and bypass', () => {
- let renderStub;
-
- setup(() => {
- element = fixture('basic');
- renderStub = sandbox.stub(element.$.diffBuilder, 'render',
- () => {
- element.$.diffBuilder.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return Promise.resolve({});
- });
- const mock = document.createElement('mock-diff-response');
- sandbox.stub(element, 'getDiffLength').returns(10000);
- element.diff = mock.diffResponse;
- element.noRenderOnPrefsChange = true;
- });
-
- test('large render w/ context = 10', done => {
- element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10});
- function rendered() {
- assert.isTrue(renderStub.called);
- assert.isFalse(element._showWarning);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
- element._renderDiffTable();
- });
-
- test('large render w/ whole file and bypass', done => {
- element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
- element._safetyBypass = 10;
- function rendered() {
- assert.isTrue(renderStub.called);
- assert.isFalse(element._showWarning);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
- element._renderDiffTable();
- });
-
- test('large render w/ whole file and no bypass', done => {
- element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
- function rendered() {
- assert.isFalse(renderStub.called);
- assert.isTrue(element._showWarning);
- done();
- element.removeEventListener('render', rendered);
- }
- element.addEventListener('render', rendered);
- element._renderDiffTable();
- });
- });
-
- suite('blame', () => {
- setup(() => {
- element = fixture('basic');
- });
-
- test('unsetting', () => {
- element.blame = [];
- const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame');
- element.classList.add('showBlame');
- element.blame = null;
- assert.isTrue(setBlameSpy.calledWithExactly(null));
- assert.isFalse(element.classList.contains('showBlame'));
- });
-
- test('setting', () => {
- const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
- element.blame = mockBlame;
- assert.isTrue(element.classList.contains('showBlame'));
- });
- });
-
- suite('trailing newline warnings', () => {
- const NO_NEWLINE_BASE = 'No newline at end of base file.';
- const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
-
- const getWarning = element =>
- element.shadowRoot.querySelector('.newlineWarning').textContent;
-
- setup(() => {
- element = fixture('basic');
- element.showNewlineWarningLeft = false;
- element.showNewlineWarningRight = false;
- });
-
- test('shows combined warning if both sides set to warn', () => {
- element.showNewlineWarningLeft = true;
- element.showNewlineWarningRight = true;
- assert.include(getWarning(element),
- NO_NEWLINE_BASE + ' — ' + NO_NEWLINE_REVISION);
- });
-
- suite('showNewlineWarningLeft', () => {
- test('show warning if true', () => {
- element.showNewlineWarningLeft = true;
- assert.include(getWarning(element), NO_NEWLINE_BASE);
- });
-
- test('hide warning if false', () => {
- element.showNewlineWarningLeft = false;
- assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
- });
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningLeft = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
- });
- });
-
- suite('showNewlineWarningRight', () => {
- test('show warning if true', () => {
- element.showNewlineWarningRight = true;
- assert.include(getWarning(element), NO_NEWLINE_REVISION);
- });
-
- test('hide warning if false', () => {
- element.showNewlineWarningRight = false;
- assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
- });
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningRight = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
- });
- });
-
- test('_computeNewlineWarningClass', () => {
- const hidden = 'newlineWarning hidden';
- const shown = 'newlineWarning';
- assert.equal(element._computeNewlineWarningClass(null, true), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', true), hidden);
- assert.equal(element._computeNewlineWarningClass(null, false), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', false), shown);
- });
- });
-
- suite('key locations', () => {
- let renderStub;
-
- setup(() => {
- element = fixture('basic');
- element.prefs = {};
- renderStub = sandbox.stub(element.$.diffBuilder, 'render')
- .returns(new Promise(() => {}));
- });
-
- test('lineOfInterest is a key location', () => {
- element.lineOfInterest = {number: 789, leftSide: true};
- element._renderDiffTable();
- assert.isTrue(renderStub.called);
- assert.deepEqual(renderStub.lastCall.args[0], {
- left: {789: true},
- right: {},
- });
- });
-
- test('line comments are key locations', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'right');
- threadEl.setAttribute('line-num', 3);
- Polymer.dom(element).appendChild(threadEl);
- Polymer.dom.flush();
-
- element._renderDiffTable();
- assert.isTrue(renderStub.called);
- assert.deepEqual(renderStub.lastCall.args[0], {
- left: {},
- right: {3: true},
- });
- });
-
- test('file comments are key locations', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('comment-side', 'left');
- Polymer.dom(element).appendChild(threadEl);
- Polymer.dom.flush();
-
- element._renderDiffTable();
- assert.isTrue(renderStub.called);
- assert.deepEqual(renderStub.lastCall.args[0], {
- left: {FILE: true},
- right: {},
- });
- });
- });
-
- suite('whitespace changes only message', () => {
- const setupDiff = function(ignore_whitespace, diffContent) {
- element = fixture('basic');
+ element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+ element.isImageDiff = true;
element.prefs = {
- ignore_whitespace,
auto_hide_diff_table_header: true,
context: 10,
cursor_blink_rate: 0,
font_size: 12,
+ ignore_whitespace: 'IGNORE_NONE',
intraline_difference: true,
line_length: 100,
line_wrapping: false,
@@ -976,98 +300,788 @@
tab_size: 8,
theme: 'DEFAULT',
};
+ });
+ test('renders image diffs with same file name', done => {
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage =
+ element.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isNotOk(rightLabelName);
+ assert.isNotOk(leftLabelName);
+
+ let leftLoaded = false;
+ let rightLoaded = false;
+
+ leftImage.addEventListener('load', () => {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.revisionImage = mockFile2;
element.diff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
- 'diff --git a/carrot.js b/carrot.js',
+ 'diff --git a/carrot.jpg b/carrot.jpg',
'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.js',
- '+++ b/carrot.jjs',
- 'file differ',
+ '--- a/carrot.jpg',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
],
- content: diffContent,
+ content: [{skip: 66}],
binary: true,
};
+ });
+
+ test('renders image diffs with a different file name', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot2.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot2.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+
+ const rendered = () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const leftLabel =
+ element.$.diffTable.querySelector('td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage =
+ element.$.diffTable.querySelector('td.right img');
+ const rightLabel = element.$.diffTable.querySelector(
+ 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isOk(rightLabelName);
+ assert.isOk(leftLabelName);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+
+ let leftLoaded = false;
+ let rightLoaded = false;
+
+ leftImage.addEventListener('load', () => {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', () => {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.baseImage._name = mockDiff.meta_a.name;
+ element.revisionImage = mockFile2;
+ element.revisionImage._name = mockDiff.meta_b.name;
+ element.diff = mockDiff;
+ });
+
+ test('renders added image', done => {
+ const mockDiff = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'ADDED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 0000000..f9c2f2c 100644',
+ '--- /dev/null',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+
+ function rendered() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const rightImage = element.$.diffTable.querySelector('td.right img');
+
+ assert.isNotOk(leftImage);
+ assert.isOk(rightImage);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+
+ element.revisionImage = mockFile2;
+ element.diff = mockDiff;
+ });
+
+ test('renders removed image', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+
+ function rendered() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ const rightImage = element.$.diffTable.querySelector('td.right img');
+
+ assert.isOk(leftImage);
+ assert.isNotOk(rightImage);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.diff = mockDiff;
+ });
+
+ test('does not render disallowed image type', done => {
+ const mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ mockFile1.type = 'image/jpeg-evil';
+
+ function rendered() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+ const leftImage = element.$.diffTable.querySelector('td.left img');
+ assert.isNotOk(leftImage);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+
+ element.baseImage = mockFile1;
+ element.diff = mockDiff;
+ });
+ });
+
+ test('_handleTap lineNum', done => {
+ const addDraftStub = sandbox.stub(element, 'addDraftAtLine');
+ const el = document.createElement('div');
+ el.className = 'lineNum';
+ el.addEventListener('click', e => {
+ element._handleTap(e);
+ assert.isTrue(addDraftStub.called);
+ assert.equal(addDraftStub.lastCall.args[0], el);
+ done();
+ });
+ el.click();
+ });
+
+ test('_handleTap context', done => {
+ const showContextStub =
+ sandbox.stub(element.$.diffBuilder, 'showContext');
+ const el = document.createElement('div');
+ el.className = 'showContext';
+ el.addEventListener('click', e => {
+ element._handleTap(e);
+ assert.isTrue(showContextStub.called);
+ done();
+ });
+ el.click();
+ });
+
+ test('_handleTap content', done => {
+ const content = document.createElement('div');
+ const lineEl = document.createElement('div');
+
+ const selectStub = sandbox.stub(element, '_selectLine');
+ sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl);
+
+ content.className = 'content';
+ content.addEventListener('click', e => {
+ element._handleTap(e);
+ assert.isTrue(selectStub.called);
+ assert.equal(selectStub.lastCall.args[0], lineEl);
+ done();
+ });
+ content.click();
+ });
+
+ suite('getCursorStops', () => {
+ const setupDiff = function() {
+ const mock = document.createElement('mock-diff-response');
+ element.diff = mock.diffResponse;
+ element.prefs = {
+ context: 10,
+ tab_size: 8,
+ font_size: 12,
+ line_length: 100,
+ cursor_blink_rate: 0,
+ line_wrapping: false,
+ intraline_difference: true,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ auto_hide_diff_table_header: true,
+ theme: 'DEFAULT',
+ ignore_whitespace: 'IGNORE_NONE',
+ };
element._renderDiffTable();
flushAsynchronousOperations();
};
- test('show the message if ignore_whitespace is criteria matches', () => {
- setupDiff('IGNORE_ALL', [{skip: 100}]);
- assert.isTrue(element.showNoChangeMessage(
- /* loading= */ false,
- element.prefs,
- element._diffLength
- ));
+ test('getCursorStops returns [] when hidden and noAutoRender', () => {
+ element.noAutoRender = true;
+ setupDiff();
+ element.hidden = true;
+ assert.equal(element.getCursorStops().length, 0);
});
- test('do not show the message if still loading', () => {
- setupDiff('IGNORE_ALL', [{skip: 100}]);
- assert.isFalse(element.showNoChangeMessage(
- /* loading= */ true,
- element.prefs,
- element._diffLength
- ));
- });
-
- test('do not show the message if contains valid changes', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
- setupDiff('IGNORE_ALL', content);
- assert.equal(element._diffLength, 3);
- assert.isFalse(element.showNoChangeMessage(
- /* loading= */ false,
- element.prefs,
- element._diffLength
- ));
- });
-
- test('do not show message if ignore whitespace is disabled', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
- setupDiff('IGNORE_NONE', content);
- assert.isFalse(element.showNoChangeMessage(
- /* loading= */ false,
- element.prefs,
- element._diffLength
- ));
+ test('getCursorStops', () => {
+ setupDiff();
+ assert.equal(element.getCursorStops().length, 50);
});
});
- test('getDiffLength', () => {
- const diff = document.createElement('mock-diff-response').diffResponse;
- assert.equal(element.getDiffLength(diff), 52);
+ test('adds .hiddenscroll', () => {
+ Gerrit.hiddenscroll = true;
+ element.displayLine = true;
+ assert.include(element.shadowRoot
+ .querySelector('.diffContainer').className, 'hiddenscroll');
});
+ });
- test('`render` event has contentRendered field in detail', done => {
+ suite('logged in', () => {
+ let fakeLineEl;
+ setup(() => {
element = fixture('basic');
- element.prefs = {};
- sandbox.stub(element.$.diffBuilder, 'render')
- .returns(Promise.resolve());
- element.addEventListener('render', event => {
- assert.isTrue(event.detail.contentRendered);
- done();
+ element.loggedIn = true;
+ element.patchRange = {};
+
+ fakeLineEl = {
+ getAttribute: sandbox.stub().returns(42),
+ classList: {
+ contains: sandbox.stub().returns(true),
+ },
+ };
+ });
+
+ test('addDraftAtLine', () => {
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_createComment');
+ element.addDraftAtLine(fakeLineEl);
+ assert.isTrue(element._createComment
+ .calledWithExactly(fakeLineEl, 42));
+ });
+
+ test('addDraftAtLine on an edit', () => {
+ element.patchRange.basePatchNum = element.EDIT_NAME;
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_createComment');
+ const alertSpy = sandbox.spy();
+ element.addEventListener('show-alert', alertSpy);
+ element.addDraftAtLine(fakeLineEl);
+ assert.isTrue(alertSpy.called);
+ assert.isFalse(element._createComment.called);
+ });
+
+ test('addDraftAtLine on an edit base', () => {
+ element.patchRange.patchNum = element.EDIT_NAME;
+ element.patchRange.basePatchNum = element.PARENT_NAME;
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_createComment');
+ const alertSpy = sandbox.spy();
+ element.addEventListener('show-alert', alertSpy);
+ element.addDraftAtLine(fakeLineEl);
+ assert.isTrue(alertSpy.called);
+ assert.isFalse(element._createComment.called);
+ });
+
+ suite('change in preferences', () => {
+ setup(() => {
+ element.diff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ diff_header: [],
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [{skip: 66}],
+ };
+ element.flushDebouncer('renderDiffTable');
});
+
+ test('change in preferences re-renders diff', () => {
+ sandbox.stub(element, '_renderDiffTable');
+ element.prefs = Object.assign(
+ {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+ element.flushDebouncer('renderDiffTable');
+ assert.isTrue(element._renderDiffTable.called);
+ });
+
+ test('change in preferences does not re-renders diff with ' +
+ 'noRenderOnPrefsChange', () => {
+ sandbox.stub(element, '_renderDiffTable');
+ element.noRenderOnPrefsChange = true;
+ element.prefs = Object.assign(
+ {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+ element.flushDebouncer('renderDiffTable');
+ assert.isFalse(element._renderDiffTable.called);
+ });
+ });
+ });
+
+ suite('diff header', () => {
+ setup(() => {
+ element = fixture('basic');
+ element.diff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ diff_header: [],
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [{skip: 66}],
+ };
+ });
+
+ test('hidden', () => {
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'index 2adc47d..f9c2f2c 100644');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', '--- a/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', '+++ b/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'test');
+ assert.equal(element._diffHeaderItems.length, 1);
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.diffHeader.textContent.trim(), 'test');
+ });
+
+ test('binary files', () => {
+ element.diff.binary = true;
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
+ assert.equal(element._diffHeaderItems.length, 0);
+ element.push('diff.diff_header', 'test');
+ assert.equal(element._diffHeaderItems.length, 1);
+ element.push('diff.diff_header', 'Binary files differ');
+ assert.equal(element._diffHeaderItems.length, 1);
+ });
+ });
+
+ suite('safety and bypass', () => {
+ let renderStub;
+
+ setup(() => {
+ element = fixture('basic');
+ renderStub = sandbox.stub(element.$.diffBuilder, 'render',
+ () => {
+ element.$.diffBuilder.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true}));
+ return Promise.resolve({});
+ });
+ const mock = document.createElement('mock-diff-response');
+ sandbox.stub(element, 'getDiffLength').returns(10000);
+ element.diff = mock.diffResponse;
+ element.noRenderOnPrefsChange = true;
+ });
+
+ test('large render w/ context = 10', done => {
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10});
+ function rendered() {
+ assert.isTrue(renderStub.called);
+ assert.isFalse(element._showWarning);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+ element._renderDiffTable();
+ });
+
+ test('large render w/ whole file and bypass', done => {
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
+ element._safetyBypass = 10;
+ function rendered() {
+ assert.isTrue(renderStub.called);
+ assert.isFalse(element._showWarning);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
+ element._renderDiffTable();
+ });
+
+ test('large render w/ whole file and no bypass', done => {
+ element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
+ function rendered() {
+ assert.isFalse(renderStub.called);
+ assert.isTrue(element._showWarning);
+ done();
+ element.removeEventListener('render', rendered);
+ }
+ element.addEventListener('render', rendered);
element._renderDiffTable();
});
});
- a11ySuite('basic');
+ suite('blame', () => {
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('unsetting', () => {
+ element.blame = [];
+ const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame');
+ element.classList.add('showBlame');
+ element.blame = null;
+ assert.isTrue(setBlameSpy.calledWithExactly(null));
+ assert.isFalse(element.classList.contains('showBlame'));
+ });
+
+ test('setting', () => {
+ const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ element.blame = mockBlame;
+ assert.isTrue(element.classList.contains('showBlame'));
+ });
+ });
+
+ suite('trailing newline warnings', () => {
+ const NO_NEWLINE_BASE = 'No newline at end of base file.';
+ const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
+
+ const getWarning = element =>
+ element.shadowRoot.querySelector('.newlineWarning').textContent;
+
+ setup(() => {
+ element = fixture('basic');
+ element.showNewlineWarningLeft = false;
+ element.showNewlineWarningRight = false;
+ });
+
+ test('shows combined warning if both sides set to warn', () => {
+ element.showNewlineWarningLeft = true;
+ element.showNewlineWarningRight = true;
+ assert.include(getWarning(element),
+ NO_NEWLINE_BASE + ' — ' + NO_NEWLINE_REVISION);
+ });
+
+ suite('showNewlineWarningLeft', () => {
+ test('show warning if true', () => {
+ element.showNewlineWarningLeft = true;
+ assert.include(getWarning(element), NO_NEWLINE_BASE);
+ });
+
+ test('hide warning if false', () => {
+ element.showNewlineWarningLeft = false;
+ assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
+ });
+
+ test('hide warning if undefined', () => {
+ element.showNewlineWarningLeft = undefined;
+ assert.notInclude(getWarning(element), NO_NEWLINE_BASE);
+ });
+ });
+
+ suite('showNewlineWarningRight', () => {
+ test('show warning if true', () => {
+ element.showNewlineWarningRight = true;
+ assert.include(getWarning(element), NO_NEWLINE_REVISION);
+ });
+
+ test('hide warning if false', () => {
+ element.showNewlineWarningRight = false;
+ assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
+ });
+
+ test('hide warning if undefined', () => {
+ element.showNewlineWarningRight = undefined;
+ assert.notInclude(getWarning(element), NO_NEWLINE_REVISION);
+ });
+ });
+
+ test('_computeNewlineWarningClass', () => {
+ const hidden = 'newlineWarning hidden';
+ const shown = 'newlineWarning';
+ assert.equal(element._computeNewlineWarningClass(null, true), hidden);
+ assert.equal(element._computeNewlineWarningClass('foo', true), hidden);
+ assert.equal(element._computeNewlineWarningClass(null, false), hidden);
+ assert.equal(element._computeNewlineWarningClass('foo', false), shown);
+ });
+ });
+
+ suite('key locations', () => {
+ let renderStub;
+
+ setup(() => {
+ element = fixture('basic');
+ element.prefs = {};
+ renderStub = sandbox.stub(element.$.diffBuilder, 'render')
+ .returns(new Promise(() => {}));
+ });
+
+ test('lineOfInterest is a key location', () => {
+ element.lineOfInterest = {number: 789, leftSide: true};
+ element._renderDiffTable();
+ assert.isTrue(renderStub.called);
+ assert.deepEqual(renderStub.lastCall.args[0], {
+ left: {789: true},
+ right: {},
+ });
+ });
+
+ test('line comments are key locations', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'right');
+ threadEl.setAttribute('line-num', 3);
+ dom(element).appendChild(threadEl);
+ flush();
+
+ element._renderDiffTable();
+ assert.isTrue(renderStub.called);
+ assert.deepEqual(renderStub.lastCall.args[0], {
+ left: {},
+ right: {3: true},
+ });
+ });
+
+ test('file comments are key locations', () => {
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('comment-side', 'left');
+ dom(element).appendChild(threadEl);
+ flush();
+
+ element._renderDiffTable();
+ assert.isTrue(renderStub.called);
+ assert.deepEqual(renderStub.lastCall.args[0], {
+ left: {FILE: true},
+ right: {},
+ });
+ });
+ });
+
+ suite('whitespace changes only message', () => {
+ const setupDiff = function(ignore_whitespace, diffContent) {
+ element = fixture('basic');
+ element.prefs = {
+ ignore_whitespace,
+ auto_hide_diff_table_header: true,
+ context: 10,
+ cursor_blink_rate: 0,
+ font_size: 12,
+ intraline_difference: true,
+ line_length: 100,
+ line_wrapping: false,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ };
+
+ element.diff = {
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.js b/carrot.js',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.js',
+ '+++ b/carrot.jjs',
+ 'file differ',
+ ],
+ content: diffContent,
+ binary: true,
+ };
+
+ element._renderDiffTable();
+ flushAsynchronousOperations();
+ };
+
+ test('show the message if ignore_whitespace is criteria matches', () => {
+ setupDiff('IGNORE_ALL', [{skip: 100}]);
+ assert.isTrue(element.showNoChangeMessage(
+ /* loading= */ false,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+
+ test('do not show the message if still loading', () => {
+ setupDiff('IGNORE_ALL', [{skip: 100}]);
+ assert.isFalse(element.showNoChangeMessage(
+ /* loading= */ true,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+
+ test('do not show the message if contains valid changes', () => {
+ const content = [{
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ }, {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ }];
+ setupDiff('IGNORE_ALL', content);
+ assert.equal(element._diffLength, 3);
+ assert.isFalse(element.showNoChangeMessage(
+ /* loading= */ false,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+
+ test('do not show message if ignore whitespace is disabled', () => {
+ const content = [{
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ }, {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ }];
+ setupDiff('IGNORE_NONE', content);
+ assert.isFalse(element.showNoChangeMessage(
+ /* loading= */ false,
+ element.prefs,
+ element._diffLength
+ ));
+ });
+ });
+
+ test('getDiffLength', () => {
+ const diff = document.createElement('mock-diff-response').diffResponse;
+ assert.equal(element.getDiffLength(diff), 52);
+ });
+
+ test('`render` event has contentRendered field in detail', done => {
+ element = fixture('basic');
+ element.prefs = {};
+ sandbox.stub(element.$.diffBuilder, 'render')
+ .returns(Promise.resolve());
+ element.addEventListener('render', event => {
+ assert.isTrue(event.detail.contentRendered);
+ done();
+ });
+ element._renderDiffTable();
+ });
+});
+
+a11ySuite('basic');
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index d24e0bc..f2b0599 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.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,278 +14,290 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Maximum length for patch set descriptions.
- const PATCH_DESC_MAX_LENGTH = 500;
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
+import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import '../../shared/gr-select/gr-select.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-patch-range-select_html.js';
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- */
- /**
- * Fired when the patch range changes
- *
- * @event patch-range-change
- *
- * @property {string} patchNum
- * @property {string} basePatchNum
- * @extends Polymer.Element
- */
- class GrPatchRangeSelect extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-patch-range-select'; }
+// Maximum length for patch set descriptions.
+const PATCH_DESC_MAX_LENGTH = 500;
- static get properties() {
- return {
- availablePatches: Array,
- _baseDropdownContent: {
- type: Object,
- computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
- '_sortedRevisions, changeComments, revisionInfo)',
- },
- _patchDropdownContent: {
- type: Object,
- computed: '_computePatchDropdownContent(availablePatches,' +
- 'basePatchNum, _sortedRevisions, changeComments)',
- },
- changeNum: String,
- changeComments: Object,
- /** @type {{ meta_a: !Array, meta_b: !Array}} */
- filesWeblinks: Object,
- patchNum: String,
- basePatchNum: String,
- revisions: Object,
- revisionInfo: Object,
- _sortedRevisions: Array,
- };
- }
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ */
+/**
+ * Fired when the patch range changes
+ *
+ * @event patch-range-change
+ *
+ * @property {string} patchNum
+ * @property {string} basePatchNum
+ * @extends Polymer.Element
+ */
+class GrPatchRangeSelect extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_updateSortedRevisions(revisions.*)',
- ];
- }
+ static get is() { return 'gr-patch-range-select'; }
- _getShaForPatch(patch) {
- return patch.sha.substring(0, 10);
- }
-
- _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
- changeComments, revisionInfo) {
- // Polymer 2: check for undefined
- if ([
- availablePatches,
- patchNum,
- _sortedRevisions,
- changeComments,
- revisionInfo,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const parentCounts = revisionInfo.getParentCountMap();
- const currentParentCount = parentCounts.hasOwnProperty(patchNum) ?
- parentCounts[patchNum] : 1;
- const maxParents = revisionInfo.getMaxParents();
- const isMerge = currentParentCount > 1;
-
- const dropdownContent = [];
- for (const basePatch of availablePatches) {
- const basePatchNum = basePatch.num;
- const entry = this._createDropdownEntry(basePatchNum, 'Patchset ',
- _sortedRevisions, changeComments, this._getShaForPatch(basePatch));
- dropdownContent.push(Object.assign({}, entry, {
- disabled: this._computeLeftDisabled(
- basePatch.num, patchNum, _sortedRevisions),
- }));
- }
-
- dropdownContent.push({
- text: isMerge ? 'Auto Merge' : 'Base',
- value: 'PARENT',
- });
-
- for (let idx = 0; isMerge && idx < maxParents; idx++) {
- dropdownContent.push({
- disabled: idx >= currentParentCount,
- triggerText: `Parent ${idx + 1}`,
- text: `Parent ${idx + 1}`,
- mobileText: `Parent ${idx + 1}`,
- value: -(idx + 1),
- });
- }
-
- return dropdownContent;
- }
-
- _computeMobileText(patchNum, changeComments, revisions) {
- return `${patchNum}` +
- `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
- `${this._computePatchSetDescription(revisions, patchNum, true)}`;
- }
-
- _computePatchDropdownContent(availablePatches, basePatchNum,
- _sortedRevisions, changeComments) {
- // Polymer 2: check for undefined
- if ([
- availablePatches,
- basePatchNum,
- _sortedRevisions,
- changeComments,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- const dropdownContent = [];
- for (const patch of availablePatches) {
- const patchNum = patch.num;
- const entry = this._createDropdownEntry(
- patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions,
- changeComments, this._getShaForPatch(patch));
- dropdownContent.push(Object.assign({}, entry, {
- disabled: this._computeRightDisabled(basePatchNum, patchNum,
- _sortedRevisions),
- }));
- }
- return dropdownContent;
- }
-
- _computeText(patchNum, prefix, changeComments, sha) {
- return `${prefix}${patchNum}` +
- `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
- (` | ${sha}`);
- }
-
- _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments,
- sha) {
- const entry = {
- triggerText: `${prefix}${patchNum}`,
- text: this._computeText(patchNum, prefix, changeComments, sha),
- mobileText: this._computeMobileText(patchNum, changeComments,
- sortedRevisions),
- bottomText: `${this._computePatchSetDescription(
- sortedRevisions, patchNum)}`,
- value: patchNum,
- };
- const date = this._computePatchSetDate(sortedRevisions, patchNum);
- if (date) {
- entry['date'] = date;
- }
- return entry;
- }
-
- _updateSortedRevisions(revisionsRecord) {
- const revisions = revisionsRecord.base;
- this._sortedRevisions = this.sortRevisions(Object.values(revisions));
- }
-
- /**
- * The basePatchNum should always be <= patchNum -- because sortedRevisions
- * is sorted in reverse order (higher patchset nums first), invalid base
- * patch nums have an index greater than the index of patchNum.
- *
- * @param {number|string} basePatchNum The possible base patch num.
- * @param {number|string} patchNum The current selected patch num.
- * @param {!Array} sortedRevisions
- */
- _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
- return this.findSortedIndex(basePatchNum, sortedRevisions) <=
- this.findSortedIndex(patchNum, sortedRevisions);
- }
-
- /**
- * The basePatchNum should always be <= patchNum -- because sortedRevisions
- * is sorted in reverse order (higher patchset nums first), invalid patch
- * nums have an index greater than the index of basePatchNum.
- *
- * In addition, if the current basePatchNum is 'PARENT', all patchNums are
- * valid.
- *
- * If the curent basePatchNum is a parent index, then only patches that have
- * at least that many parents are valid.
- *
- * @param {number|string} basePatchNum The current selected base patch num.
- * @param {number|string} patchNum The possible patch num.
- * @param {!Array} sortedRevisions
- * @return {boolean}
- */
- _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) {
- if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; }
-
- if (this.isMergeParent(basePatchNum)) {
- // Note: parent indices use 1-offset.
- return this.revisionInfo.getParentCount(patchNum) <
- this.getParentIndex(basePatchNum);
- }
-
- return this.findSortedIndex(basePatchNum, sortedRevisions) <=
- this.findSortedIndex(patchNum, sortedRevisions);
- }
-
- _computePatchSetCommentsString(changeComments, patchNum) {
- if (!changeComments) { return; }
-
- const commentCount = changeComments.computeCommentCount(patchNum);
- const commentString = GrCountStringFormatter.computePluralString(
- commentCount, 'comment');
-
- const unresolvedCount = changeComments.computeUnresolvedNum(patchNum);
- const unresolvedString = GrCountStringFormatter.computeString(
- unresolvedCount, 'unresolved');
-
- if (!commentString.length && !unresolvedString.length) {
- return '';
- }
-
- return ` (${commentString}` +
- // Add a comma + space if both comments and unresolved
- (commentString && unresolvedString ? ', ' : '') +
- `${unresolvedString})`;
- }
-
- /**
- * @param {!Array} revisions
- * @param {number|string} patchNum
- * @param {boolean=} opt_addFrontSpace
- */
- _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
- const rev = this.getRevisionByPatchNum(revisions, patchNum);
- return (rev && rev.description) ?
- (opt_addFrontSpace ? ' ' : '') +
- rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
- }
-
- /**
- * @param {!Array} revisions
- * @param {number|string} patchNum
- */
- _computePatchSetDate(revisions, patchNum) {
- const rev = this.getRevisionByPatchNum(revisions, patchNum);
- return rev ? rev.created : undefined;
- }
-
- /**
- * Catches value-change events from the patchset dropdowns and determines
- * whether or not a patch change event should be fired.
- */
- _handlePatchChange(e) {
- const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum};
- const target = Polymer.dom(e).localTarget;
-
- if (target === this.$.patchNumDropdown) {
- detail.patchNum = e.detail.value;
- } else {
- detail.basePatchNum = e.detail.value;
- }
-
- this.dispatchEvent(
- new CustomEvent('patch-range-change', {detail, bubbles: false}));
- }
+ static get properties() {
+ return {
+ availablePatches: Array,
+ _baseDropdownContent: {
+ type: Object,
+ computed: '_computeBaseDropdownContent(availablePatches, patchNum,' +
+ '_sortedRevisions, changeComments, revisionInfo)',
+ },
+ _patchDropdownContent: {
+ type: Object,
+ computed: '_computePatchDropdownContent(availablePatches,' +
+ 'basePatchNum, _sortedRevisions, changeComments)',
+ },
+ changeNum: String,
+ changeComments: Object,
+ /** @type {{ meta_a: !Array, meta_b: !Array}} */
+ filesWeblinks: Object,
+ patchNum: String,
+ basePatchNum: String,
+ revisions: Object,
+ revisionInfo: Object,
+ _sortedRevisions: Array,
+ };
}
- customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect);
-})();
+ static get observers() {
+ return [
+ '_updateSortedRevisions(revisions.*)',
+ ];
+ }
+
+ _getShaForPatch(patch) {
+ return patch.sha.substring(0, 10);
+ }
+
+ _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions,
+ changeComments, revisionInfo) {
+ // Polymer 2: check for undefined
+ if ([
+ availablePatches,
+ patchNum,
+ _sortedRevisions,
+ changeComments,
+ revisionInfo,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const parentCounts = revisionInfo.getParentCountMap();
+ const currentParentCount = parentCounts.hasOwnProperty(patchNum) ?
+ parentCounts[patchNum] : 1;
+ const maxParents = revisionInfo.getMaxParents();
+ const isMerge = currentParentCount > 1;
+
+ const dropdownContent = [];
+ for (const basePatch of availablePatches) {
+ const basePatchNum = basePatch.num;
+ const entry = this._createDropdownEntry(basePatchNum, 'Patchset ',
+ _sortedRevisions, changeComments, this._getShaForPatch(basePatch));
+ dropdownContent.push(Object.assign({}, entry, {
+ disabled: this._computeLeftDisabled(
+ basePatch.num, patchNum, _sortedRevisions),
+ }));
+ }
+
+ dropdownContent.push({
+ text: isMerge ? 'Auto Merge' : 'Base',
+ value: 'PARENT',
+ });
+
+ for (let idx = 0; isMerge && idx < maxParents; idx++) {
+ dropdownContent.push({
+ disabled: idx >= currentParentCount,
+ triggerText: `Parent ${idx + 1}`,
+ text: `Parent ${idx + 1}`,
+ mobileText: `Parent ${idx + 1}`,
+ value: -(idx + 1),
+ });
+ }
+
+ return dropdownContent;
+ }
+
+ _computeMobileText(patchNum, changeComments, revisions) {
+ return `${patchNum}` +
+ `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
+ `${this._computePatchSetDescription(revisions, patchNum, true)}`;
+ }
+
+ _computePatchDropdownContent(availablePatches, basePatchNum,
+ _sortedRevisions, changeComments) {
+ // Polymer 2: check for undefined
+ if ([
+ availablePatches,
+ basePatchNum,
+ _sortedRevisions,
+ changeComments,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ const dropdownContent = [];
+ for (const patch of availablePatches) {
+ const patchNum = patch.num;
+ const entry = this._createDropdownEntry(
+ patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions,
+ changeComments, this._getShaForPatch(patch));
+ dropdownContent.push(Object.assign({}, entry, {
+ disabled: this._computeRightDisabled(basePatchNum, patchNum,
+ _sortedRevisions),
+ }));
+ }
+ return dropdownContent;
+ }
+
+ _computeText(patchNum, prefix, changeComments, sha) {
+ return `${prefix}${patchNum}` +
+ `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
+ (` | ${sha}`);
+ }
+
+ _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments,
+ sha) {
+ const entry = {
+ triggerText: `${prefix}${patchNum}`,
+ text: this._computeText(patchNum, prefix, changeComments, sha),
+ mobileText: this._computeMobileText(patchNum, changeComments,
+ sortedRevisions),
+ bottomText: `${this._computePatchSetDescription(
+ sortedRevisions, patchNum)}`,
+ value: patchNum,
+ };
+ const date = this._computePatchSetDate(sortedRevisions, patchNum);
+ if (date) {
+ entry['date'] = date;
+ }
+ return entry;
+ }
+
+ _updateSortedRevisions(revisionsRecord) {
+ const revisions = revisionsRecord.base;
+ this._sortedRevisions = this.sortRevisions(Object.values(revisions));
+ }
+
+ /**
+ * The basePatchNum should always be <= patchNum -- because sortedRevisions
+ * is sorted in reverse order (higher patchset nums first), invalid base
+ * patch nums have an index greater than the index of patchNum.
+ *
+ * @param {number|string} basePatchNum The possible base patch num.
+ * @param {number|string} patchNum The current selected patch num.
+ * @param {!Array} sortedRevisions
+ */
+ _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
+ return this.findSortedIndex(basePatchNum, sortedRevisions) <=
+ this.findSortedIndex(patchNum, sortedRevisions);
+ }
+
+ /**
+ * The basePatchNum should always be <= patchNum -- because sortedRevisions
+ * is sorted in reverse order (higher patchset nums first), invalid patch
+ * nums have an index greater than the index of basePatchNum.
+ *
+ * In addition, if the current basePatchNum is 'PARENT', all patchNums are
+ * valid.
+ *
+ * If the curent basePatchNum is a parent index, then only patches that have
+ * at least that many parents are valid.
+ *
+ * @param {number|string} basePatchNum The current selected base patch num.
+ * @param {number|string} patchNum The possible patch num.
+ * @param {!Array} sortedRevisions
+ * @return {boolean}
+ */
+ _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) {
+ if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; }
+
+ if (this.isMergeParent(basePatchNum)) {
+ // Note: parent indices use 1-offset.
+ return this.revisionInfo.getParentCount(patchNum) <
+ this.getParentIndex(basePatchNum);
+ }
+
+ return this.findSortedIndex(basePatchNum, sortedRevisions) <=
+ this.findSortedIndex(patchNum, sortedRevisions);
+ }
+
+ _computePatchSetCommentsString(changeComments, patchNum) {
+ if (!changeComments) { return; }
+
+ const commentCount = changeComments.computeCommentCount(patchNum);
+ const commentString = GrCountStringFormatter.computePluralString(
+ commentCount, 'comment');
+
+ const unresolvedCount = changeComments.computeUnresolvedNum(patchNum);
+ const unresolvedString = GrCountStringFormatter.computeString(
+ unresolvedCount, 'unresolved');
+
+ if (!commentString.length && !unresolvedString.length) {
+ return '';
+ }
+
+ return ` (${commentString}` +
+ // Add a comma + space if both comments and unresolved
+ (commentString && unresolvedString ? ', ' : '') +
+ `${unresolvedString})`;
+ }
+
+ /**
+ * @param {!Array} revisions
+ * @param {number|string} patchNum
+ * @param {boolean=} opt_addFrontSpace
+ */
+ _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
+ const rev = this.getRevisionByPatchNum(revisions, patchNum);
+ return (rev && rev.description) ?
+ (opt_addFrontSpace ? ' ' : '') +
+ rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
+ }
+
+ /**
+ * @param {!Array} revisions
+ * @param {number|string} patchNum
+ */
+ _computePatchSetDate(revisions, patchNum) {
+ const rev = this.getRevisionByPatchNum(revisions, patchNum);
+ return rev ? rev.created : undefined;
+ }
+
+ /**
+ * Catches value-change events from the patchset dropdowns and determines
+ * whether or not a patch change event should be fired.
+ */
+ _handlePatchChange(e) {
+ const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum};
+ const target = dom(e).localTarget;
+
+ if (target === this.$.patchNumDropdown) {
+ detail.patchNum = e.detail.value;
+ } else {
+ detail.basePatchNum = e.detail.value;
+ }
+
+ this.dispatchEvent(
+ new CustomEvent('patch-range-change', {detail, bubbles: false}));
+ }
+}
+
+customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect);
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
index ee1f536..5779a90 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_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="/bower_components/polymer/polymer.html">
-
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
-<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-patch-range-select">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
align-items: center;
@@ -59,34 +52,22 @@
}
</style>
<span class="patchRange">
- <gr-dropdown-list
- id="basePatchDropdown"
- value="[[basePatchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_baseDropdownContent]]">
+ <gr-dropdown-list id="basePatchDropdown" value="[[basePatchNum]]" on-value-change="_handlePatchChange" items="[[_baseDropdownContent]]">
</gr-dropdown-list>
</span>
<span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
<template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
- <a target="_blank" rel="noopener"
- href$="[[weblink.url]]">[[weblink.name]]</a>
+ <a target="_blank" rel="noopener" href\$="[[weblink.url]]">[[weblink.name]]</a>
</template>
</span>
- <span class="arrow">→</span>
+ <span class="arrow">→</span>
<span class="patchRange">
- <gr-dropdown-list
- id="patchNumDropdown"
- value="[[patchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_patchDropdownContent]]">
+ <gr-dropdown-list id="patchNumDropdown" value="[[patchNum]]" on-value-change="_handlePatchChange" items="[[_patchDropdownContent]]">
</gr-dropdown-list>
<span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
<template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
- <a target="_blank"
- href$="[[weblink.url]]">[[weblink.name]]</a>
+ <a target="_blank" href\$="[[weblink.url]]">[[weblink.name]]</a>
</template>
</span>
</span>
- </template>
- <script src="gr-patch-range-select.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 65dedef..3c07750 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -19,21 +19,30 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-patch-range-select</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="../../diff/gr-comment-api/gr-comment-api.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="../../shared/revision-info/revision-info.html">
+<script type="module" src="../gr-comment-api/gr-comment-api.js"></script>
+<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script>
+<script type="module" src="../../shared/revision-info/revision-info.js"></script>
-<link rel="import" href="gr-patch-range-select.html">
+<script type="module" src="./gr-patch-range-select.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-comment-api/gr-comment-api.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import '../../shared/revision-info/revision-info.js';
+import './gr-patch-range-select.js';
+import '../gr-comment-api/gr-comment-api-mock_test.js';
+void(0);
+</script>
<dom-module id="comment-api-mock">
<template>
@@ -41,7 +50,7 @@
change-comments="[[_changeComments]]"></gr-patch-range-select>
<gr-comment-api id="commentAPI"></gr-comment-api>
</template>
- <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script>
+ <script type="module" src="../gr-comment-api/gr-comment-api-mock_test.js"></script>
</dom-module>
<test-fixture id="basic">
@@ -50,384 +59,391 @@
</template>
</test-fixture>
-<script>
- suite('gr-patch-range-select tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let commentApiWrapper;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-comment-api/gr-comment-api.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import '../../shared/revision-info/revision-info.js';
+import './gr-patch-range-select.js';
+import '../gr-comment-api/gr-comment-api-mock_test.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-patch-range-select tests', () => {
+ let element;
+ let sandbox;
+ let commentApiWrapper;
- function getInfo(revisions) {
- const revisionObj = {};
- for (let i = 0; i < revisions.length; i++) {
- revisionObj[i] = revisions[i];
- }
- return new Gerrit.RevisionInfo({revisions: revisionObj});
+ function getInfo(revisions) {
+ const revisionObj = {};
+ for (let i = 0; i < revisions.length; i++) {
+ revisionObj[i] = revisions[i];
}
+ return new Gerrit.RevisionInfo({revisions: revisionObj});
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getDiffComments() { return Promise.resolve({}); },
- getDiffRobotComments() { return Promise.resolve({}); },
- getDiffDrafts() { return Promise.resolve({}); },
+ stub('gr-rest-api-interface', {
+ getDiffComments() { return Promise.resolve({}); },
+ getDiffRobotComments() { return Promise.resolve({}); },
+ getDiffDrafts() { return Promise.resolve({}); },
+ });
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ commentApiWrapper = fixture('basic');
+ element = commentApiWrapper.$.patchRange;
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ return commentApiWrapper.loadComments();
+ });
+
+ teardown(() => sandbox.restore());
+
+ test('enabled/disabled options', () => {
+ const patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
+ const sortedRevisions = [
+ {_number: 3},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 2},
+ {_number: 1},
+ ];
+ for (const patchNum of ['1', '2', '3']) {
+ assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum,
+ patchNum, sortedRevisions));
+ }
+ for (const basePatchNum of ['1', '2']) {
+ assert.isFalse(element._computeLeftDisabled(basePatchNum,
+ patchRange.patchNum, sortedRevisions));
+ }
+ assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
+
+ patchRange.basePatchNum = element.EDIT_NAME;
+ assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
+ sortedRevisions));
+ assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
+ sortedRevisions));
+ assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2',
+ sortedRevisions));
+ assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
+ sortedRevisions));
+ assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
+ element.EDIT_NAME, sortedRevisions));
+ });
+
+ test('_computeBaseDropdownContent', () => {
+ const availablePatches = [
+ {num: 'edit', sha: '1'},
+ {num: 3, sha: '2'},
+ {num: 2, sha: '3'},
+ {num: 1, sha: '4'},
+ ];
+ const revisions = [
+ {
+ commit: {parents: []},
+ _number: 2,
+ description: 'description',
+ },
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(revisions);
+ const patchNum = 1;
+ const sortedRevisions = [
+ {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 2, description: 'description'},
+ {_number: 1},
+ ];
+ const expectedResult = [
+ {
+ disabled: true,
+ triggerText: 'Patchset edit',
+ text: 'Patchset edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: 'Mon, 01 Jan 2001 00:00:00 GMT',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ },
+ {
+ text: 'Base',
+ value: 'PARENT',
+ },
+ ];
+ assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
+ patchNum, sortedRevisions, element.changeComments,
+ element.revisionInfo),
+ expectedResult);
+ });
+
+ test('_computeBaseDropdownContent called when patchNum updates', () => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'},
+ {num: 2, sha: '2'},
+ {num: 3, sha: '3'},
+ {num: 'edit', sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
+
+ sandbox.stub(element, '_computeBaseDropdownContent');
+
+ // Should be recomputed for each available patch
+ element.set('patchNum', 1);
+ assert.equal(element._computeBaseDropdownContent.callCount, 1);
+ });
+
+ test('_computeBaseDropdownContent called when changeComments update',
+ done => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 'edit', sha: '1'},
+ {num: 3, sha: '2'},
+ {num: 2, sha: '3'},
+ {num: 1, sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
+
+ // Should be recomputed for each available patch
+ sandbox.stub(element, '_computeBaseDropdownContent');
+ assert.equal(element._computeBaseDropdownContent.callCount, 0);
+ commentApiWrapper.loadComments().then()
+ .then(() => {
+ assert.equal(element._computeBaseDropdownContent.callCount, 1);
+ done();
+ });
});
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = fixture('basic');
- element = commentApiWrapper.$.patchRange;
+ test('_computePatchDropdownContent called when basePatchNum updates', () => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'},
+ {num: 2, sha: '2'},
+ {num: 3, sha: '3'},
+ {num: 'edit', sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- return commentApiWrapper.loadComments();
- });
-
- teardown(() => sandbox.restore());
-
- test('enabled/disabled options', () => {
- const patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '3',
- };
- const sortedRevisions = [
- {_number: 3},
- {_number: element.EDIT_NAME, basePatchNum: 2},
- {_number: 2},
- {_number: 1},
- ];
- for (const patchNum of ['1', '2', '3']) {
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum,
- patchNum, sortedRevisions));
- }
- for (const basePatchNum of ['1', '2']) {
- assert.isFalse(element._computeLeftDisabled(basePatchNum,
- patchRange.patchNum, sortedRevisions));
- }
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
-
- patchRange.basePatchNum = element.EDIT_NAME;
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2',
- sortedRevisions));
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
- element.EDIT_NAME, sortedRevisions));
- });
-
- test('_computeBaseDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const revisions = [
- {
- commit: {parents: []},
- _number: 2,
- description: 'description',
- },
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(revisions);
- const patchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: element.EDIT_NAME, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
- const expectedResult = [
- {
- disabled: true,
- triggerText: 'Patchset edit',
- text: 'Patchset edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: true,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: true,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- {
- text: 'Base',
- value: 'PARENT',
- },
- ];
- assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
- patchNum, sortedRevisions, element.changeComments,
- element.revisionInfo),
- expectedResult);
- });
-
- test('_computeBaseDropdownContent called when patchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- sandbox.stub(element, '_computeBaseDropdownContent');
-
- // Should be recomputed for each available patch
- element.set('patchNum', 1);
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computeBaseDropdownContent called when changeComments update',
- done => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computeBaseDropdownContent');
- assert.equal(element._computeBaseDropdownContent.callCount, 0);
- commentApiWrapper.loadComments().then()
- .then(() => {
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- done();
- });
- });
-
- test('_computePatchDropdownContent called when basePatchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computePatchDropdownContent');
- element.set('basePatchNum', 1);
- assert.equal(element._computePatchDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent called when comments update', done => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flushAsynchronousOperations();
-
- // Should be recomputed for each available patch
- sandbox.stub(element, '_computePatchDropdownContent');
- assert.equal(element._computePatchDropdownContent.callCount, 0);
- commentApiWrapper.loadComments().then()
- .then(() => {
- done();
- });
- });
-
- test('_computePatchDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const basePatchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: element.EDIT_NAME, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
-
- const expectedResult = [
- {
- disabled: false,
- triggerText: 'edit',
- text: 'edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: false,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: false,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- ];
-
- assert.deepEqual(element._computePatchDropdownContent(availablePatches,
- basePatchNum, sortedRevisions, element.changeComments),
- expectedResult);
- });
-
- test('filesWeblinks', () => {
- element.filesWeblinks = {
- meta_a: [
- {
- name: 'foo',
- url: 'f.oo',
- },
- ],
- meta_b: [
- {
- name: 'bar',
- url: 'ba.r',
- },
- ],
- };
- flushAsynchronousOperations();
- const domApi = Polymer.dom(element.root);
- assert.equal(
- domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
- assert.equal(
- domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
- });
-
- test('_computePatchSetCommentsString', () => {
- // Test string with unresolved comments.
- element.changeComments._comments = {
- foo: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- }],
- bar: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-12 20:48:40.000000000',
- },
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-13 20:48:40.000000000',
- }],
- abc: [],
- };
-
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (3 comments, 1 unresolved)');
-
- // Test string with no unresolved comments.
- delete element.changeComments._comments['foo'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (2 comments)');
-
- // Test string with no comments.
- delete element.changeComments._comments['bar'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), '');
- });
-
- test('patch-range-change fires', () => {
- const handler = sandbox.stub();
- element.basePatchNum = 1;
- element.patchNum = 3;
- element.addEventListener('patch-range-change', handler);
-
- element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]);
- assert.isTrue(handler.calledOnce);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 2, patchNum: 3});
-
- // BasePatchNum should not have changed, due to one-way data binding.
- element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 1, patchNum: 'edit'});
- });
+ // Should be recomputed for each available patch
+ sandbox.stub(element, '_computePatchDropdownContent');
+ element.set('basePatchNum', 1);
+ assert.equal(element._computePatchDropdownContent.callCount, 1);
});
+
+ test('_computePatchDropdownContent called when comments update', done => {
+ element.revisions = [
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ {commit: {parents: []}},
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'},
+ {num: 2, sha: '2'},
+ {num: 3, sha: '3'},
+ {num: 'edit', sha: '4'},
+ ];
+ element.patchNum = 2;
+ element.basePatchNum = 'PARENT';
+ flushAsynchronousOperations();
+
+ // Should be recomputed for each available patch
+ sandbox.stub(element, '_computePatchDropdownContent');
+ assert.equal(element._computePatchDropdownContent.callCount, 0);
+ commentApiWrapper.loadComments().then()
+ .then(() => {
+ done();
+ });
+ });
+
+ test('_computePatchDropdownContent', () => {
+ const availablePatches = [
+ {num: 'edit', sha: '1'},
+ {num: 3, sha: '2'},
+ {num: 2, sha: '3'},
+ {num: 1, sha: '4'},
+ ];
+ const basePatchNum = 1;
+ const sortedRevisions = [
+ {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
+ {_number: element.EDIT_NAME, basePatchNum: 2},
+ {_number: 2, description: 'description'},
+ {_number: 1},
+ ];
+
+ const expectedResult = [
+ {
+ disabled: false,
+ triggerText: 'edit',
+ text: 'edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: 'Mon, 01 Jan 2001 00:00:00 GMT',
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ },
+ ];
+
+ assert.deepEqual(element._computePatchDropdownContent(availablePatches,
+ basePatchNum, sortedRevisions, element.changeComments),
+ expectedResult);
+ });
+
+ test('filesWeblinks', () => {
+ element.filesWeblinks = {
+ meta_a: [
+ {
+ name: 'foo',
+ url: 'f.oo',
+ },
+ ],
+ meta_b: [
+ {
+ name: 'bar',
+ url: 'ba.r',
+ },
+ ],
+ };
+ flushAsynchronousOperations();
+ const domApi = dom(element.root);
+ assert.equal(
+ domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
+ assert.equal(
+ domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
+ });
+
+ test('_computePatchSetCommentsString', () => {
+ // Test string with unresolved comments.
+ element.changeComments._comments = {
+ foo: [{
+ id: '27dcee4d_f7b77cfa',
+ message: 'test',
+ patch_set: 1,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000',
+ }],
+ bar: [{
+ id: '27dcee4d_f7b77cfa',
+ message: 'test',
+ patch_set: 1,
+ updated: '2017-10-12 20:48:40.000000000',
+ },
+ {
+ id: '27dcee4d_f7b77cfa',
+ message: 'test',
+ patch_set: 1,
+ updated: '2017-10-13 20:48:40.000000000',
+ }],
+ abc: [],
+ };
+
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), ' (3 comments, 1 unresolved)');
+
+ // Test string with no unresolved comments.
+ delete element.changeComments._comments['foo'];
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), ' (2 comments)');
+
+ // Test string with no comments.
+ delete element.changeComments._comments['bar'];
+ assert.equal(element._computePatchSetCommentsString(
+ element.changeComments, 1), '');
+ });
+
+ test('patch-range-change fires', () => {
+ const handler = sandbox.stub();
+ element.basePatchNum = 1;
+ element.patchNum = 3;
+ element.addEventListener('patch-range-change', handler);
+
+ element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]);
+ assert.isTrue(handler.calledOnce);
+ assert.deepEqual(handler.lastCall.args[0].detail,
+ {basePatchNum: 2, patchNum: 3});
+
+ // BasePatchNum should not have changed, due to one-way data binding.
+ element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]);
+ assert.deepEqual(handler.lastCall.args[0].detail,
+ {basePatchNum: 1, patchNum: 'edit'});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index fd94b61..8f1b1c3 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -14,204 +14,210 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Polymer 1 adds # before array's key, while Polymer 2 doesn't
- const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
+import '../gr-diff-highlight/gr-annotation.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-ranged-comment-layer_html.js';
- const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
- const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
+// Polymer 1 adds # before array's key, while Polymer 2 doesn't
+const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
- /** @extends Polymer.Element */
- class GrRangedCommentLayer extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-ranged-comment-layer'; }
- /**
- * Fired when the range in a range comment was malformed and had to be
- * normalized.
- *
- * It's `detail` has a `lineNum` and `side` parameter.
- *
- * @event normalize-range
- */
+const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
+const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
- static get properties() {
- return {
- /** @type {!Array<!Gerrit.HoveredRange>} */
- commentRanges: Array,
- _listeners: {
- type: Array,
- value() { return []; },
- },
- _rangesMap: {
- type: Object,
- value() { return {left: {}, right: {}}; },
- },
- };
+/** @extends Polymer.Element */
+class GrRangedCommentLayer extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-ranged-comment-layer'; }
+ /**
+ * Fired when the range in a range comment was malformed and had to be
+ * normalized.
+ *
+ * It's `detail` has a `lineNum` and `side` parameter.
+ *
+ * @event normalize-range
+ */
+
+ static get properties() {
+ return {
+ /** @type {!Array<!Gerrit.HoveredRange>} */
+ commentRanges: Array,
+ _listeners: {
+ type: Array,
+ value() { return []; },
+ },
+ _rangesMap: {
+ type: Object,
+ value() { return {left: {}, right: {}}; },
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_handleCommentRangesChange(commentRanges.*)',
+ ];
+ }
+
+ get styleModuleName() {
+ return 'gr-ranged-comment-styles';
+ }
+
+ /**
+ * Layer method to add annotations to a line.
+ *
+ * @param {!HTMLElement} el The DIV.contentText element to apply the
+ * annotation to.
+ * @param {!HTMLElement} lineNumberEl
+ * @param {!Object} line The line object. (GrDiffLine)
+ */
+ annotate(el, lineNumberEl, line) {
+ let ranges = [];
+ if (line.type === GrDiffLine.Type.REMOVE || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'right')) {
+ ranges = ranges.concat(this._getRangesForLine(line, 'left'));
+ }
+ if (line.type === GrDiffLine.Type.ADD || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'left')) {
+ ranges = ranges.concat(this._getRangesForLine(line, 'right'));
}
- static get observers() {
- return [
- '_handleCommentRangesChange(commentRanges.*)',
- ];
+ for (const range of ranges) {
+ GrAnnotation.annotateElement(el, range.start,
+ range.end - range.start,
+ range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT);
}
+ }
- get styleModuleName() {
- return 'gr-ranged-comment-styles';
+ /**
+ * Register a listener for layer updates.
+ *
+ * @param {function(number, number, string)} fn The update handler function.
+ * Should accept as arguments the line numbers for the start and end of
+ * the update and the side as a string.
+ */
+ addListener(fn) {
+ this._listeners.push(fn);
+ }
+
+ /**
+ * Notify Layer listeners of changes to annotations.
+ *
+ * @param {number} start The line where the update starts.
+ * @param {number} end The line where the update ends.
+ * @param {string} side The side of the update. ('left' or 'right')
+ */
+ _notifyUpdateRange(start, end, side) {
+ for (const listener of this._listeners) {
+ listener(start, end, side);
}
+ }
- /**
- * Layer method to add annotations to a line.
- *
- * @param {!HTMLElement} el The DIV.contentText element to apply the
- * annotation to.
- * @param {!HTMLElement} lineNumberEl
- * @param {!Object} line The line object. (GrDiffLine)
- */
- annotate(el, lineNumberEl, line) {
- let ranges = [];
- if (line.type === GrDiffLine.Type.REMOVE || (
- line.type === GrDiffLine.Type.BOTH &&
- el.getAttribute('data-side') !== 'right')) {
- ranges = ranges.concat(this._getRangesForLine(line, 'left'));
- }
- if (line.type === GrDiffLine.Type.ADD || (
- line.type === GrDiffLine.Type.BOTH &&
- el.getAttribute('data-side') !== 'left')) {
- ranges = ranges.concat(this._getRangesForLine(line, 'right'));
- }
+ /**
+ * Handle change in the ranges by updating the ranges maps and by
+ * emitting appropriate update notifications.
+ *
+ * @param {Object} record The change record.
+ */
+ _handleCommentRangesChange(record) {
+ if (!record) return;
- for (const range of ranges) {
- GrAnnotation.annotateElement(el, range.start,
- range.end - range.start,
- range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT);
+ // If the entire set of comments was changed.
+ if (record.path === 'commentRanges') {
+ this._rangesMap = {left: {}, right: {}};
+ for (const {side, range, hovering} of record.value) {
+ this._updateRangesMap(
+ side, range, hovering, (forLine, start, end, hovering) => {
+ forLine.push({start, end, hovering});
+ });
}
}
- /**
- * Register a listener for layer updates.
- *
- * @param {function(number, number, string)} fn The update handler function.
- * Should accept as arguments the line numbers for the start and end of
- * the update and the side as a string.
- */
- addListener(fn) {
- this._listeners.push(fn);
+ // If the change only changed the `hovering` property of a comment.
+ const match = record.path.match(HOVER_PATH_PATTERN);
+ if (match) {
+ // The #number indicates the key of that item in the array
+ // not the index, especially in polymer 1.
+ const {side, range, hovering} = this.get(match[1]);
+
+ this._updateRangesMap(
+ side, range, hovering, (forLine, start, end, hovering) => {
+ const index = forLine.findIndex(lineRange =>
+ lineRange.start === start && lineRange.end === end);
+ forLine[index].hovering = hovering;
+ });
}
- /**
- * Notify Layer listeners of changes to annotations.
- *
- * @param {number} start The line where the update starts.
- * @param {number} end The line where the update ends.
- * @param {string} side The side of the update. ('left' or 'right')
- */
- _notifyUpdateRange(start, end, side) {
- for (const listener of this._listeners) {
- listener(start, end, side);
- }
- }
-
- /**
- * Handle change in the ranges by updating the ranges maps and by
- * emitting appropriate update notifications.
- *
- * @param {Object} record The change record.
- */
- _handleCommentRangesChange(record) {
- if (!record) return;
-
- // If the entire set of comments was changed.
- if (record.path === 'commentRanges') {
- this._rangesMap = {left: {}, right: {}};
- for (const {side, range, hovering} of record.value) {
+ // If comments were spliced in or out.
+ if (record.path === 'commentRanges.splices') {
+ for (const indexSplice of record.value.indexSplices) {
+ const removed = indexSplice.removed;
+ for (const {side, range, hovering} of removed) {
+ this._updateRangesMap(
+ side, range, hovering, (forLine, start, end) => {
+ const index = forLine.findIndex(lineRange =>
+ lineRange.start === start && lineRange.end === end);
+ forLine.splice(index, 1);
+ });
+ }
+ const added = indexSplice.object.slice(
+ indexSplice.index, indexSplice.index + indexSplice.addedCount);
+ for (const {side, range, hovering} of added) {
this._updateRangesMap(
side, range, hovering, (forLine, start, end, hovering) => {
forLine.push({start, end, hovering});
});
}
}
-
- // If the change only changed the `hovering` property of a comment.
- const match = record.path.match(HOVER_PATH_PATTERN);
- if (match) {
- // The #number indicates the key of that item in the array
- // not the index, especially in polymer 1.
- const {side, range, hovering} = this.get(match[1]);
-
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end, hovering) => {
- const index = forLine.findIndex(lineRange =>
- lineRange.start === start && lineRange.end === end);
- forLine[index].hovering = hovering;
- });
- }
-
- // If comments were spliced in or out.
- if (record.path === 'commentRanges.splices') {
- for (const indexSplice of record.value.indexSplices) {
- const removed = indexSplice.removed;
- for (const {side, range, hovering} of removed) {
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end) => {
- const index = forLine.findIndex(lineRange =>
- lineRange.start === start && lineRange.end === end);
- forLine.splice(index, 1);
- });
- }
- const added = indexSplice.object.slice(
- indexSplice.index, indexSplice.index + indexSplice.addedCount);
- for (const {side, range, hovering} of added) {
- this._updateRangesMap(
- side, range, hovering, (forLine, start, end, hovering) => {
- forLine.push({start, end, hovering});
- });
- }
- }
- }
- }
-
- _updateRangesMap(side, range, hovering, operation) {
- const forSide = this._rangesMap[side] || (this._rangesMap[side] = {});
- for (let line = range.start_line; line <= range.end_line; line++) {
- const forLine = forSide[line] || (forSide[line] = []);
- const start = line === range.start_line ? range.start_character : 0;
- const end = line === range.end_line ? range.end_character : -1;
- operation(forLine, start, end, hovering);
- }
- this._notifyUpdateRange(range.start_line, range.end_line, side);
- }
-
- _getRangesForLine(line, side) {
- const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
- const ranges = this.get(['_rangesMap', side, lineNum]) || [];
- return ranges
- .map(range => {
- // Make a copy, so that the normalization below does not mess with
- // our map.
- range = Object.assign({}, range);
- range.end = range.end === -1 ? line.text.length : range.end;
-
- // Normalize invalid ranges where the start is after the end but the
- // start still makes sense. Set the end to the end of the line.
- // @see Issue 5744
- if (range.start >= range.end && range.start < line.text.length) {
- range.end = line.text.length;
- this.dispatchEvent(new CustomEvent('normalize-range', {
- bubbles: true,
- composed: true,
- detail: {lineNum, side},
- }));
- }
-
- return range;
- })
- // Sort the ranges so that hovering highlights are on top.
- .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0));
}
}
- customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer);
-})();
+ _updateRangesMap(side, range, hovering, operation) {
+ const forSide = this._rangesMap[side] || (this._rangesMap[side] = {});
+ for (let line = range.start_line; line <= range.end_line; line++) {
+ const forLine = forSide[line] || (forSide[line] = []);
+ const start = line === range.start_line ? range.start_character : 0;
+ const end = line === range.end_line ? range.end_character : -1;
+ operation(forLine, start, end, hovering);
+ }
+ this._notifyUpdateRange(range.start_line, range.end_line, side);
+ }
+
+ _getRangesForLine(line, side) {
+ const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
+ const ranges = this.get(['_rangesMap', side, lineNum]) || [];
+ return ranges
+ .map(range => {
+ // Make a copy, so that the normalization below does not mess with
+ // our map.
+ range = Object.assign({}, range);
+ range.end = range.end === -1 ? line.text.length : range.end;
+
+ // Normalize invalid ranges where the start is after the end but the
+ // start still makes sense. Set the end to the end of the line.
+ // @see Issue 5744
+ if (range.start >= range.end && range.start < line.text.length) {
+ range.end = line.text.length;
+ this.dispatchEvent(new CustomEvent('normalize-range', {
+ bubbles: true,
+ composed: true,
+ detail: {lineNum, side},
+ }));
+ }
+
+ return range;
+ })
+ // Sort the ranges so that hovering highlights are on top.
+ .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0));
+ }
+}
+
+customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
index 7625c8a..29757e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
@@ -1,25 +1,21 @@
-<!--
-@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
+export const htmlTemplate = html`
-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">
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-
-<dom-module id="gr-ranged-comment-layer">
- <template>
- </template>
- <script src="gr-ranged-comment-layer.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index 48883c1..d2d97de 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-ranged-comment-layer</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="../gr-diff/gr-diff-line.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 type="module" src="../gr-diff/gr-diff-line.js"></script>
-<link rel="import" href="gr-ranged-comment-layer.html">
+<script type="module" src="./gr-ranged-comment-layer.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-diff/gr-diff-line.js';
+import './gr-ranged-comment-layer.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,310 +43,313 @@
</template>
</test-fixture>
-<script>
- suite('gr-ranged-comment-layer', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-diff/gr-diff-line.js';
+import './gr-ranged-comment-layer.js';
+suite('gr-ranged-comment-layer', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ const initialCommentRanges = [
+ {
+ side: 'left',
+ range: {
+ end_character: 9,
+ end_line: 39,
+ start_character: 6,
+ start_line: 36,
+ },
+ },
+ {
+ side: 'right',
+ range: {
+ end_character: 22,
+ end_line: 12,
+ start_character: 10,
+ start_line: 10,
+ },
+ },
+ {
+ side: 'right',
+ range: {
+ end_character: 15,
+ end_line: 100,
+ start_character: 5,
+ start_line: 100,
+ },
+ },
+ {
+ side: 'right',
+ range: {
+ end_character: 2,
+ end_line: 55,
+ start_character: 32,
+ start_line: 55,
+ },
+ },
+ ];
+
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.commentRanges = initialCommentRanges;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('annotate', () => {
let sandbox;
+ let el;
+ let line;
+ let annotateElementStub;
+ const lineNumberEl = document.createElement('td');
setup(() => {
- const initialCommentRanges = [
- {
- side: 'left',
- range: {
- end_character: 9,
- end_line: 39,
- start_character: 6,
- start_line: 36,
- },
- },
- {
- side: 'right',
- range: {
- end_character: 22,
- end_line: 12,
- start_character: 10,
- start_line: 10,
- },
- },
- {
- side: 'right',
- range: {
- end_character: 15,
- end_line: 100,
- start_character: 5,
- start_line: 100,
- },
- },
- {
- side: 'right',
- range: {
- end_character: 2,
- end_line: 55,
- start_character: 32,
- start_line: 55,
- },
- },
- ];
-
sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.commentRanges = initialCommentRanges;
+ annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+ el = document.createElement('div');
+ el.setAttribute('data-side', 'left');
+ line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
});
teardown(() => {
sandbox.restore();
});
- suite('annotate', () => {
- let sandbox;
- let el;
- let line;
- let annotateElementStub;
- const lineNumberEl = document.createElement('td');
+ test('type=Remove no-comment', () => {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 40;
- setup(() => {
- sandbox = sinon.sandbox.create();
- annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
- el = document.createElement('div');
- el.setAttribute('data-side', 'left');
- line = new GrDiffLine(GrDiffLine.Type.BOTH);
- line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,';
- });
+ element.annotate(el, lineNumberEl, line);
- teardown(() => {
- sandbox.restore();
- });
-
- test('type=Remove no-comment', () => {
- line.type = GrDiffLine.Type.REMOVE;
- line.beforeNumber = 40;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('type=Remove has-comment', () => {
- line.type = GrDiffLine.Type.REMOVE;
- line.beforeNumber = 36;
- const expectedStart = 6;
- const expectedLength = line.text.length - expectedStart;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(lastCall.args[3], 'style-scope gr-diff range');
- });
-
- test('type=Remove has-comment hovering', () => {
- line.type = GrDiffLine.Type.REMOVE;
- line.beforeNumber = 36;
- element.set(['commentRanges', 0, 'hovering'], true);
-
- const expectedStart = 6;
- const expectedLength = line.text.length - expectedStart;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(lastCall.args[3], 'style-scope gr-diff rangeHighlight');
- });
-
- test('type=Both has-comment', () => {
- line.type = GrDiffLine.Type.BOTH;
- line.beforeNumber = 36;
-
- const expectedStart = 6;
- const expectedLength = line.text.length - expectedStart;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(lastCall.args[3], 'style-scope gr-diff range');
- });
-
- test('type=Both has-comment off side', () => {
- line.type = GrDiffLine.Type.BOTH;
- line.beforeNumber = 36;
- el.setAttribute('data-side', 'right');
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('type=Add has-comment', () => {
- line.type = GrDiffLine.Type.ADD;
- line.afterNumber = 12;
- el.setAttribute('data-side', 'right');
-
- const expectedStart = 0;
- const expectedLength = 22;
-
- element.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementStub.called);
- const lastCall = annotateElementStub.lastCall;
- assert.equal(lastCall.args[0], el);
- assert.equal(lastCall.args[1], expectedStart);
- assert.equal(lastCall.args[2], expectedLength);
- assert.equal(lastCall.args[3], 'style-scope gr-diff range');
- });
+ assert.isFalse(annotateElementStub.called);
});
- test('_handleCommentRangesChange overwrite', () => {
- element.set('commentRanges', []);
+ test('type=Remove has-comment', () => {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 36;
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
- assert.equal(Object.keys(element._rangesMap.left).length, 0);
- assert.equal(Object.keys(element._rangesMap.right).length, 0);
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementStub.called);
+ const lastCall = annotateElementStub.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'style-scope gr-diff range');
});
- test('_handleCommentRangesChange hovering', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
- const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+ test('type=Remove has-comment hovering', () => {
+ line.type = GrDiffLine.Type.REMOVE;
+ line.beforeNumber = 36;
+ element.set(['commentRanges', 0, 'hovering'], true);
- element.set(['commentRanges', 1, 'hovering'], true);
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
- assert.isTrue(notifyStub.called);
- const lastCall = notifyStub.lastCall;
- assert.equal(lastCall.args[0], 10);
- assert.equal(lastCall.args[1], 12);
- assert.equal(lastCall.args[2], 'right');
+ element.annotate(el, lineNumberEl, line);
- assert.isTrue(updateRangesMapSpy.called);
+ assert.isTrue(annotateElementStub.called);
+ const lastCall = annotateElementStub.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'style-scope gr-diff rangeHighlight');
});
- test('_handleCommentRangesChange splice out', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
+ test('type=Both has-comment', () => {
+ line.type = GrDiffLine.Type.BOTH;
+ line.beforeNumber = 36;
- element.splice('commentRanges', 1, 1);
+ const expectedStart = 6;
+ const expectedLength = line.text.length - expectedStart;
- assert.isTrue(notifyStub.called);
- const lastCall = notifyStub.lastCall;
- assert.equal(lastCall.args[0], 10);
- assert.equal(lastCall.args[1], 12);
- assert.equal(lastCall.args[2], 'right');
+ element.annotate(el, lineNumberEl, line);
+
+ assert.isTrue(annotateElementStub.called);
+ const lastCall = annotateElementStub.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'style-scope gr-diff range');
});
- test('_handleCommentRangesChange splice in', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
+ test('type=Both has-comment off side', () => {
+ line.type = GrDiffLine.Type.BOTH;
+ line.beforeNumber = 36;
+ el.setAttribute('data-side', 'right');
- element.splice('commentRanges', 1, 0, {
- side: 'left',
- range: {
- end_character: 15,
- end_line: 275,
- start_character: 5,
- start_line: 250,
- },
- });
+ element.annotate(el, lineNumberEl, line);
- assert.isTrue(notifyStub.called);
- const lastCall = notifyStub.lastCall;
- assert.equal(lastCall.args[0], 250);
- assert.equal(lastCall.args[1], 275);
- assert.equal(lastCall.args[2], 'left');
+ assert.isFalse(annotateElementStub.called);
});
- test('_handleCommentRangesChange mixed actions', () => {
- const notifyStub = sinon.stub();
- element.addListener(notifyStub);
- const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+ test('type=Add has-comment', () => {
+ line.type = GrDiffLine.Type.ADD;
+ line.afterNumber = 12;
+ el.setAttribute('data-side', 'right');
- element.set(['commentRanges', 1, 'hovering'], true);
- assert.isTrue(updateRangesMapSpy.callCount === 1);
- element.splice('commentRanges', 1, 1);
- assert.isTrue(updateRangesMapSpy.callCount === 2);
- element.splice('commentRanges', 1, 1);
- assert.isTrue(updateRangesMapSpy.callCount === 3);
- element.splice('commentRanges', 1, 0, {
- side: 'left',
- range: {
- end_character: 15,
- end_line: 275,
- start_character: 5,
- start_line: 250,
- },
- });
- assert.isTrue(updateRangesMapSpy.callCount === 4);
- element.set(['commentRanges', 2, 'hovering'], true);
- assert.isTrue(updateRangesMapSpy.callCount === 5);
- });
+ const expectedStart = 0;
+ const expectedLength = 22;
- test('_computeCommentMap creates maps correctly', () => {
- // There is only one ranged comment on the left, but it spans ll.36-39.
- const leftKeys = [];
- for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); }
- assert.deepEqual(Object.keys(element._rangesMap.left).sort(),
- leftKeys.sort());
+ element.annotate(el, lineNumberEl, line);
- assert.equal(element._rangesMap.left[36].length, 1);
- assert.equal(element._rangesMap.left[36][0].start, 6);
- assert.equal(element._rangesMap.left[36][0].end, -1);
-
- assert.equal(element._rangesMap.left[37].length, 1);
- assert.equal(element._rangesMap.left[37][0].start, 0);
- assert.equal(element._rangesMap.left[37][0].end, -1);
-
- assert.equal(element._rangesMap.left[38].length, 1);
- assert.equal(element._rangesMap.left[38][0].start, 0);
- assert.equal(element._rangesMap.left[38][0].end, -1);
-
- assert.equal(element._rangesMap.left[39].length, 1);
- assert.equal(element._rangesMap.left[39][0].start, 0);
- assert.equal(element._rangesMap.left[39][0].end, 9);
-
- // The right has two ranged comments, one spanning ll.10-12 and the other
- // on line 100.
- const rightKeys = [];
- for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); }
- rightKeys.push('55', '100');
- assert.deepEqual(Object.keys(element._rangesMap.right).sort(),
- rightKeys.sort());
-
- assert.equal(element._rangesMap.right[10].length, 1);
- assert.equal(element._rangesMap.right[10][0].start, 10);
- assert.equal(element._rangesMap.right[10][0].end, -1);
-
- assert.equal(element._rangesMap.right[11].length, 1);
- assert.equal(element._rangesMap.right[11][0].start, 0);
- assert.equal(element._rangesMap.right[11][0].end, -1);
-
- assert.equal(element._rangesMap.right[12].length, 1);
- assert.equal(element._rangesMap.right[12][0].start, 0);
- assert.equal(element._rangesMap.right[12][0].end, 22);
-
- assert.equal(element._rangesMap.right[100].length, 1);
- assert.equal(element._rangesMap.right[100][0].start, 5);
- assert.equal(element._rangesMap.right[100][0].end, 15);
- });
-
- test('_getRangesForLine normalizes invalid ranges', () => {
- const line = {
- afterNumber: 55,
- text: '_getRangesForLine normalizes invalid ranges',
- };
- const ranges = element._getRangesForLine(line, 'right');
- assert.equal(ranges.length, 1);
- const range = ranges[0];
- assert.isTrue(range.start < range.end, 'start and end are normalized');
- assert.equal(range.end, line.text.length);
+ assert.isTrue(annotateElementStub.called);
+ const lastCall = annotateElementStub.lastCall;
+ assert.equal(lastCall.args[0], el);
+ assert.equal(lastCall.args[1], expectedStart);
+ assert.equal(lastCall.args[2], expectedLength);
+ assert.equal(lastCall.args[3], 'style-scope gr-diff range');
});
});
+
+ test('_handleCommentRangesChange overwrite', () => {
+ element.set('commentRanges', []);
+
+ assert.equal(Object.keys(element._rangesMap.left).length, 0);
+ assert.equal(Object.keys(element._rangesMap.right).length, 0);
+ });
+
+ test('_handleCommentRangesChange hovering', () => {
+ const notifyStub = sinon.stub();
+ element.addListener(notifyStub);
+ const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+
+ element.set(['commentRanges', 1, 'hovering'], true);
+
+ assert.isTrue(notifyStub.called);
+ const lastCall = notifyStub.lastCall;
+ assert.equal(lastCall.args[0], 10);
+ assert.equal(lastCall.args[1], 12);
+ assert.equal(lastCall.args[2], 'right');
+
+ assert.isTrue(updateRangesMapSpy.called);
+ });
+
+ test('_handleCommentRangesChange splice out', () => {
+ const notifyStub = sinon.stub();
+ element.addListener(notifyStub);
+
+ element.splice('commentRanges', 1, 1);
+
+ assert.isTrue(notifyStub.called);
+ const lastCall = notifyStub.lastCall;
+ assert.equal(lastCall.args[0], 10);
+ assert.equal(lastCall.args[1], 12);
+ assert.equal(lastCall.args[2], 'right');
+ });
+
+ test('_handleCommentRangesChange splice in', () => {
+ const notifyStub = sinon.stub();
+ element.addListener(notifyStub);
+
+ element.splice('commentRanges', 1, 0, {
+ side: 'left',
+ range: {
+ end_character: 15,
+ end_line: 275,
+ start_character: 5,
+ start_line: 250,
+ },
+ });
+
+ assert.isTrue(notifyStub.called);
+ const lastCall = notifyStub.lastCall;
+ assert.equal(lastCall.args[0], 250);
+ assert.equal(lastCall.args[1], 275);
+ assert.equal(lastCall.args[2], 'left');
+ });
+
+ test('_handleCommentRangesChange mixed actions', () => {
+ const notifyStub = sinon.stub();
+ element.addListener(notifyStub);
+ const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap');
+
+ element.set(['commentRanges', 1, 'hovering'], true);
+ assert.isTrue(updateRangesMapSpy.callCount === 1);
+ element.splice('commentRanges', 1, 1);
+ assert.isTrue(updateRangesMapSpy.callCount === 2);
+ element.splice('commentRanges', 1, 1);
+ assert.isTrue(updateRangesMapSpy.callCount === 3);
+ element.splice('commentRanges', 1, 0, {
+ side: 'left',
+ range: {
+ end_character: 15,
+ end_line: 275,
+ start_character: 5,
+ start_line: 250,
+ },
+ });
+ assert.isTrue(updateRangesMapSpy.callCount === 4);
+ element.set(['commentRanges', 2, 'hovering'], true);
+ assert.isTrue(updateRangesMapSpy.callCount === 5);
+ });
+
+ test('_computeCommentMap creates maps correctly', () => {
+ // There is only one ranged comment on the left, but it spans ll.36-39.
+ const leftKeys = [];
+ for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); }
+ assert.deepEqual(Object.keys(element._rangesMap.left).sort(),
+ leftKeys.sort());
+
+ assert.equal(element._rangesMap.left[36].length, 1);
+ assert.equal(element._rangesMap.left[36][0].start, 6);
+ assert.equal(element._rangesMap.left[36][0].end, -1);
+
+ assert.equal(element._rangesMap.left[37].length, 1);
+ assert.equal(element._rangesMap.left[37][0].start, 0);
+ assert.equal(element._rangesMap.left[37][0].end, -1);
+
+ assert.equal(element._rangesMap.left[38].length, 1);
+ assert.equal(element._rangesMap.left[38][0].start, 0);
+ assert.equal(element._rangesMap.left[38][0].end, -1);
+
+ assert.equal(element._rangesMap.left[39].length, 1);
+ assert.equal(element._rangesMap.left[39][0].start, 0);
+ assert.equal(element._rangesMap.left[39][0].end, 9);
+
+ // The right has two ranged comments, one spanning ll.10-12 and the other
+ // on line 100.
+ const rightKeys = [];
+ for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); }
+ rightKeys.push('55', '100');
+ assert.deepEqual(Object.keys(element._rangesMap.right).sort(),
+ rightKeys.sort());
+
+ assert.equal(element._rangesMap.right[10].length, 1);
+ assert.equal(element._rangesMap.right[10][0].start, 10);
+ assert.equal(element._rangesMap.right[10][0].end, -1);
+
+ assert.equal(element._rangesMap.right[11].length, 1);
+ assert.equal(element._rangesMap.right[11][0].start, 0);
+ assert.equal(element._rangesMap.right[11][0].end, -1);
+
+ assert.equal(element._rangesMap.right[12].length, 1);
+ assert.equal(element._rangesMap.right[12][0].start, 0);
+ assert.equal(element._rangesMap.right[12][0].end, 22);
+
+ assert.equal(element._rangesMap.right[100].length, 1);
+ assert.equal(element._rangesMap.right[100][0].start, 5);
+ assert.equal(element._rangesMap.right[100][0].end, 15);
+ });
+
+ test('_getRangesForLine normalizes invalid ranges', () => {
+ const line = {
+ afterNumber: 55,
+ text: '_getRangesForLine normalizes invalid ranges',
+ };
+ const ranges = element._getRangesForLine(line, 'right');
+ assert.equal(ranges.length, 1);
+ const range = ranges[0];
+ assert.isTrue(range.start < range.end, 'start and end are normalized');
+ assert.equal(range.end, line.text.length);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
index cefd241..49ed980 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-ranged-comment-theme">
+$_documentContainer.innerHTML = `<dom-module id="gr-ranged-comment-theme">
<template>
<style>
.range {
@@ -27,4 +29,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index 3d831c9..20d3081 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -14,93 +14,104 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-tooltip/gr-tooltip.js';
+import {flush} 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-selection-action-box_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrSelectionActionBox extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-selection-action-box'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the comment creation action was taken (click).
+ *
+ * @event create-comment-requested
*/
- class GrSelectionActionBox extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-selection-action-box'; }
- /**
- * Fired when the comment creation action was taken (click).
- *
- * @event create-comment-requested
- */
- static get properties() {
- return {
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- positionBelow: Boolean,
- };
- }
-
- /** @override */
- created() {
- super.created();
-
- // See https://crbug.com/gerrit/4767
- this.addEventListener('mousedown',
- e => this._handleMouseDown(e));
- }
-
- placeAbove(el) {
- Polymer.dom.flush();
- const rect = this._getTargetBoundingRect(el);
- const boxRect = this.$.tooltip.getBoundingClientRect();
- const parentRect = this._getParentBoundingClientRect();
- this.style.top =
- rect.top - parentRect.top - boxRect.height - 6 + 'px';
- this.style.left =
- rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
- }
-
- placeBelow(el) {
- Polymer.dom.flush();
- const rect = this._getTargetBoundingRect(el);
- const boxRect = this.$.tooltip.getBoundingClientRect();
- const parentRect = this._getParentBoundingClientRect();
- this.style.top =
- rect.top - parentRect.top + boxRect.height - 6 + 'px';
- this.style.left =
- rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
- }
-
- _getParentBoundingClientRect() {
- // With native shadow DOM, the parent is the shadow root, not the gr-diff
- // element
- const parent = this.parentElement || this.parentNode.host;
- return parent.getBoundingClientRect();
- }
-
- _getTargetBoundingRect(el) {
- let rect;
- if (el instanceof Text) {
- const range = document.createRange();
- range.selectNode(el);
- rect = range.getBoundingClientRect();
- range.detach();
- } else {
- rect = el.getBoundingClientRect();
- }
- return rect;
- }
-
- _handleMouseDown(e) {
- if (e.button !== 0) { return; } // 0 = main button
- e.preventDefault();
- e.stopPropagation();
- this.fire('create-comment-requested');
- }
+ static get properties() {
+ return {
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ positionBelow: Boolean,
+ };
}
- customElements.define(GrSelectionActionBox.is, GrSelectionActionBox);
-})();
+ /** @override */
+ created() {
+ super.created();
+
+ // See https://crbug.com/gerrit/4767
+ this.addEventListener('mousedown',
+ e => this._handleMouseDown(e));
+ }
+
+ placeAbove(el) {
+ flush();
+ const rect = this._getTargetBoundingRect(el);
+ const boxRect = this.$.tooltip.getBoundingClientRect();
+ const parentRect = this._getParentBoundingClientRect();
+ this.style.top =
+ rect.top - parentRect.top - boxRect.height - 6 + 'px';
+ this.style.left =
+ rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+ }
+
+ placeBelow(el) {
+ flush();
+ const rect = this._getTargetBoundingRect(el);
+ const boxRect = this.$.tooltip.getBoundingClientRect();
+ const parentRect = this._getParentBoundingClientRect();
+ this.style.top =
+ rect.top - parentRect.top + boxRect.height - 6 + 'px';
+ this.style.left =
+ rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+ }
+
+ _getParentBoundingClientRect() {
+ // With native shadow DOM, the parent is the shadow root, not the gr-diff
+ // element
+ const parent = this.parentElement || this.parentNode.host;
+ return parent.getBoundingClientRect();
+ }
+
+ _getTargetBoundingRect(el) {
+ let rect;
+ if (el instanceof Text) {
+ const range = document.createRange();
+ range.selectNode(el);
+ rect = range.getBoundingClientRect();
+ range.detach();
+ } else {
+ rect = el.getBoundingClientRect();
+ }
+ return rect;
+ }
+
+ _handleMouseDown(e) {
+ if (e.button !== 0) { return; } // 0 = main button
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('create-comment-requested');
+ }
+}
+
+customElements.define(GrSelectionActionBox.is, GrSelectionActionBox);
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
index aa4d2e1..670a755 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
@@ -1,28 +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="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-tooltip/gr-tooltip.html">
-
-<dom-module id="gr-selection-action-box">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
cursor: pointer;
@@ -31,10 +25,5 @@
white-space: nowrap;
}
</style>
- <gr-tooltip
- id="tooltip"
- text="Press c to comment"
- position-below="[[positionBelow]]"></gr-tooltip>
- </template>
- <script src="gr-selection-action-box.js"></script>
-</dom-module>
+ <gr-tooltip id="tooltip" text="Press c to comment" position-below="[[positionBelow]]"></gr-tooltip>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index bb802f8..c0c711a 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_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-selection-action-box</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-selection-action-box.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-selection-action-box.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-selection-action-box.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -38,98 +43,100 @@
</template>
</test-fixture>
-<script>
- suite('gr-selection-action-box', async () => {
- await readyToTest();
- let container;
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-selection-action-box.js';
+suite('gr-selection-action-box', () => {
+ let container;
+ let element;
+ let sandbox;
+
+ setup(() => {
+ container = fixture('basic');
+ element = container.querySelector('gr-selection-action-box');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(element, 'fire');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('ignores regular keys', () => {
+ MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
+ assert.isFalse(element.fire.called);
+ });
+
+ suite('mousedown reacts only to main button', () => {
+ let e;
setup(() => {
- container = fixture('basic');
- element = container.querySelector('gr-selection-action-box');
- sandbox = sinon.sandbox.create();
- sandbox.stub(element, 'fire');
+ e = {
+ button: 0,
+ preventDefault: sandbox.stub(),
+ stopPropagation: sandbox.stub(),
+ };
});
- teardown(() => {
- sandbox.restore();
+ test('event handled if main button', () => {
+ element._handleMouseDown(e);
+ assert.isTrue(e.preventDefault.called);
+ assert(element.fire.calledWithExactly('create-comment-requested'));
});
- test('ignores regular keys', () => {
- MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
+ test('event ignored if not main button', () => {
+ e.button = 1;
+ element._handleMouseDown(e);
+ assert.isFalse(e.preventDefault.called);
assert.isFalse(element.fire.called);
});
+ });
- suite('mousedown reacts only to main button', () => {
- let e;
+ suite('placeAbove', () => {
+ let target;
- setup(() => {
- e = {
- button: 0,
- preventDefault: sandbox.stub(),
- stopPropagation: sandbox.stub(),
- };
- });
-
- test('event handled if main button', () => {
- element._handleMouseDown(e);
- assert.isTrue(e.preventDefault.called);
- assert(element.fire.calledWithExactly('create-comment-requested'));
- });
-
- test('event ignored if not main button', () => {
- e.button = 1;
- element._handleMouseDown(e);
- assert.isFalse(e.preventDefault.called);
- assert.isFalse(element.fire.called);
- });
+ setup(() => {
+ target = container.querySelector('.target');
+ sandbox.stub(container, 'getBoundingClientRect').returns(
+ {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
+ sandbox.stub(element, '_getTargetBoundingRect').returns(
+ {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
+ sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns(
+ {width: 10, height: 10});
});
- suite('placeAbove', () => {
- let target;
+ test('placeAbove for Element argument', () => {
+ element.placeAbove(target);
+ assert.equal(element.style.top, '25px');
+ assert.equal(element.style.left, '72px');
+ });
- setup(() => {
- target = container.querySelector('.target');
- sandbox.stub(container, 'getBoundingClientRect').returns(
- {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
- sandbox.stub(element, '_getTargetBoundingRect').returns(
- {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
- sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns(
- {width: 10, height: 10});
- });
+ test('placeAbove for Text Node argument', () => {
+ element.placeAbove(target.firstChild);
+ assert.equal(element.style.top, '25px');
+ assert.equal(element.style.left, '72px');
+ });
- test('placeAbove for Element argument', () => {
- element.placeAbove(target);
- assert.equal(element.style.top, '25px');
- assert.equal(element.style.left, '72px');
- });
+ test('placeBelow for Element argument', () => {
+ element.placeBelow(target);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
- test('placeAbove for Text Node argument', () => {
- element.placeAbove(target.firstChild);
- assert.equal(element.style.top, '25px');
- assert.equal(element.style.left, '72px');
- });
+ test('placeBelow for Text Node argument', () => {
+ element.placeBelow(target.firstChild);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
- test('placeBelow for Element argument', () => {
- element.placeBelow(target);
- assert.equal(element.style.top, '45px');
- assert.equal(element.style.left, '72px');
- });
-
- test('placeBelow for Text Node argument', () => {
- element.placeBelow(target.firstChild);
- assert.equal(element.style.top, '45px');
- assert.equal(element.style.left, '72px');
- });
-
- test('uses document.createRange', () => {
- sandbox.spy(document, 'createRange');
- element._getTargetBoundingRect.restore();
- sandbox.spy(element, '_getTargetBoundingRect');
- element.placeAbove(target.firstChild);
- assert.isTrue(document.createRange.called);
- });
+ test('uses document.createRange', () => {
+ sandbox.spy(document, 'createRange');
+ element._getTargetBoundingRect.restore();
+ sandbox.spy(element, '_getTargetBoundingRect');
+ element.placeAbove(target.firstChild);
+ assert.isTrue(document.createRange.called);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index b6e2884..33e894b 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -14,534 +14,543 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const LANGUAGE_MAP = {
- 'application/dart': 'dart',
- 'application/json': 'json',
- 'application/x-powershell': 'powershell',
- 'application/typescript': 'typescript',
- 'application/xml': 'xml',
- 'application/xquery': 'xquery',
- 'application/x-erb': 'erb',
- 'text/css': 'css',
- 'text/html': 'html',
- 'text/javascript': 'js',
- 'text/jsx': 'jsx',
- 'text/x-c': 'cpp',
- 'text/x-c++src': 'cpp',
- 'text/x-clojure': 'clojure',
- 'text/x-cmake': 'cmake',
- 'text/x-coffeescript': 'coffeescript',
- 'text/x-common-lisp': 'lisp',
- 'text/x-crystal': 'crystal',
- 'text/x-csharp': 'csharp',
- 'text/x-csrc': 'cpp',
- 'text/x-d': 'd',
- 'text/x-diff': 'diff',
- 'text/x-django': 'django',
- 'text/x-dockerfile': 'dockerfile',
- 'text/x-ebnf': 'ebnf',
- 'text/x-elm': 'elm',
- 'text/x-erlang': 'erlang',
- 'text/x-fortran': 'fortran',
- 'text/x-fsharp': 'fsharp',
- 'text/x-go': 'go',
- 'text/x-groovy': 'groovy',
- 'text/x-haml': 'haml',
- 'text/x-handlebars': 'handlebars',
- 'text/x-haskell': 'haskell',
- 'text/x-haxe': 'haxe',
- 'text/x-ini': 'ini',
- 'text/x-java': 'java',
- 'text/x-julia': 'julia',
- 'text/x-kotlin': 'kotlin',
- 'text/x-latex': 'latex',
- 'text/x-less': 'less',
- 'text/x-lua': 'lua',
- 'text/x-mathematica': 'mathematica',
- 'text/x-nginx-conf': 'nginx',
- 'text/x-nsis': 'nsis',
- 'text/x-objectivec': 'objectivec',
- 'text/x-ocaml': 'ocaml',
- 'text/x-perl': 'perl',
- 'text/x-pgsql': 'pgsql', // postgresql
- 'text/x-php': 'php',
- 'text/x-properties': 'properties',
- 'text/x-protobuf': 'protobuf',
- 'text/x-puppet': 'puppet',
- 'text/x-python': 'python',
- 'text/x-q': 'q',
- 'text/x-ruby': 'ruby',
- 'text/x-rustsrc': 'rust',
- 'text/x-scala': 'scala',
- 'text/x-scss': 'scss',
- 'text/x-scheme': 'scheme',
- 'text/x-shell': 'shell',
- 'text/x-soy': 'soy',
- 'text/x-spreadsheet': 'excel',
- 'text/x-sh': 'bash',
- 'text/x-sql': 'sql',
- 'text/x-swift': 'swift',
- 'text/x-systemverilog': 'sv',
- 'text/x-tcl': 'tcl',
- 'text/x-torque': 'torque',
- 'text/x-twig': 'twig',
- 'text/x-vb': 'vb',
- 'text/x-verilog': 'v',
- 'text/x-vhdl': 'vhdl',
- 'text/x-yaml': 'yaml',
- 'text/vbscript': 'vbscript',
- };
- const ASYNC_DELAY = 10;
+import '../../shared/gr-lib-loader/gr-lib-loader.js';
+import '../../../scripts/util.js';
+import '../gr-diff/gr-diff-line.js';
+import '../gr-diff-highlight/gr-annotation.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-syntax-layer_html.js';
- const CLASS_WHITELIST = {
- 'gr-diff gr-syntax gr-syntax-attr': true,
- 'gr-diff gr-syntax gr-syntax-attribute': true,
- 'gr-diff gr-syntax gr-syntax-built_in': true,
- 'gr-diff gr-syntax gr-syntax-comment': true,
- 'gr-diff gr-syntax gr-syntax-doctag': true,
- 'gr-diff gr-syntax gr-syntax-function': true,
- 'gr-diff gr-syntax gr-syntax-keyword': true,
- 'gr-diff gr-syntax gr-syntax-link': true,
- 'gr-diff gr-syntax gr-syntax-literal': true,
- 'gr-diff gr-syntax gr-syntax-meta': true,
- 'gr-diff gr-syntax gr-syntax-meta-keyword': true,
- 'gr-diff gr-syntax gr-syntax-name': true,
- 'gr-diff gr-syntax gr-syntax-number': true,
- 'gr-diff gr-syntax gr-syntax-params': true,
- 'gr-diff gr-syntax gr-syntax-regexp': true,
- 'gr-diff gr-syntax gr-syntax-selector-attr': true,
- 'gr-diff gr-syntax gr-syntax-selector-class': true,
- 'gr-diff gr-syntax gr-syntax-selector-id': true,
- 'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
- 'gr-diff gr-syntax gr-syntax-selector-tag': true,
- 'gr-diff gr-syntax gr-syntax-string': true,
- 'gr-diff gr-syntax gr-syntax-tag': true,
- 'gr-diff gr-syntax gr-syntax-template-tag': true,
- 'gr-diff gr-syntax gr-syntax-template-variable': true,
- 'gr-diff gr-syntax gr-syntax-title': true,
- 'gr-diff gr-syntax gr-syntax-type': true,
- 'gr-diff gr-syntax gr-syntax-variable': true,
- };
+const LANGUAGE_MAP = {
+ 'application/dart': 'dart',
+ 'application/json': 'json',
+ 'application/x-powershell': 'powershell',
+ 'application/typescript': 'typescript',
+ 'application/xml': 'xml',
+ 'application/xquery': 'xquery',
+ 'application/x-erb': 'erb',
+ 'text/css': 'css',
+ 'text/html': 'html',
+ 'text/javascript': 'js',
+ 'text/jsx': 'jsx',
+ 'text/x-c': 'cpp',
+ 'text/x-c++src': 'cpp',
+ 'text/x-clojure': 'clojure',
+ 'text/x-cmake': 'cmake',
+ 'text/x-coffeescript': 'coffeescript',
+ 'text/x-common-lisp': 'lisp',
+ 'text/x-crystal': 'crystal',
+ 'text/x-csharp': 'csharp',
+ 'text/x-csrc': 'cpp',
+ 'text/x-d': 'd',
+ 'text/x-diff': 'diff',
+ 'text/x-django': 'django',
+ 'text/x-dockerfile': 'dockerfile',
+ 'text/x-ebnf': 'ebnf',
+ 'text/x-elm': 'elm',
+ 'text/x-erlang': 'erlang',
+ 'text/x-fortran': 'fortran',
+ 'text/x-fsharp': 'fsharp',
+ 'text/x-go': 'go',
+ 'text/x-groovy': 'groovy',
+ 'text/x-haml': 'haml',
+ 'text/x-handlebars': 'handlebars',
+ 'text/x-haskell': 'haskell',
+ 'text/x-haxe': 'haxe',
+ 'text/x-ini': 'ini',
+ 'text/x-java': 'java',
+ 'text/x-julia': 'julia',
+ 'text/x-kotlin': 'kotlin',
+ 'text/x-latex': 'latex',
+ 'text/x-less': 'less',
+ 'text/x-lua': 'lua',
+ 'text/x-mathematica': 'mathematica',
+ 'text/x-nginx-conf': 'nginx',
+ 'text/x-nsis': 'nsis',
+ 'text/x-objectivec': 'objectivec',
+ 'text/x-ocaml': 'ocaml',
+ 'text/x-perl': 'perl',
+ 'text/x-pgsql': 'pgsql', // postgresql
+ 'text/x-php': 'php',
+ 'text/x-properties': 'properties',
+ 'text/x-protobuf': 'protobuf',
+ 'text/x-puppet': 'puppet',
+ 'text/x-python': 'python',
+ 'text/x-q': 'q',
+ 'text/x-ruby': 'ruby',
+ 'text/x-rustsrc': 'rust',
+ 'text/x-scala': 'scala',
+ 'text/x-scss': 'scss',
+ 'text/x-scheme': 'scheme',
+ 'text/x-shell': 'shell',
+ 'text/x-soy': 'soy',
+ 'text/x-spreadsheet': 'excel',
+ 'text/x-sh': 'bash',
+ 'text/x-sql': 'sql',
+ 'text/x-swift': 'swift',
+ 'text/x-systemverilog': 'sv',
+ 'text/x-tcl': 'tcl',
+ 'text/x-torque': 'torque',
+ 'text/x-twig': 'twig',
+ 'text/x-vb': 'vb',
+ 'text/x-verilog': 'v',
+ 'text/x-vhdl': 'vhdl',
+ 'text/x-yaml': 'yaml',
+ 'text/vbscript': 'vbscript',
+};
+const ASYNC_DELAY = 10;
- const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
- const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
- const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
- const GO_BACKSLASH_LITERAL = '\'\\\\\'';
- const GLOBAL_LT_PATTERN = /</g;
+const CLASS_WHITELIST = {
+ 'gr-diff gr-syntax gr-syntax-attr': true,
+ 'gr-diff gr-syntax gr-syntax-attribute': true,
+ 'gr-diff gr-syntax gr-syntax-built_in': true,
+ 'gr-diff gr-syntax gr-syntax-comment': true,
+ 'gr-diff gr-syntax gr-syntax-doctag': true,
+ 'gr-diff gr-syntax gr-syntax-function': true,
+ 'gr-diff gr-syntax gr-syntax-keyword': true,
+ 'gr-diff gr-syntax gr-syntax-link': true,
+ 'gr-diff gr-syntax gr-syntax-literal': true,
+ 'gr-diff gr-syntax gr-syntax-meta': true,
+ 'gr-diff gr-syntax gr-syntax-meta-keyword': true,
+ 'gr-diff gr-syntax gr-syntax-name': true,
+ 'gr-diff gr-syntax gr-syntax-number': true,
+ 'gr-diff gr-syntax gr-syntax-params': true,
+ 'gr-diff gr-syntax gr-syntax-regexp': true,
+ 'gr-diff gr-syntax gr-syntax-selector-attr': true,
+ 'gr-diff gr-syntax gr-syntax-selector-class': true,
+ 'gr-diff gr-syntax gr-syntax-selector-id': true,
+ 'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
+ 'gr-diff gr-syntax gr-syntax-selector-tag': true,
+ 'gr-diff gr-syntax gr-syntax-string': true,
+ 'gr-diff gr-syntax gr-syntax-tag': true,
+ 'gr-diff gr-syntax gr-syntax-template-tag': true,
+ 'gr-diff gr-syntax gr-syntax-template-variable': true,
+ 'gr-diff gr-syntax gr-syntax-title': true,
+ 'gr-diff gr-syntax gr-syntax-type': true,
+ 'gr-diff gr-syntax gr-syntax-variable': true,
+};
- /** @extends Polymer.Element */
- class GrSyntaxLayer extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-syntax-layer'; }
+const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
+const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
+const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
+const GO_BACKSLASH_LITERAL = '\'\\\\\'';
+const GLOBAL_LT_PATTERN = /</g;
- static get properties() {
- return {
- diff: {
- type: Object,
- observer: '_diffChanged',
- },
- enabled: {
- type: Boolean,
- value: true,
- },
- _baseRanges: {
- type: Array,
- value() { return []; },
- },
- _revisionRanges: {
- type: Array,
- value() { return []; },
- },
- _baseLanguage: String,
- _revisionLanguage: String,
- _listeners: {
- type: Array,
- value() { return []; },
- },
- /** @type {?number} */
- _processHandle: Number,
- /**
- * The promise last returned from `process()` while the asynchronous
- * processing is running - `null` otherwise. Provides a `cancel()`
- * method that rejects it with `{isCancelled: true}`.
- *
- * @type {?Object}
- */
- _processPromise: {
- type: Object,
- value: null,
- },
- _hljs: Object,
- };
+/** @extends Polymer.Element */
+class GrSyntaxLayer extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-syntax-layer'; }
+
+ static get properties() {
+ return {
+ diff: {
+ type: Object,
+ observer: '_diffChanged',
+ },
+ enabled: {
+ type: Boolean,
+ value: true,
+ },
+ _baseRanges: {
+ type: Array,
+ value() { return []; },
+ },
+ _revisionRanges: {
+ type: Array,
+ value() { return []; },
+ },
+ _baseLanguage: String,
+ _revisionLanguage: String,
+ _listeners: {
+ type: Array,
+ value() { return []; },
+ },
+ /** @type {?number} */
+ _processHandle: Number,
+ /**
+ * The promise last returned from `process()` while the asynchronous
+ * processing is running - `null` otherwise. Provides a `cancel()`
+ * method that rejects it with `{isCancelled: true}`.
+ *
+ * @type {?Object}
+ */
+ _processPromise: {
+ type: Object,
+ value: null,
+ },
+ _hljs: Object,
+ };
+ }
+
+ addListener(fn) {
+ this.push('_listeners', fn);
+ }
+
+ removeListener(fn) {
+ this._listeners = this._listeners.filter(f => f != fn);
+ }
+
+ /**
+ * Annotation layer method to add syntax annotations to the given element
+ * for the given line.
+ *
+ * @param {!HTMLElement} el
+ * @param {!HTMLElement} lineNumberEl
+ * @param {!Object} line (GrDiffLine)
+ */
+ annotate(el, lineNumberEl, line) {
+ if (!this.enabled) { return; }
+
+ // Determine the side.
+ let side;
+ if (line.type === GrDiffLine.Type.REMOVE || (
+ line.type === GrDiffLine.Type.BOTH &&
+ el.getAttribute('data-side') !== 'right')) {
+ side = 'left';
+ } else if (line.type === GrDiffLine.Type.ADD || (
+ el.getAttribute('data-side') !== 'left')) {
+ side = 'right';
}
- addListener(fn) {
- this.push('_listeners', fn);
+ // Find the relevant syntax ranges, if any.
+ let ranges = [];
+ if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
+ ranges = this._baseRanges[line.beforeNumber - 1] || [];
+ } else if (side === 'right' &&
+ this._revisionRanges.length >= line.afterNumber) {
+ ranges = this._revisionRanges[line.afterNumber - 1] || [];
}
- removeListener(fn) {
- this._listeners = this._listeners.filter(f => f != fn);
+ // Apply the ranges to the element.
+ for (const range of ranges) {
+ GrAnnotation.annotateElement(
+ el, range.start, range.length, range.className);
+ }
+ }
+
+ _getLanguage(diffFileMetaInfo) {
+ // The Gerrit API provides only content-type, but for other users of
+ // gr-diff it may be more convenient to specify the language directly.
+ return diffFileMetaInfo.language ||
+ LANGUAGE_MAP[diffFileMetaInfo.content_type];
+ }
+
+ /**
+ * Start processing syntax for the loaded diff and notify layer listeners
+ * as syntax info comes online.
+ *
+ * @return {Promise}
+ */
+ process() {
+ // Cancel any still running process() calls, because they append to the
+ // same _baseRanges and _revisionRanges fields.
+ this._cancel();
+
+ // Discard existing ranges.
+ this._baseRanges = [];
+ this._revisionRanges = [];
+
+ if (!this.enabled || !this.diff.content.length) {
+ return Promise.resolve();
}
- /**
- * Annotation layer method to add syntax annotations to the given element
- * for the given line.
- *
- * @param {!HTMLElement} el
- * @param {!HTMLElement} lineNumberEl
- * @param {!Object} line (GrDiffLine)
- */
- annotate(el, lineNumberEl, line) {
- if (!this.enabled) { return; }
-
- // Determine the side.
- let side;
- if (line.type === GrDiffLine.Type.REMOVE || (
- line.type === GrDiffLine.Type.BOTH &&
- el.getAttribute('data-side') !== 'right')) {
- side = 'left';
- } else if (line.type === GrDiffLine.Type.ADD || (
- el.getAttribute('data-side') !== 'left')) {
- side = 'right';
- }
-
- // Find the relevant syntax ranges, if any.
- let ranges = [];
- if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
- ranges = this._baseRanges[line.beforeNumber - 1] || [];
- } else if (side === 'right' &&
- this._revisionRanges.length >= line.afterNumber) {
- ranges = this._revisionRanges[line.afterNumber - 1] || [];
- }
-
- // Apply the ranges to the element.
- for (const range of ranges) {
- GrAnnotation.annotateElement(
- el, range.start, range.length, range.className);
- }
+ if (this.diff.meta_a) {
+ this._baseLanguage = this._getLanguage(this.diff.meta_a);
+ }
+ if (this.diff.meta_b) {
+ this._revisionLanguage = this._getLanguage(this.diff.meta_b);
+ }
+ if (!this._baseLanguage && !this._revisionLanguage) {
+ return Promise.resolve();
}
- _getLanguage(diffFileMetaInfo) {
- // The Gerrit API provides only content-type, but for other users of
- // gr-diff it may be more convenient to specify the language directly.
- return diffFileMetaInfo.language ||
- LANGUAGE_MAP[diffFileMetaInfo.content_type];
+ const state = {
+ sectionIndex: 0,
+ lineIndex: 0,
+ baseContext: undefined,
+ revisionContext: undefined,
+ lineNums: {left: 1, right: 1},
+ lastNotify: {left: 1, right: 1},
+ };
+
+ const rangesCache = new Map();
+
+ this._processPromise = util.makeCancelable(this._loadHLJS()
+ .then(() => new Promise(resolve => {
+ const nextStep = () => {
+ this._processHandle = null;
+ this._processNextLine(state, rangesCache);
+
+ // Move to the next line in the section.
+ state.lineIndex++;
+
+ // If the section has been exhausted, move to the next one.
+ if (this._isSectionDone(state)) {
+ state.lineIndex = 0;
+ state.sectionIndex++;
+ }
+
+ // If all sections have been exhausted, finish.
+ if (state.sectionIndex >= this.diff.content.length) {
+ resolve();
+ this._notify(state);
+ return;
+ }
+
+ if (state.lineIndex % 100 === 0) {
+ this._notify(state);
+ this._processHandle = this.async(nextStep, ASYNC_DELAY);
+ } else {
+ nextStep.call(this);
+ }
+ };
+
+ this._processHandle = this.async(nextStep, 1);
+ })));
+ return this._processPromise
+ .finally(() => { this._processPromise = null; });
+ }
+
+ /**
+ * Cancel any asynchronous syntax processing jobs.
+ */
+ _cancel() {
+ if (this._processHandle != null) {
+ this.cancelAsync(this._processHandle);
+ this._processHandle = null;
}
-
- /**
- * Start processing syntax for the loaded diff and notify layer listeners
- * as syntax info comes online.
- *
- * @return {Promise}
- */
- process() {
- // Cancel any still running process() calls, because they append to the
- // same _baseRanges and _revisionRanges fields.
- this._cancel();
-
- // Discard existing ranges.
- this._baseRanges = [];
- this._revisionRanges = [];
-
- if (!this.enabled || !this.diff.content.length) {
- return Promise.resolve();
- }
-
- if (this.diff.meta_a) {
- this._baseLanguage = this._getLanguage(this.diff.meta_a);
- }
- if (this.diff.meta_b) {
- this._revisionLanguage = this._getLanguage(this.diff.meta_b);
- }
- if (!this._baseLanguage && !this._revisionLanguage) {
- return Promise.resolve();
- }
-
- const state = {
- sectionIndex: 0,
- lineIndex: 0,
- baseContext: undefined,
- revisionContext: undefined,
- lineNums: {left: 1, right: 1},
- lastNotify: {left: 1, right: 1},
- };
-
- const rangesCache = new Map();
-
- this._processPromise = util.makeCancelable(this._loadHLJS()
- .then(() => new Promise(resolve => {
- const nextStep = () => {
- this._processHandle = null;
- this._processNextLine(state, rangesCache);
-
- // Move to the next line in the section.
- state.lineIndex++;
-
- // If the section has been exhausted, move to the next one.
- if (this._isSectionDone(state)) {
- state.lineIndex = 0;
- state.sectionIndex++;
- }
-
- // If all sections have been exhausted, finish.
- if (state.sectionIndex >= this.diff.content.length) {
- resolve();
- this._notify(state);
- return;
- }
-
- if (state.lineIndex % 100 === 0) {
- this._notify(state);
- this._processHandle = this.async(nextStep, ASYNC_DELAY);
- } else {
- nextStep.call(this);
- }
- };
-
- this._processHandle = this.async(nextStep, 1);
- })));
- return this._processPromise
- .finally(() => { this._processPromise = null; });
+ if (this._processPromise) {
+ this._processPromise.cancel();
}
+ }
- /**
- * Cancel any asynchronous syntax processing jobs.
- */
- _cancel() {
- if (this._processHandle != null) {
- this.cancelAsync(this._processHandle);
- this._processHandle = null;
- }
- if (this._processPromise) {
- this._processPromise.cancel();
- }
- }
+ _diffChanged() {
+ this._cancel();
+ this._baseRanges = [];
+ this._revisionRanges = [];
+ }
- _diffChanged() {
- this._cancel();
- this._baseRanges = [];
- this._revisionRanges = [];
- }
+ /**
+ * Take a string of HTML with the (potentially nested) syntax markers
+ * Highlight.js emits and emit a list of text ranges and classes for the
+ * markers.
+ *
+ * @param {string} str The string of HTML.
+ * @param {Map<string, !Array<!Object>>} rangesCache A map for caching
+ * ranges for each string. A cache is read and written by this method.
+ * Since diff is mostly comparing same file on two sides, there is good rate
+ * of duplication at least for parts that are on left and right parts.
+ * @return {!Array<!Object>} The list of ranges.
+ */
+ _rangesFromString(str, rangesCache) {
+ const cached = rangesCache.get(str);
+ if (cached) return cached;
- /**
- * Take a string of HTML with the (potentially nested) syntax markers
- * Highlight.js emits and emit a list of text ranges and classes for the
- * markers.
- *
- * @param {string} str The string of HTML.
- * @param {Map<string, !Array<!Object>>} rangesCache A map for caching
- * ranges for each string. A cache is read and written by this method.
- * Since diff is mostly comparing same file on two sides, there is good rate
- * of duplication at least for parts that are on left and right parts.
- * @return {!Array<!Object>} The list of ranges.
- */
- _rangesFromString(str, rangesCache) {
- const cached = rangesCache.get(str);
- if (cached) return cached;
+ const div = document.createElement('div');
+ div.innerHTML = str;
+ const ranges = this._rangesFromElement(div, 0);
+ rangesCache.set(str, ranges);
+ return ranges;
+ }
- const div = document.createElement('div');
- div.innerHTML = str;
- const ranges = this._rangesFromElement(div, 0);
- rangesCache.set(str, ranges);
- return ranges;
- }
-
- _rangesFromElement(elem, offset) {
- let result = [];
- for (const node of elem.childNodes) {
- const nodeLength = GrAnnotation.getLength(node);
- // Note: HLJS may emit a span with class undefined when it thinks there
- // may be a syntax error.
- if (node.tagName === 'SPAN' && node.className !== 'undefined') {
- if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
- result.push({
- start: offset,
- length: nodeLength,
- className: node.className,
- });
- }
- if (node.children.length) {
- result = result.concat(this._rangesFromElement(node, offset));
- }
+ _rangesFromElement(elem, offset) {
+ let result = [];
+ for (const node of elem.childNodes) {
+ const nodeLength = GrAnnotation.getLength(node);
+ // Note: HLJS may emit a span with class undefined when it thinks there
+ // may be a syntax error.
+ if (node.tagName === 'SPAN' && node.className !== 'undefined') {
+ if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
+ result.push({
+ start: offset,
+ length: nodeLength,
+ className: node.className,
+ });
}
- offset += nodeLength;
+ if (node.children.length) {
+ result = result.concat(this._rangesFromElement(node, offset));
+ }
}
- return result;
+ offset += nodeLength;
}
+ return result;
+ }
- /**
- * For a given state, process the syntax for the next line (or pair of
- * lines).
- *
- * @param {!Object} state The processing state for the layer.
- */
- _processNextLine(state, rangesCache) {
- let baseLine;
- let revisionLine;
+ /**
+ * For a given state, process the syntax for the next line (or pair of
+ * lines).
+ *
+ * @param {!Object} state The processing state for the layer.
+ */
+ _processNextLine(state, rangesCache) {
+ let baseLine;
+ let revisionLine;
- const section = this.diff.content[state.sectionIndex];
- if (section.ab) {
- baseLine = section.ab[state.lineIndex];
- revisionLine = section.ab[state.lineIndex];
+ const section = this.diff.content[state.sectionIndex];
+ if (section.ab) {
+ baseLine = section.ab[state.lineIndex];
+ revisionLine = section.ab[state.lineIndex];
+ state.lineNums.left++;
+ state.lineNums.right++;
+ } else {
+ if (section.a && section.a.length > state.lineIndex) {
+ baseLine = section.a[state.lineIndex];
state.lineNums.left++;
+ }
+ if (section.b && section.b.length > state.lineIndex) {
+ revisionLine = section.b[state.lineIndex];
state.lineNums.right++;
- } else {
- if (section.a && section.a.length > state.lineIndex) {
- baseLine = section.a[state.lineIndex];
- state.lineNums.left++;
- }
- if (section.b && section.b.length > state.lineIndex) {
- revisionLine = section.b[state.lineIndex];
- state.lineNums.right++;
- }
- }
-
- // To store the result of the syntax highlighter.
- let result;
-
- if (this._baseLanguage && baseLine !== undefined &&
- this._hljs.getLanguage(this._baseLanguage)) {
- baseLine = this._workaround(this._baseLanguage, baseLine);
- result = this._hljs.highlight(this._baseLanguage, baseLine, true,
- state.baseContext);
- this.push('_baseRanges',
- this._rangesFromString(result.value, rangesCache));
- state.baseContext = result.top;
- }
-
- if (this._revisionLanguage && revisionLine !== undefined &&
- this._hljs.getLanguage(this._revisionLanguage)) {
- revisionLine = this._workaround(this._revisionLanguage, revisionLine);
- result = this._hljs.highlight(this._revisionLanguage, revisionLine,
- true, state.revisionContext);
- this.push('_revisionRanges',
- this._rangesFromString(result.value, rangesCache));
- state.revisionContext = result.top;
}
}
- /**
- * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
- * cases before sending them into HLJS so that they parse correctly.
- *
- * Important notes:
- * * These tests should be as constrained as possible to avoid interfering
- * with code it shouldn't AND to avoid executing regexes as much as
- * possible.
- * * These tests should document the issue clearly enough that the test can
- * be condidently removed when the issue is solved in HLJS.
- * * These tests should rewrite the line of code to have the same number of
- * characters. This method rewrites the string that gets parsed, but NOT
- * the string that gets displayed and highlighted. Thus, the positions
- * must be consistent.
- *
- * @param {!string} language The name of the HLJS language plugin in use.
- * @param {!string} line The line of code to potentially rewrite.
- * @return {string} A potentially-rewritten line of code.
- */
- _workaround(language, line) {
- if (language === 'cpp') {
- /**
- * Prevent confusing < and << operators for the start of a meta string
- * by converting them to a different operator.
- * {@see Issue 4864}
- * {@see https://github.com/isagalaev/highlight.js/issues/1341}
- */
- if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
- line = line.replace(GLOBAL_LT_PATTERN, '|');
- }
+ // To store the result of the syntax highlighter.
+ let result;
- /**
- * Rewrite CPP wchar_t characters literals to wchar_t string literals
- * because HLJS only understands the string form.
- * {@see Issue 5242}
- * {#see https://github.com/isagalaev/highlight.js/issues/1412}
- */
- if (CPP_WCHAR_PATTERN.test(line)) {
- line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
- }
+ if (this._baseLanguage && baseLine !== undefined &&
+ this._hljs.getLanguage(this._baseLanguage)) {
+ baseLine = this._workaround(this._baseLanguage, baseLine);
+ result = this._hljs.highlight(this._baseLanguage, baseLine, true,
+ state.baseContext);
+ this.push('_baseRanges',
+ this._rangesFromString(result.value, rangesCache));
+ state.baseContext = result.top;
+ }
- return line;
+ if (this._revisionLanguage && revisionLine !== undefined &&
+ this._hljs.getLanguage(this._revisionLanguage)) {
+ revisionLine = this._workaround(this._revisionLanguage, revisionLine);
+ result = this._hljs.highlight(this._revisionLanguage, revisionLine,
+ true, state.revisionContext);
+ this.push('_revisionRanges',
+ this._rangesFromString(result.value, rangesCache));
+ state.revisionContext = result.top;
+ }
+ }
+
+ /**
+ * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
+ * cases before sending them into HLJS so that they parse correctly.
+ *
+ * Important notes:
+ * * These tests should be as constrained as possible to avoid interfering
+ * with code it shouldn't AND to avoid executing regexes as much as
+ * possible.
+ * * These tests should document the issue clearly enough that the test can
+ * be condidently removed when the issue is solved in HLJS.
+ * * These tests should rewrite the line of code to have the same number of
+ * characters. This method rewrites the string that gets parsed, but NOT
+ * the string that gets displayed and highlighted. Thus, the positions
+ * must be consistent.
+ *
+ * @param {!string} language The name of the HLJS language plugin in use.
+ * @param {!string} line The line of code to potentially rewrite.
+ * @return {string} A potentially-rewritten line of code.
+ */
+ _workaround(language, line) {
+ if (language === 'cpp') {
+ /**
+ * Prevent confusing < and << operators for the start of a meta string
+ * by converting them to a different operator.
+ * {@see Issue 4864}
+ * {@see https://github.com/isagalaev/highlight.js/issues/1341}
+ */
+ if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
+ line = line.replace(GLOBAL_LT_PATTERN, '|');
}
/**
- * Prevent confusing the closing paren of a parameterized Java annotation
- * being applied to a formal argument as the closing paren of the argument
- * list. Rewrite the parens as spaces.
- * {@see Issue 4776}
- * {@see https://github.com/isagalaev/highlight.js/issues/1324}
+ * Rewrite CPP wchar_t characters literals to wchar_t string literals
+ * because HLJS only understands the string form.
+ * {@see Issue 5242}
+ * {#see https://github.com/isagalaev/highlight.js/issues/1412}
*/
- if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
- return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
- }
-
- /**
- * HLJS misunderstands backslash character literals in Go.
- * {@see Issue 5007}
- * {#see https://github.com/isagalaev/highlight.js/issues/1411}
- */
- if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) {
- return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"');
+ if (CPP_WCHAR_PATTERN.test(line)) {
+ line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
}
return line;
}
/**
- * Tells whether the state has exhausted its current section.
- *
- * @param {!Object} state
- * @return {boolean}
+ * Prevent confusing the closing paren of a parameterized Java annotation
+ * being applied to a formal argument as the closing paren of the argument
+ * list. Rewrite the parens as spaces.
+ * {@see Issue 4776}
+ * {@see https://github.com/isagalaev/highlight.js/issues/1324}
*/
- _isSectionDone(state) {
- const section = this.diff.content[state.sectionIndex];
- if (section.ab) {
- return state.lineIndex >= section.ab.length;
- } else {
- return (!section.a || state.lineIndex >= section.a.length) &&
- (!section.b || state.lineIndex >= section.b.length);
- }
+ if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
+ return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
}
/**
- * For a given state, notify layer listeners of any processed line ranges
- * that have not yet been notified.
- *
- * @param {!Object} state
+ * HLJS misunderstands backslash character literals in Go.
+ * {@see Issue 5007}
+ * {#see https://github.com/isagalaev/highlight.js/issues/1411}
*/
- _notify(state) {
- if (state.lineNums.left - state.lastNotify.left) {
- this._notifyRange(
- state.lastNotify.left,
- state.lineNums.left,
- 'left');
- state.lastNotify.left = state.lineNums.left;
- }
- if (state.lineNums.right - state.lastNotify.right) {
- this._notifyRange(
- state.lastNotify.right,
- state.lineNums.right,
- 'right');
- state.lastNotify.right = state.lineNums.right;
- }
+ if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) {
+ return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"');
}
- _notifyRange(start, end, side) {
- for (const fn of this._listeners) {
- fn(start, end, side);
- }
- }
+ return line;
+ }
- _loadHLJS() {
- return this.$.libLoader.getHLJS().then(hljs => {
- this._hljs = hljs;
- });
+ /**
+ * Tells whether the state has exhausted its current section.
+ *
+ * @param {!Object} state
+ * @return {boolean}
+ */
+ _isSectionDone(state) {
+ const section = this.diff.content[state.sectionIndex];
+ if (section.ab) {
+ return state.lineIndex >= section.ab.length;
+ } else {
+ return (!section.a || state.lineIndex >= section.a.length) &&
+ (!section.b || state.lineIndex >= section.b.length);
}
}
- customElements.define(GrSyntaxLayer.is, GrSyntaxLayer);
-})();
+ /**
+ * For a given state, notify layer listeners of any processed line ranges
+ * that have not yet been notified.
+ *
+ * @param {!Object} state
+ */
+ _notify(state) {
+ if (state.lineNums.left - state.lastNotify.left) {
+ this._notifyRange(
+ state.lastNotify.left,
+ state.lineNums.left,
+ 'left');
+ state.lastNotify.left = state.lineNums.left;
+ }
+ if (state.lineNums.right - state.lastNotify.right) {
+ this._notifyRange(
+ state.lastNotify.right,
+ state.lineNums.right,
+ 'right');
+ state.lastNotify.right = state.lineNums.right;
+ }
+ }
+
+ _notifyRange(start, end, side) {
+ for (const fn of this._listeners) {
+ fn(start, end, side);
+ }
+ }
+
+ _loadHLJS() {
+ return this.$.libLoader.getHLJS().then(hljs => {
+ this._hljs = hljs;
+ });
+ }
+}
+
+customElements.define(GrSyntaxLayer.is, GrSyntaxLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
index f9e1279..183cbd1c 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
@@ -1,28 +1,21 @@
-<!--
-@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="../../shared/gr-lib-loader/gr-lib-loader.html">
-<script src="../../../scripts/util.js"></script>
-<script src="../gr-diff/gr-diff-line.js"></script>
-<script src="../gr-diff-highlight/gr-annotation.js"></script>
-
-<dom-module id="gr-syntax-layer">
- <template>
+export const htmlTemplate = html`
<gr-lib-loader id="libLoader"></gr-lib-loader>
- </template>
- <script src="gr-syntax-layer.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 62a3f1e..aa49f71 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-syntax-layer</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="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-syntax-layer.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="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script>
+<script type="module" src="./gr-syntax-layer.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-syntax-layer.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,469 +42,472 @@
</template>
</test-fixture>
-<script>
- suite('gr-syntax-layer tests', async () => {
- await readyToTest();
- let sandbox;
- let diff;
- let element;
- const lineNumberEl = document.createElement('td');
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-rest-api-interface/mock-diff-response_test.js';
+import './gr-syntax-layer.js';
+suite('gr-syntax-layer tests', () => {
+ let sandbox;
+ let diff;
+ let element;
+ const lineNumberEl = document.createElement('td');
- function getMockHLJS() {
- const html = '<span class="gr-diff gr-syntax gr-syntax-string">' +
- 'ipsum</span>';
- return {
- configure() {},
- highlight(lang, line, ignore, state) {
- return {
- value: line.replace(/ipsum/, html),
- top: state === undefined ? 1 : state + 1,
- };
- },
- // Return something truthy because this method is used to check if the
- // language is supported.
- getLanguage(s) {
- return {};
- },
- };
- }
+ function getMockHLJS() {
+ const html = '<span class="gr-diff gr-syntax gr-syntax-string">' +
+ 'ipsum</span>';
+ return {
+ configure() {},
+ highlight(lang, line, ignore, state) {
+ return {
+ value: line.replace(/ipsum/, html),
+ top: state === undefined ? 1 : state + 1,
+ };
+ },
+ // Return something truthy because this method is used to check if the
+ // language is supported.
+ getLanguage(s) {
+ return {};
+ },
+ };
+ }
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- const mock = document.createElement('mock-diff-response');
- diff = mock.diffResponse;
- element.diff = diff;
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ const mock = document.createElement('mock-diff-response');
+ diff = mock.diffResponse;
+ element.diff = diff;
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('annotate without range does nothing', () => {
- const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const el = document.createElement('div');
- el.textContent = 'Etiam dui, blandit wisi.';
- const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
- line.beforeNumber = 12;
+ test('annotate without range does nothing', () => {
+ const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const el = document.createElement('div');
+ el.textContent = 'Etiam dui, blandit wisi.';
+ const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.beforeNumber = 12;
- element.annotate(el, lineNumberEl, line);
+ element.annotate(el, lineNumberEl, line);
- assert.isFalse(annotationSpy.called);
- });
+ assert.isFalse(annotationSpy.called);
+ });
- test('annotate with range applies it', () => {
- const str = 'Etiam dui, blandit wisi.';
- const start = 6;
- const length = 3;
- const className = 'foobar';
+ test('annotate with range applies it', () => {
+ const str = 'Etiam dui, blandit wisi.';
+ const start = 6;
+ const length = 3;
+ const className = 'foobar';
- const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const el = document.createElement('div');
- el.textContent = str;
- const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
- line.beforeNumber = 12;
- element._baseRanges[11] = [{
- start,
- length,
- className,
- }];
+ const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const el = document.createElement('div');
+ el.textContent = str;
+ const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.beforeNumber = 12;
+ element._baseRanges[11] = [{
+ start,
+ length,
+ className,
+ }];
- element.annotate(el, lineNumberEl, line);
+ element.annotate(el, lineNumberEl, line);
- assert.isTrue(annotationSpy.called);
- assert.equal(annotationSpy.lastCall.args[0], el);
- assert.equal(annotationSpy.lastCall.args[1], start);
- assert.equal(annotationSpy.lastCall.args[2], length);
- assert.equal(annotationSpy.lastCall.args[3], className);
- assert.isOk(el.querySelector('hl.' + className));
- });
+ assert.isTrue(annotationSpy.called);
+ assert.equal(annotationSpy.lastCall.args[0], el);
+ assert.equal(annotationSpy.lastCall.args[1], start);
+ assert.equal(annotationSpy.lastCall.args[2], length);
+ assert.equal(annotationSpy.lastCall.args[3], className);
+ assert.isOk(el.querySelector('hl.' + className));
+ });
- test('annotate with range but disabled does nothing', () => {
- const str = 'Etiam dui, blandit wisi.';
- const start = 6;
- const length = 3;
- const className = 'foobar';
+ test('annotate with range but disabled does nothing', () => {
+ const str = 'Etiam dui, blandit wisi.';
+ const start = 6;
+ const length = 3;
+ const className = 'foobar';
- const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const el = document.createElement('div');
- el.textContent = str;
- const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
- line.beforeNumber = 12;
- element._baseRanges[11] = [{
- start,
- length,
- className,
- }];
- element.enabled = false;
+ const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const el = document.createElement('div');
+ el.textContent = str;
+ const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.beforeNumber = 12;
+ element._baseRanges[11] = [{
+ start,
+ length,
+ className,
+ }];
+ element.enabled = false;
- element.annotate(el, lineNumberEl, line);
+ element.annotate(el, lineNumberEl, line);
- assert.isFalse(annotationSpy.called);
- });
+ assert.isFalse(annotationSpy.called);
+ });
- test('process on empty diff does nothing', done => {
- element.diff = {
- meta_a: {content_type: 'application/json'},
- meta_b: {content_type: 'application/json'},
- content: [],
- };
- const processNextSpy = sandbox.spy(element, '_processNextLine');
+ test('process on empty diff does nothing', done => {
+ element.diff = {
+ meta_a: {content_type: 'application/json'},
+ meta_b: {content_type: 'application/json'},
+ content: [],
+ };
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
- const processPromise = element.process();
+ const processPromise = element.process();
- processPromise.then(() => {
- assert.isFalse(processNextSpy.called);
- assert.equal(element._baseRanges.length, 0);
- assert.equal(element._revisionRanges.length, 0);
- done();
- });
- });
-
- test('process for unsupported languages does nothing', done => {
- element.diff = {
- meta_a: {content_type: 'text/x+objective-cobol-plus-plus'},
- meta_b: {content_type: 'application/not-a-real-language'},
- content: [],
- };
- const processNextSpy = sandbox.spy(element, '_processNextLine');
-
- const processPromise = element.process();
-
- processPromise.then(() => {
- assert.isFalse(processNextSpy.called);
- assert.equal(element._baseRanges.length, 0);
- assert.equal(element._revisionRanges.length, 0);
- done();
- });
- });
-
- test('process while disabled does nothing', done => {
- const processNextSpy = sandbox.spy(element, '_processNextLine');
- element.enabled = false;
- const loadHLJSSpy = sandbox.spy(element, '_loadHLJS');
-
- const processPromise = element.process();
-
- processPromise.then(() => {
- assert.isFalse(processNextSpy.called);
- assert.equal(element._baseRanges.length, 0);
- assert.equal(element._revisionRanges.length, 0);
- assert.isFalse(loadHLJSSpy.called);
- done();
- });
- });
-
- test('process highlight ipsum', done => {
- element.diff.meta_a.content_type = 'application/json';
- element.diff.meta_b.content_type = 'application/json';
-
- const mockHLJS = getMockHLJS();
- const highlightSpy = sinon.spy(mockHLJS, 'highlight');
- sandbox.stub(element.$.libLoader, 'getHLJS',
- () => Promise.resolve(mockHLJS));
- const processNextSpy = sandbox.spy(element, '_processNextLine');
- const processPromise = element.process();
-
- processPromise.then(() => {
- const linesA = diff.meta_a.lines;
- const linesB = diff.meta_b.lines;
-
- assert.isTrue(processNextSpy.called);
- assert.equal(element._baseRanges.length, linesA);
- assert.equal(element._revisionRanges.length, linesB);
-
- assert.equal(highlightSpy.callCount, linesA + linesB);
-
- // The first line of both sides have a range.
- let ranges = [element._baseRanges[0], element._revisionRanges[0]];
- for (const range of ranges) {
- assert.equal(range.length, 1);
- assert.equal(range[0].className,
- 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 'lorem '.length);
- assert.equal(range[0].length, 'ipsum'.length);
- }
-
- // There are no ranges from ll.1-12 on the left and ll.1-11 on the
- // right.
- ranges = element._baseRanges.slice(1, 12)
- .concat(element._revisionRanges.slice(1, 11));
-
- for (const range of ranges) {
- assert.equal(range.length, 0);
- }
-
- // There should be another pair of ranges on l.13 for the left and
- // l.12 for the right.
- ranges = [element._baseRanges[13], element._revisionRanges[12]];
-
- for (const range of ranges) {
- assert.equal(range.length, 1);
- assert.equal(range[0].className,
- 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 32);
- assert.equal(range[0].length, 'ipsum'.length);
- }
-
- // The next group should have a similar instance on either side.
-
- let range = element._baseRanges[15];
- assert.equal(range.length, 1);
- assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 34);
- assert.equal(range[0].length, 'ipsum'.length);
-
- range = element._revisionRanges[14];
- assert.equal(range.length, 1);
- assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
- assert.equal(range[0].start, 35);
- assert.equal(range[0].length, 'ipsum'.length);
-
- done();
- });
- });
-
- test('_diffChanged calls cancel', () => {
- const cancelSpy = sandbox.spy(element, '_diffChanged');
- element.diff = {content: []};
- assert.isTrue(cancelSpy.called);
- });
-
- test('_rangesFromElement no ranges', () => {
- const elem = document.createElement('span');
- elem.textContent = 'Etiam dui, blandit wisi.';
- const offset = 100;
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 0);
- });
-
- test('_rangesFromElement single range', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui, blandit';
- const str2 = ' wisi.';
- const className = 'gr-diff gr-syntax gr-syntax-string';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- const span = document.createElement('span');
- span.textContent = str1;
- span.className = className;
- elem.appendChild(span);
- elem.appendChild(document.createTextNode(str2));
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 1);
- assert.equal(result[0].start, str0.length + offset);
- assert.equal(result[0].length, str1.length);
- assert.equal(result[0].className, className);
- });
-
- test('_rangesFromElement non-whitelist', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui, blandit';
- const str2 = ' wisi.';
- const className = 'not-in-the-whitelist';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- const span = document.createElement('span');
- span.textContent = str1;
- span.className = className;
- elem.appendChild(span);
- elem.appendChild(document.createTextNode(str2));
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 0);
- });
-
- test('_rangesFromElement milti range', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui,';
- const str2 = ' blandit';
- const str3 = ' wisi.';
- const className = 'gr-diff gr-syntax gr-syntax-string';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- let span = document.createElement('span');
- span.textContent = str1;
- span.className = className;
- elem.appendChild(span);
- elem.appendChild(document.createTextNode(str2));
- span = document.createElement('span');
- span.textContent = str3;
- span.className = className;
- elem.appendChild(span);
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 2);
-
- assert.equal(result[0].start, str0.length + offset);
- assert.equal(result[0].length, str1.length);
- assert.equal(result[0].className, className);
-
- assert.equal(result[1].start,
- str0.length + str1.length + str2.length + offset);
- assert.equal(result[1].length, str3.length);
- assert.equal(result[1].className, className);
- });
-
- test('_rangesFromElement nested range', () => {
- const str0 = 'Etiam ';
- const str1 = 'dui,';
- const str2 = ' blandit';
- const str3 = ' wisi.';
- const className = 'gr-diff gr-syntax gr-syntax-string';
- const offset = 100;
-
- const elem = document.createElement('span');
- elem.appendChild(document.createTextNode(str0));
- const span1 = document.createElement('span');
- span1.textContent = str1;
- span1.className = className;
- elem.appendChild(span1);
- const span2 = document.createElement('span');
- span2.textContent = str2;
- span2.className = className;
- span1.appendChild(span2);
- elem.appendChild(document.createTextNode(str3));
-
- const result = element._rangesFromElement(elem, offset);
-
- assert.equal(result.length, 2);
-
- assert.equal(result[0].start, str0.length + offset);
- assert.equal(result[0].length, str1.length + str2.length);
- assert.equal(result[0].className, className);
-
- assert.equal(result[1].start, str0.length + str1.length + offset);
- assert.equal(result[1].length, str2.length);
- assert.equal(result[1].className, className);
- });
-
- test('_rangesFromString whitelist allows recursion', () => {
- const str = [
- '<span class="non-whtelisted-class">',
- '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
- '</span>'].join('');
- const result = element._rangesFromString(str, new Map());
- assert.notEqual(result.length, 0);
- });
-
- test('_rangesFromString cache same syntax markers', () => {
- sandbox.spy(element, '_rangesFromElement');
- const str =
- '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>';
- const cacheMap = new Map();
- element._rangesFromString(str, cacheMap);
- element._rangesFromString(str, cacheMap);
- assert.isTrue(element._rangesFromElement.calledOnce);
- });
-
- test('_isSectionDone', () => {
- let state = {sectionIndex: 0, lineIndex: 0};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 0, lineIndex: 2};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 0, lineIndex: 4};
- assert.isTrue(element._isSectionDone(state));
-
- state = {sectionIndex: 1, lineIndex: 2};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 1, lineIndex: 3};
- assert.isTrue(element._isSectionDone(state));
-
- state = {sectionIndex: 3, lineIndex: 0};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 3, lineIndex: 3};
- assert.isFalse(element._isSectionDone(state));
-
- state = {sectionIndex: 3, lineIndex: 4};
- assert.isTrue(element._isSectionDone(state));
- });
-
- test('workaround CPP LT directive', () => {
- // Does nothing to regular line.
- let line = 'int main(int argc, char** argv) { return 0; }';
- assert.equal(element._workaround('cpp', line), line);
-
- // Does nothing to include directive.
- line = '#include <stdio>';
- assert.equal(element._workaround('cpp', line), line);
-
- // Converts left-shift operator in #define.
- line = '#define GiB (1ull << 30)';
- let expected = '#define GiB (1ull || 30)';
- assert.equal(element._workaround('cpp', line), expected);
-
- // Converts less-than operator in #if.
- line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)';
- expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)';
- assert.equal(element._workaround('cpp', line), expected);
- });
-
- test('workaround Java param-annotation', () => {
- // Does nothing to regular line.
- let line = 'public static void foo(int bar) { }';
- assert.equal(element._workaround('java', line), line);
-
- // Does nothing to regular annotation.
- line = 'public static void foo(@Nullable int bar) { }';
- assert.equal(element._workaround('java', line), line);
-
- // Converts parameterized annotation.
- line = 'public static void foo(@SuppressWarnings("unused") int bar) { }';
- const expected = 'public static void foo(@SuppressWarnings "unused" ' +
- ' int bar) { }';
- assert.equal(element._workaround('java', line), expected);
- });
-
- test('workaround CPP whcar_t character literals', () => {
- // Does nothing to regular line.
- let line = 'int main(int argc, char** argv) { return 0; }';
- assert.equal(element._workaround('cpp', line), line);
-
- // Does nothing to wchar_t string.
- line = 'wchar_t* sz = L"abc 123";';
- assert.equal(element._workaround('cpp', line), line);
-
- // Converts wchar_t character literal to string.
- line = 'wchar_t myChar = L\'#\'';
- let expected = 'wchar_t myChar = L"."';
- assert.equal(element._workaround('cpp', line), expected);
-
- // Converts wchar_t character literal with escape sequence to string.
- line = 'wchar_t myChar = L\'\\"\'';
- expected = 'wchar_t myChar = L"\\."';
- assert.equal(element._workaround('cpp', line), expected);
- });
-
- test('workaround go backslash character literals', () => {
- // Does nothing to regular line.
- let line = 'func foo(in []byte) (lit []byte, n int, err error) {';
- assert.equal(element._workaround('go', line), line);
-
- // Does nothing to string with backslash literal
- line = 'c := "\\\\"';
- assert.equal(element._workaround('go', line), line);
-
- // Converts backslash literal character to a string.
- line = 'c := \'\\\\\'';
- const expected = 'c := "\\\\"';
- assert.equal(element._workaround('go', line), expected);
+ processPromise.then(() => {
+ assert.isFalse(processNextSpy.called);
+ assert.equal(element._baseRanges.length, 0);
+ assert.equal(element._revisionRanges.length, 0);
+ done();
});
});
+
+ test('process for unsupported languages does nothing', done => {
+ element.diff = {
+ meta_a: {content_type: 'text/x+objective-cobol-plus-plus'},
+ meta_b: {content_type: 'application/not-a-real-language'},
+ content: [],
+ };
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
+
+ const processPromise = element.process();
+
+ processPromise.then(() => {
+ assert.isFalse(processNextSpy.called);
+ assert.equal(element._baseRanges.length, 0);
+ assert.equal(element._revisionRanges.length, 0);
+ done();
+ });
+ });
+
+ test('process while disabled does nothing', done => {
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
+ element.enabled = false;
+ const loadHLJSSpy = sandbox.spy(element, '_loadHLJS');
+
+ const processPromise = element.process();
+
+ processPromise.then(() => {
+ assert.isFalse(processNextSpy.called);
+ assert.equal(element._baseRanges.length, 0);
+ assert.equal(element._revisionRanges.length, 0);
+ assert.isFalse(loadHLJSSpy.called);
+ done();
+ });
+ });
+
+ test('process highlight ipsum', done => {
+ element.diff.meta_a.content_type = 'application/json';
+ element.diff.meta_b.content_type = 'application/json';
+
+ const mockHLJS = getMockHLJS();
+ const highlightSpy = sinon.spy(mockHLJS, 'highlight');
+ sandbox.stub(element.$.libLoader, 'getHLJS',
+ () => Promise.resolve(mockHLJS));
+ const processNextSpy = sandbox.spy(element, '_processNextLine');
+ const processPromise = element.process();
+
+ processPromise.then(() => {
+ const linesA = diff.meta_a.lines;
+ const linesB = diff.meta_b.lines;
+
+ assert.isTrue(processNextSpy.called);
+ assert.equal(element._baseRanges.length, linesA);
+ assert.equal(element._revisionRanges.length, linesB);
+
+ assert.equal(highlightSpy.callCount, linesA + linesB);
+
+ // The first line of both sides have a range.
+ let ranges = [element._baseRanges[0], element._revisionRanges[0]];
+ for (const range of ranges) {
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className,
+ 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 'lorem '.length);
+ assert.equal(range[0].length, 'ipsum'.length);
+ }
+
+ // There are no ranges from ll.1-12 on the left and ll.1-11 on the
+ // right.
+ ranges = element._baseRanges.slice(1, 12)
+ .concat(element._revisionRanges.slice(1, 11));
+
+ for (const range of ranges) {
+ assert.equal(range.length, 0);
+ }
+
+ // There should be another pair of ranges on l.13 for the left and
+ // l.12 for the right.
+ ranges = [element._baseRanges[13], element._revisionRanges[12]];
+
+ for (const range of ranges) {
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className,
+ 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 32);
+ assert.equal(range[0].length, 'ipsum'.length);
+ }
+
+ // The next group should have a similar instance on either side.
+
+ let range = element._baseRanges[15];
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 34);
+ assert.equal(range[0].length, 'ipsum'.length);
+
+ range = element._revisionRanges[14];
+ assert.equal(range.length, 1);
+ assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+ assert.equal(range[0].start, 35);
+ assert.equal(range[0].length, 'ipsum'.length);
+
+ done();
+ });
+ });
+
+ test('_diffChanged calls cancel', () => {
+ const cancelSpy = sandbox.spy(element, '_diffChanged');
+ element.diff = {content: []};
+ assert.isTrue(cancelSpy.called);
+ });
+
+ test('_rangesFromElement no ranges', () => {
+ const elem = document.createElement('span');
+ elem.textContent = 'Etiam dui, blandit wisi.';
+ const offset = 100;
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 0);
+ });
+
+ test('_rangesFromElement single range', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui, blandit';
+ const str2 = ' wisi.';
+ const className = 'gr-diff gr-syntax gr-syntax-string';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ const span = document.createElement('span');
+ span.textContent = str1;
+ span.className = className;
+ elem.appendChild(span);
+ elem.appendChild(document.createTextNode(str2));
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 1);
+ assert.equal(result[0].start, str0.length + offset);
+ assert.equal(result[0].length, str1.length);
+ assert.equal(result[0].className, className);
+ });
+
+ test('_rangesFromElement non-whitelist', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui, blandit';
+ const str2 = ' wisi.';
+ const className = 'not-in-the-whitelist';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ const span = document.createElement('span');
+ span.textContent = str1;
+ span.className = className;
+ elem.appendChild(span);
+ elem.appendChild(document.createTextNode(str2));
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 0);
+ });
+
+ test('_rangesFromElement milti range', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui,';
+ const str2 = ' blandit';
+ const str3 = ' wisi.';
+ const className = 'gr-diff gr-syntax gr-syntax-string';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ let span = document.createElement('span');
+ span.textContent = str1;
+ span.className = className;
+ elem.appendChild(span);
+ elem.appendChild(document.createTextNode(str2));
+ span = document.createElement('span');
+ span.textContent = str3;
+ span.className = className;
+ elem.appendChild(span);
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 2);
+
+ assert.equal(result[0].start, str0.length + offset);
+ assert.equal(result[0].length, str1.length);
+ assert.equal(result[0].className, className);
+
+ assert.equal(result[1].start,
+ str0.length + str1.length + str2.length + offset);
+ assert.equal(result[1].length, str3.length);
+ assert.equal(result[1].className, className);
+ });
+
+ test('_rangesFromElement nested range', () => {
+ const str0 = 'Etiam ';
+ const str1 = 'dui,';
+ const str2 = ' blandit';
+ const str3 = ' wisi.';
+ const className = 'gr-diff gr-syntax gr-syntax-string';
+ const offset = 100;
+
+ const elem = document.createElement('span');
+ elem.appendChild(document.createTextNode(str0));
+ const span1 = document.createElement('span');
+ span1.textContent = str1;
+ span1.className = className;
+ elem.appendChild(span1);
+ const span2 = document.createElement('span');
+ span2.textContent = str2;
+ span2.className = className;
+ span1.appendChild(span2);
+ elem.appendChild(document.createTextNode(str3));
+
+ const result = element._rangesFromElement(elem, offset);
+
+ assert.equal(result.length, 2);
+
+ assert.equal(result[0].start, str0.length + offset);
+ assert.equal(result[0].length, str1.length + str2.length);
+ assert.equal(result[0].className, className);
+
+ assert.equal(result[1].start, str0.length + str1.length + offset);
+ assert.equal(result[1].length, str2.length);
+ assert.equal(result[1].className, className);
+ });
+
+ test('_rangesFromString whitelist allows recursion', () => {
+ const str = [
+ '<span class="non-whtelisted-class">',
+ '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
+ '</span>'].join('');
+ const result = element._rangesFromString(str, new Map());
+ assert.notEqual(result.length, 0);
+ });
+
+ test('_rangesFromString cache same syntax markers', () => {
+ sandbox.spy(element, '_rangesFromElement');
+ const str =
+ '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>';
+ const cacheMap = new Map();
+ element._rangesFromString(str, cacheMap);
+ element._rangesFromString(str, cacheMap);
+ assert.isTrue(element._rangesFromElement.calledOnce);
+ });
+
+ test('_isSectionDone', () => {
+ let state = {sectionIndex: 0, lineIndex: 0};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 0, lineIndex: 2};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 0, lineIndex: 4};
+ assert.isTrue(element._isSectionDone(state));
+
+ state = {sectionIndex: 1, lineIndex: 2};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 1, lineIndex: 3};
+ assert.isTrue(element._isSectionDone(state));
+
+ state = {sectionIndex: 3, lineIndex: 0};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 3, lineIndex: 3};
+ assert.isFalse(element._isSectionDone(state));
+
+ state = {sectionIndex: 3, lineIndex: 4};
+ assert.isTrue(element._isSectionDone(state));
+ });
+
+ test('workaround CPP LT directive', () => {
+ // Does nothing to regular line.
+ let line = 'int main(int argc, char** argv) { return 0; }';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Does nothing to include directive.
+ line = '#include <stdio>';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Converts left-shift operator in #define.
+ line = '#define GiB (1ull << 30)';
+ let expected = '#define GiB (1ull || 30)';
+ assert.equal(element._workaround('cpp', line), expected);
+
+ // Converts less-than operator in #if.
+ line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)';
+ expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)';
+ assert.equal(element._workaround('cpp', line), expected);
+ });
+
+ test('workaround Java param-annotation', () => {
+ // Does nothing to regular line.
+ let line = 'public static void foo(int bar) { }';
+ assert.equal(element._workaround('java', line), line);
+
+ // Does nothing to regular annotation.
+ line = 'public static void foo(@Nullable int bar) { }';
+ assert.equal(element._workaround('java', line), line);
+
+ // Converts parameterized annotation.
+ line = 'public static void foo(@SuppressWarnings("unused") int bar) { }';
+ const expected = 'public static void foo(@SuppressWarnings "unused" ' +
+ ' int bar) { }';
+ assert.equal(element._workaround('java', line), expected);
+ });
+
+ test('workaround CPP whcar_t character literals', () => {
+ // Does nothing to regular line.
+ let line = 'int main(int argc, char** argv) { return 0; }';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Does nothing to wchar_t string.
+ line = 'wchar_t* sz = L"abc 123";';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Converts wchar_t character literal to string.
+ line = 'wchar_t myChar = L\'#\'';
+ let expected = 'wchar_t myChar = L"."';
+ assert.equal(element._workaround('cpp', line), expected);
+
+ // Converts wchar_t character literal with escape sequence to string.
+ line = 'wchar_t myChar = L\'\\"\'';
+ expected = 'wchar_t myChar = L"\\."';
+ assert.equal(element._workaround('cpp', line), expected);
+ });
+
+ test('workaround go backslash character literals', () => {
+ // Does nothing to regular line.
+ let line = 'func foo(in []byte) (lit []byte, n int, err error) {';
+ assert.equal(element._workaround('go', line), line);
+
+ // Does nothing to string with backslash literal
+ line = 'c := "\\\\"';
+ assert.equal(element._workaround('go', line), line);
+
+ // Converts backslash literal character to a string.
+ line = 'c := \'\\\\\'';
+ const expected = 'c := "\\\\"';
+ assert.equal(element._workaround('go', line), expected);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
index e5ae06d..76a01de 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-syntax-theme">
+$_documentContainer.innerHTML = `<dom-module id="gr-syntax-theme">
<template>
<style>
/**
@@ -107,4 +109,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index 022a985..0a57883 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -14,78 +14,90 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.ListViewMixin
- * @extends Polymer.Element
- */
- class GrDocumentationSearch extends Polymer.mixinBehaviors( [
- Gerrit.ListViewBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-documentation-search'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import '../../../styles/gr-table-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-list-view/gr-list-view.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-documentation-search_html.js';
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
+/**
+ * @appliesMixin Gerrit.ListViewMixin
+ * @extends Polymer.Element
+ */
+class GrDocumentationSearch extends mixinBehaviors( [
+ Gerrit.ListViewBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _path: {
- type: String,
- readOnly: true,
- value: '/Documentation',
- },
- _documentationSearches: Array,
+ static get is() { return 'gr-documentation-search'; }
- _loading: {
- type: Boolean,
- value: true,
- },
- _filter: {
- type: String,
- value: '',
- },
- };
- }
+ static get properties() {
+ return {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
- /** @override */
- attached() {
- super.attached();
- this.dispatchEvent(
- new CustomEvent('title-change', {title: 'Documentation Search'}));
- }
+ _path: {
+ type: String,
+ readOnly: true,
+ value: '/Documentation',
+ },
+ _documentationSearches: Array,
- _paramsChanged(params) {
- this._loading = true;
- this._filter = this.getFilterValue(params);
-
- return this._getDocumentationSearches(this._filter);
- }
-
- _getDocumentationSearches(filter) {
- this._documentationSearches = [];
- return this.$.restAPI.getDocumentationSearches(filter)
- .then(searches => {
- // Late response.
- if (filter !== this._filter || !searches) { return; }
- this._documentationSearches = searches;
- this._loading = false;
- });
- }
-
- _computeSearchUrl(url) {
- if (!url) { return ''; }
- return this.getBaseUrl() + '/' + url;
- }
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _filter: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrDocumentationSearch.is, GrDocumentationSearch);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.dispatchEvent(
+ new CustomEvent('title-change', {title: 'Documentation Search'}));
+ }
+
+ _paramsChanged(params) {
+ this._loading = true;
+ this._filter = this.getFilterValue(params);
+
+ return this._getDocumentationSearches(this._filter);
+ }
+
+ _getDocumentationSearches(filter) {
+ this._documentationSearches = [];
+ return this.$.restAPI.getDocumentationSearches(filter)
+ .then(searches => {
+ // Late response.
+ if (filter !== this._filter || !searches) { return; }
+ this._documentationSearches = searches;
+ this._loading = false;
+ });
+ }
+
+ _computeSearchUrl(url) {
+ if (!url) { return ''; }
+ return this.getBaseUrl() + '/' + url;
+ }
+}
+
+customElements.define(GrDocumentationSearch.is, GrDocumentationSearch);
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
index 5ae679e..ced351a 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
@@ -1,56 +1,43 @@
-<!--
-@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="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
-<link rel="import" href="../../../styles/gr-table-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-documentation-search">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <gr-list-view
- filter="[[_filter]]"
- items=false
- offset=0
- loading="[[_loading]]"
- path="[[_path]]">
+ <gr-list-view filter="[[_filter]]" items="false" offset="0" loading="[[_loading]]" path="[[_path]]">
<table id="list" class="genericList">
- <tr class="headerRow">
+ <tbody><tr class="headerRow">
<th class="name topHeader">Name</th>
<th class="name topHeader"></th>
<th class="name topHeader"></th>
</tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
- <tbody class$="[[computeLoadingClass(_loading)]]">
+ </tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_documentationSearches]]">
<tr class="table">
<td class="name">
- <a href$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a>
+ <a href\$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a>
</td>
<td></td>
<td></td>
@@ -60,6 +47,4 @@
</table>
</gr-list-view>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-documentation-search.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
index e9bf78d..9c3a08d 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-documentation-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/page/page.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-documentation-search.html">
+<script src="/node_modules/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 type="module" src="./gr-documentation-search.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-documentation-search.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,90 +41,92 @@
</template>
</test-fixture>
-<script>
- let counter;
- const documentationGenerator = () => {
- return {
- title: `Gerrit Code Review - REST API Developers Notes${++counter}`,
- url: 'Documentation/dev-rest-api.html',
- };
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-documentation-search.js';
+let counter;
+const documentationGenerator = () => {
+ return {
+ title: `Gerrit Code Review - REST API Developers Notes${++counter}`,
+ url: 'Documentation/dev-rest-api.html',
};
+};
- suite('gr-documentation-search tests', async () => {
- await readyToTest();
- let element;
- let documentationSearches;
- let sandbox;
- let value;
+suite('gr-documentation-search tests', () => {
+ let element;
+ let documentationSearches;
+ let sandbox;
+ let value;
- setup(() => {
- sandbox = sinon.sandbox.create();
- sandbox.stub(page, 'show');
- element = fixture('basic');
- counter = 0;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(page, 'show');
+ element = fixture('basic');
+ counter = 0;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('list with searches for documentation', () => {
+ setup(done => {
+ documentationSearches = _.times(26, documentationGenerator);
+ stub('gr-rest-api-interface', {
+ getDocumentationSearches() {
+ return Promise.resolve(documentationSearches);
+ },
+ });
+ element._paramsChanged(value).then(() => { flush(done); });
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('list with searches for documentation', () => {
- setup(done => {
- documentationSearches = _.times(26, documentationGenerator);
- stub('gr-rest-api-interface', {
- getDocumentationSearches() {
- return Promise.resolve(documentationSearches);
- },
- });
- element._paramsChanged(value).then(() => { flush(done); });
- });
-
- test('test for test repo in the list', done => {
- flush(() => {
- assert.equal(element._documentationSearches[0].title,
- 'Gerrit Code Review - REST API Developers Notes1');
- assert.equal(element._documentationSearches[0].url,
- 'Documentation/dev-rest-api.html');
- done();
- });
- });
- });
-
- suite('filter', () => {
- setup(() => {
- documentationSearches = _.times(25, documentationGenerator);
- _.times(1, documentationSearches);
- });
-
- test('_paramsChanged', done => {
- sandbox.stub(
- element.$.restAPI,
- 'getDocumentationSearches',
- () => Promise.resolve(documentationSearches));
- const value = {
- filter: 'test',
- };
- element._paramsChanged(value).then(() => {
- assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall
- .calledWithExactly('test'));
- done();
- });
- });
- });
-
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._repos = _.times(25, documentationGenerator);
-
- flushAsynchronousOperations();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ test('test for test repo in the list', done => {
+ flush(() => {
+ assert.equal(element._documentationSearches[0].title,
+ 'Gerrit Code Review - REST API Developers Notes1');
+ assert.equal(element._documentationSearches[0].url,
+ 'Documentation/dev-rest-api.html');
+ done();
});
});
});
+
+ suite('filter', () => {
+ setup(() => {
+ documentationSearches = _.times(25, documentationGenerator);
+ _.times(1, documentationSearches);
+ });
+
+ test('_paramsChanged', done => {
+ sandbox.stub(
+ element.$.restAPI,
+ 'getDocumentationSearches',
+ () => Promise.resolve(documentationSearches));
+ const value = {
+ filter: 'test',
+ };
+ element._paramsChanged(value).then(() => {
+ assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall
+ .calledWithExactly('test'));
+ done();
+ });
+ });
+ });
+
+ suite('loading', () => {
+ test('correct contents are displayed', () => {
+ assert.isTrue(element._loading);
+ assert.equal(element.computeLoadingClass(element._loading), 'loading');
+ assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+ element._loading = false;
+ element._repos = _.times(25, documentationGenerator);
+
+ flushAsynchronousOperations();
+ assert.equal(element.computeLoadingClass(element._loading), '');
+ assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
index 73dbaf8..09f4abf 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -14,32 +14,38 @@
* 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 GrDefaultEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-default-editor'; }
- /**
- * Fired when the content of the editor changes.
- *
- * @event content-change
- */
+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-default-editor_html.js';
- static get properties() {
- return {
- fileContent: String,
- };
- }
+/** @extends Polymer.Element */
+class GrDefaultEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleTextareaInput(e) {
- this.dispatchEvent(new CustomEvent(
- 'content-change',
- {detail: {value: e.target.value}, bubbles: true, composed: true}));
- }
+ static get is() { return 'gr-default-editor'; }
+ /**
+ * Fired when the content of the editor changes.
+ *
+ * @event content-change
+ */
+
+ static get properties() {
+ return {
+ fileContent: String,
+ };
}
- customElements.define(GrDefaultEditor.is, GrDefaultEditor);
-})();
+ _handleTextareaInput(e) {
+ this.dispatchEvent(new CustomEvent(
+ 'content-change',
+ {detail: {value: e.target.value}, bubbles: true, composed: true}));
+ }
+}
+
+customElements.define(GrDefaultEditor.is, GrDefaultEditor);
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
index 19a4e63..e7fc6fd 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
@@ -1,25 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-default-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
textarea {
border: none;
@@ -36,10 +33,5 @@
outline: none;
}
</style>
- <textarea
- id="textarea"
- value="[[fileContent]]"
- on-input="_handleTextareaInput"></textarea>
- </template>
- <script src="gr-default-editor.js"></script>
-</dom-module>
+ <textarea id="textarea" value="[[fileContent]]" on-input="_handleTextareaInput"></textarea>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
index 228c70e..043f656 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -18,16 +18,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-default-editor</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>
-<link rel="import" href="gr-default-editor.html">
+<script type="module" src="./gr-default-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-default-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,26 +40,28 @@
</template>
</test-fixture>
-<script>
- suite('gr-default-editor tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-default-editor.js';
+suite('gr-default-editor tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- element.fileContent = '';
- });
-
- test('fires content-change event', done => {
- const contentChangedHandler = e => {
- assert.equal(e.detail.value, 'test');
- done();
- };
- const textarea = element.$.textarea;
- element.addEventListener('content-change', contentChangedHandler);
- textarea.value = 'test';
- textarea.dispatchEvent(new CustomEvent('input',
- {target: textarea, bubbles: true, composed: true}));
- });
+ setup(() => {
+ element = fixture('basic');
+ element.fileContent = '';
});
+
+ test('fires content-change event', done => {
+ const contentChangedHandler = e => {
+ assert.equal(e.detail.value, 'test');
+ done();
+ };
+ const textarea = element.$.textarea;
+ element.addEventListener('content-change', contentChangedHandler);
+ textarea.value = 'test';
+ textarea.dispatchEvent(new CustomEvent('input',
+ {target: textarea, bubbles: true, composed: true}));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.js b/polygerrit-ui/app/elements/edit/gr-edit-constants.js
index 5895124..2a929f2 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-constants.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-constants.js
@@ -1,33 +1,31 @@
-<!--
-@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
+ const GrEditConstants = window.GrEditConstants || {};
-http://www.apache.org/licenses/LICENSE-2.0
+ // Order corresponds to order in the UI.
+ GrEditConstants.Actions = {
+ OPEN: {label: 'Add/Open', id: 'open'},
+ DELETE: {label: 'Delete', id: 'delete'},
+ RENAME: {label: 'Rename', id: 'rename'},
+ RESTORE: {label: 'Restore', id: 'restore'},
+ };
-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';
-
- const GrEditConstants = window.GrEditConstants || {};
-
- // Order corresponds to order in the UI.
- GrEditConstants.Actions = {
- OPEN: {label: 'Add/Open', id: 'open'},
- DELETE: {label: 'Delete', id: 'delete'},
- RENAME: {label: 'Rename', id: 'rename'},
- RESTORE: {label: 'Restore', id: 'restore'},
- };
-
- window.GrEditConstants = GrEditConstants;
- })(window);
-</script>
+ window.GrEditConstants = GrEditConstants;
+})(window);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index e655f7b..e17fe03 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -14,231 +14,250 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrEditControls extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-edit-controls'; }
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dialog/gr-dialog.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-edit-constants.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-edit-controls_html.js';
- static get properties() {
- return {
- change: Object,
- patchNum: String,
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrEditControls extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * TODO(kaspern): by default, the RESTORE action should be hidden in the
- * file-list as it is a per-file action only. Remove this default value
- * when the Actions dictionary is moved to a shared constants file and
- * use the hiddenActions property in the parent component.
- */
- hiddenActions: {
- type: Array,
- value() { return [GrEditConstants.Actions.RESTORE.id]; },
+ static get is() { return 'gr-edit-controls'; }
+
+ static get properties() {
+ return {
+ change: Object,
+ patchNum: String,
+
+ /**
+ * TODO(kaspern): by default, the RESTORE action should be hidden in the
+ * file-list as it is a per-file action only. Remove this default value
+ * when the Actions dictionary is moved to a shared constants file and
+ * use the hiddenActions property in the parent component.
+ */
+ hiddenActions: {
+ type: Array,
+ value() { return [GrEditConstants.Actions.RESTORE.id]; },
+ },
+
+ _actions: {
+ type: Array,
+ value() { return Object.values(GrEditConstants.Actions); },
+ },
+ _path: {
+ type: String,
+ value: '',
+ },
+ _newPath: {
+ type: String,
+ value: '',
+ },
+ _query: {
+ type: Function,
+ value() {
+ return this._queryFiles.bind(this);
},
+ },
+ };
+ }
- _actions: {
- type: Array,
- value() { return Object.values(GrEditConstants.Actions); },
- },
- _path: {
- type: String,
- value: '',
- },
- _newPath: {
- type: String,
- value: '',
- },
- _query: {
- type: Function,
- value() {
- return this._queryFiles.bind(this);
- },
- },
- };
- }
-
- _handleTap(e) {
- e.preventDefault();
- const action = Polymer.dom(e).localTarget.id;
- switch (action) {
- case GrEditConstants.Actions.OPEN.id:
- this.openOpenDialog();
- return;
- case GrEditConstants.Actions.DELETE.id:
- this.openDeleteDialog();
- return;
- case GrEditConstants.Actions.RENAME.id:
- this.openRenameDialog();
- return;
- case GrEditConstants.Actions.RESTORE.id:
- this.openRestoreDialog();
- return;
- }
- }
-
- /**
- * @param {string=} opt_path
- */
- openOpenDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.openDialog);
- }
-
- /**
- * @param {string=} opt_path
- */
- openDeleteDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.deleteDialog);
- }
-
- /**
- * @param {string=} opt_path
- */
- openRenameDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.renameDialog);
- }
-
- /**
- * @param {string=} opt_path
- */
- openRestoreDialog(opt_path) {
- if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.restoreDialog);
- }
-
- /**
- * Given a path string, checks that it is a valid file path.
- *
- * @param {string} path
- * @return {boolean}
- */
- _isValidPath(path) {
- // Double negation needed for strict boolean return type.
- return !!path.length && !path.endsWith('/');
- }
-
- _computeRenameDisabled(path, newPath) {
- return this._isValidPath(path) && this._isValidPath(newPath);
- }
-
- /**
- * Given a dom event, gets the dialog that lies along this event path.
- *
- * @param {!Event} e
- * @return {!Element|undefined}
- */
- _getDialogFromEvent(e) {
- return Polymer.dom(e).path.find(element => {
- if (!element.classList) { return false; }
- return element.classList.contains('dialog');
- });
- }
-
- _showDialog(dialog) {
- // Some dialogs may not fire their on-close event when closed in certain
- // ways (e.g. by clicking outside the dialog body). This call prevents
- // multiple dialogs from being shown in the same overlay.
- this._hideAllDialogs();
-
- return this.$.overlay.open().then(() => {
- dialog.classList.toggle('invisible', false);
- const autocomplete = dialog.querySelector('gr-autocomplete');
- if (autocomplete) { autocomplete.focus(); }
- this.async(() => { this.$.overlay.center(); }, 1);
- });
- }
-
- _hideAllDialogs() {
- const dialogs = Polymer.dom(this.root).querySelectorAll('.dialog');
- for (const dialog of dialogs) { this._closeDialog(dialog); }
- }
-
- /**
- * @param {Element|undefined} dialog
- * @param {boolean=} clearInputs
- */
- _closeDialog(dialog, clearInputs) {
- if (!dialog) { return; }
-
- if (clearInputs) {
- // Dialog may have autocompletes and plain inputs -- as these have
- // different properties representing their bound text, it is easier to
- // just make two separate queries.
- dialog.querySelectorAll('gr-autocomplete')
- .forEach(input => { input.text = ''; });
-
- dialog.querySelectorAll('iron-input')
- .forEach(input => { input.bindValue = ''; });
- }
-
- dialog.classList.toggle('invisible', true);
- return this.$.overlay.close();
- }
-
- _handleDialogCancel(e) {
- this._closeDialog(this._getDialogFromEvent(e));
- }
-
- _handleOpenConfirm(e) {
- const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path,
- this.patchNum);
- Gerrit.Nav.navigateToRelativeUrl(url);
- this._closeDialog(this._getDialogFromEvent(e), true);
- }
-
- _handleDeleteConfirm(e) {
- // Get the dialog before the api call as the event will change during bubbling
- // which will make Polymer.dom(e).path an emtpy array in polymer 2
- const dialog = this._getDialogFromEvent(e);
- this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
- .then(res => {
- if (!res.ok) { return; }
- this._closeDialog(dialog, true);
- Gerrit.Nav.navigateToChange(this.change);
- });
- }
-
- _handleRestoreConfirm(e) {
- const dialog = this._getDialogFromEvent(e);
- this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path)
- .then(res => {
- if (!res.ok) { return; }
- this._closeDialog(dialog, true);
- Gerrit.Nav.navigateToChange(this.change);
- });
- }
-
- _handleRenameConfirm(e) {
- const dialog = this._getDialogFromEvent(e);
- return this.$.restAPI.renameFileInChangeEdit(this.change._number,
- this._path, this._newPath).then(res => {
- if (!res.ok) { return; }
- this._closeDialog(dialog, true);
- Gerrit.Nav.navigateToChange(this.change);
- });
- }
-
- _queryFiles(input) {
- return this.$.restAPI.queryChangeFiles(this.change._number,
- this.patchNum, input).then(res => res.map(file => {
- return {name: file};
- }));
- }
-
- _computeIsInvisible(id, hiddenActions) {
- return hiddenActions.includes(id) ? 'invisible' : '';
+ _handleTap(e) {
+ e.preventDefault();
+ const action = dom(e).localTarget.id;
+ switch (action) {
+ case GrEditConstants.Actions.OPEN.id:
+ this.openOpenDialog();
+ return;
+ case GrEditConstants.Actions.DELETE.id:
+ this.openDeleteDialog();
+ return;
+ case GrEditConstants.Actions.RENAME.id:
+ this.openRenameDialog();
+ return;
+ case GrEditConstants.Actions.RESTORE.id:
+ this.openRestoreDialog();
+ return;
}
}
- customElements.define(GrEditControls.is, GrEditControls);
-})();
+ /**
+ * @param {string=} opt_path
+ */
+ openOpenDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.openDialog);
+ }
+
+ /**
+ * @param {string=} opt_path
+ */
+ openDeleteDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.deleteDialog);
+ }
+
+ /**
+ * @param {string=} opt_path
+ */
+ openRenameDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.renameDialog);
+ }
+
+ /**
+ * @param {string=} opt_path
+ */
+ openRestoreDialog(opt_path) {
+ if (opt_path) { this._path = opt_path; }
+ return this._showDialog(this.$.restoreDialog);
+ }
+
+ /**
+ * Given a path string, checks that it is a valid file path.
+ *
+ * @param {string} path
+ * @return {boolean}
+ */
+ _isValidPath(path) {
+ // Double negation needed for strict boolean return type.
+ return !!path.length && !path.endsWith('/');
+ }
+
+ _computeRenameDisabled(path, newPath) {
+ return this._isValidPath(path) && this._isValidPath(newPath);
+ }
+
+ /**
+ * Given a dom event, gets the dialog that lies along this event path.
+ *
+ * @param {!Event} e
+ * @return {!Element|undefined}
+ */
+ _getDialogFromEvent(e) {
+ return dom(e).path.find(element => {
+ if (!element.classList) { return false; }
+ return element.classList.contains('dialog');
+ });
+ }
+
+ _showDialog(dialog) {
+ // Some dialogs may not fire their on-close event when closed in certain
+ // ways (e.g. by clicking outside the dialog body). This call prevents
+ // multiple dialogs from being shown in the same overlay.
+ this._hideAllDialogs();
+
+ return this.$.overlay.open().then(() => {
+ dialog.classList.toggle('invisible', false);
+ const autocomplete = dialog.querySelector('gr-autocomplete');
+ if (autocomplete) { autocomplete.focus(); }
+ this.async(() => { this.$.overlay.center(); }, 1);
+ });
+ }
+
+ _hideAllDialogs() {
+ const dialogs = dom(this.root).querySelectorAll('.dialog');
+ for (const dialog of dialogs) { this._closeDialog(dialog); }
+ }
+
+ /**
+ * @param {Element|undefined} dialog
+ * @param {boolean=} clearInputs
+ */
+ _closeDialog(dialog, clearInputs) {
+ if (!dialog) { return; }
+
+ if (clearInputs) {
+ // Dialog may have autocompletes and plain inputs -- as these have
+ // different properties representing their bound text, it is easier to
+ // just make two separate queries.
+ dialog.querySelectorAll('gr-autocomplete')
+ .forEach(input => { input.text = ''; });
+
+ dialog.querySelectorAll('iron-input')
+ .forEach(input => { input.bindValue = ''; });
+ }
+
+ dialog.classList.toggle('invisible', true);
+ return this.$.overlay.close();
+ }
+
+ _handleDialogCancel(e) {
+ this._closeDialog(this._getDialogFromEvent(e));
+ }
+
+ _handleOpenConfirm(e) {
+ const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path,
+ this.patchNum);
+ Gerrit.Nav.navigateToRelativeUrl(url);
+ this._closeDialog(this._getDialogFromEvent(e), true);
+ }
+
+ _handleDeleteConfirm(e) {
+ // Get the dialog before the api call as the event will change during bubbling
+ // which will make Polymer.dom(e).path an emtpy array in polymer 2
+ const dialog = this._getDialogFromEvent(e);
+ this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
+ .then(res => {
+ if (!res.ok) { return; }
+ this._closeDialog(dialog, true);
+ Gerrit.Nav.navigateToChange(this.change);
+ });
+ }
+
+ _handleRestoreConfirm(e) {
+ const dialog = this._getDialogFromEvent(e);
+ this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path)
+ .then(res => {
+ if (!res.ok) { return; }
+ this._closeDialog(dialog, true);
+ Gerrit.Nav.navigateToChange(this.change);
+ });
+ }
+
+ _handleRenameConfirm(e) {
+ const dialog = this._getDialogFromEvent(e);
+ return this.$.restAPI.renameFileInChangeEdit(this.change._number,
+ this._path, this._newPath).then(res => {
+ if (!res.ok) { return; }
+ this._closeDialog(dialog, true);
+ Gerrit.Nav.navigateToChange(this.change);
+ });
+ }
+
+ _queryFiles(input) {
+ return this.$.restAPI.queryChangeFiles(this.change._number,
+ this.patchNum, input).then(res => res.map(file => {
+ return {name: file};
+ }));
+ }
+
+ _computeIsInvisible(id, hiddenActions) {
+ return hiddenActions.includes(id) ? 'invisible' : '';
+ }
+}
+
+customElements.define(GrEditControls.is, GrEditControls);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
index cb950da..2d09069 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
@@ -1,38 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-edit-constants.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-edit-controls">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
align-items: center;
@@ -70,94 +54,40 @@
}
</style>
<template is="dom-repeat" items="[[_actions]]" as="action">
- <gr-button
- id$="[[action.id]]"
- class$="[[_computeIsInvisible(action.id, hiddenActions)]]"
- link
- on-click="_handleTap">[[action.label]]</gr-button>
+ <gr-button id\$="[[action.id]]" class\$="[[_computeIsInvisible(action.id, hiddenActions)]]" link="" on-click="_handleTap">[[action.label]]</gr-button>
</template>
- <gr-overlay id="overlay" with-backdrop>
- <gr-dialog
- id="openDialog"
- class="invisible dialog"
- disabled$="[[!_isValidPath(_path)]]"
- confirm-label="Confirm"
- confirm-on-enter
- on-confirm="_handleOpenConfirm"
- on-cancel="_handleDialogCancel">
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-dialog id="openDialog" class="invisible dialog" disabled\$="[[!_isValidPath(_path)]]" confirm-label="Confirm" confirm-on-enter="" on-confirm="_handleOpenConfirm" on-cancel="_handleDialogCancel">
<div class="header" slot="header">
Add a new file or open an existing file
</div>
<div class="main" slot="main">
- <gr-autocomplete
- placeholder="Enter an existing or new full file path."
- query="[[_query]]"
- text="{{_path}}"></gr-autocomplete>
+ <gr-autocomplete placeholder="Enter an existing or new full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete>
</div>
</gr-dialog>
- <gr-dialog
- id="deleteDialog"
- class="invisible dialog"
- disabled$="[[!_isValidPath(_path)]]"
- confirm-label="Delete"
- confirm-on-enter
- on-confirm="_handleDeleteConfirm"
- on-cancel="_handleDialogCancel">
+ <gr-dialog id="deleteDialog" class="invisible dialog" disabled\$="[[!_isValidPath(_path)]]" confirm-label="Delete" confirm-on-enter="" on-confirm="_handleDeleteConfirm" on-cancel="_handleDialogCancel">
<div class="header" slot="header">Delete a file from the repo</div>
<div class="main" slot="main">
- <gr-autocomplete
- placeholder="Enter an existing full file path."
- query="[[_query]]"
- text="{{_path}}"></gr-autocomplete>
+ <gr-autocomplete placeholder="Enter an existing full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete>
</div>
</gr-dialog>
- <gr-dialog
- id="renameDialog"
- class="invisible dialog"
- disabled$="[[!_computeRenameDisabled(_path, _newPath)]]"
- confirm-label="Rename"
- confirm-on-enter
- on-confirm="_handleRenameConfirm"
- on-cancel="_handleDialogCancel">
+ <gr-dialog id="renameDialog" class="invisible dialog" disabled\$="[[!_computeRenameDisabled(_path, _newPath)]]" confirm-label="Rename" confirm-on-enter="" on-confirm="_handleRenameConfirm" on-cancel="_handleDialogCancel">
<div class="header" slot="header">Rename a file in the repo</div>
<div class="main" slot="main">
- <gr-autocomplete
- placeholder="Enter an existing full file path."
- query="[[_query]]"
- text="{{_path}}"></gr-autocomplete>
- <iron-input
- class="newPathIronInput"
- bind-value="{{_newPath}}"
- placeholder="Enter the new path.">
- <input
- class="newPathInput"
- is="iron-input"
- bind-value="{{_newPath}}"
- placeholder="Enter the new path.">
+ <gr-autocomplete placeholder="Enter an existing full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete>
+ <iron-input class="newPathIronInput" bind-value="{{_newPath}}" placeholder="Enter the new path.">
+ <input class="newPathInput" is="iron-input" bind-value="{{_newPath}}" placeholder="Enter the new path.">
</iron-input>
</div>
</gr-dialog>
- <gr-dialog
- id="restoreDialog"
- class="invisible dialog"
- confirm-label="Restore"
- confirm-on-enter
- on-confirm="_handleRestoreConfirm"
- on-cancel="_handleDialogCancel">
+ <gr-dialog id="restoreDialog" class="invisible dialog" confirm-label="Restore" confirm-on-enter="" on-confirm="_handleRestoreConfirm" on-cancel="_handleDialogCancel">
<div class="header" slot="header">Restore this file?</div>
<div class="main" slot="main">
- <iron-input
- disabled
- bind-value="{{_path}}">
- <input
- is="iron-input"
- disabled
- bind-value="{{_path}}">
+ <iron-input disabled="" bind-value="{{_path}}">
+ <input is="iron-input" disabled="" bind-value="{{_path}}">
</iron-input>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-edit-controls.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index 80de093..034a7a7 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -18,16 +18,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-edit-controls</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>
-<link rel="import" href="gr-edit-controls.html">
+<script type="module" src="./gr-edit-controls.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-edit-controls.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,351 +40,355 @@
</template>
</test-fixture>
-<script>
- suite('gr-edit-controls tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let showDialogSpy;
- let closeDialogSpy;
- let queryStub;
-
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-edit-controls.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+suite('gr-edit-controls tests', () => {
+ let element;
+ let sandbox;
+ let showDialogSpy;
+ let closeDialogSpy;
+ let queryStub;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.change = {_number: '42'};
+ showDialogSpy = sandbox.spy(element, '_showDialog');
+ closeDialogSpy = sandbox.spy(element, '_closeDialog');
+ sandbox.stub(element, '_hideAllDialogs');
+ queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
+ .returns(Promise.resolve([]));
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('all actions exist', () => {
+ assert.equal(dom(element.root).querySelectorAll('gr-button').length,
+ element._actions.length);
+ });
+
+ suite('edit button CUJ', () => {
+ let navStubs;
+ let openAutoCcmplete;
+
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.change = {_number: '42'};
- showDialogSpy = sandbox.spy(element, '_showDialog');
- closeDialogSpy = sandbox.spy(element, '_closeDialog');
- sandbox.stub(element, '_hideAllDialogs');
- queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
- .returns(Promise.resolve([]));
- flushAsynchronousOperations();
+ navStubs = [
+ sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'),
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'),
+ ];
+ openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
});
-
- teardown(() => { sandbox.restore(); });
-
- test('all actions exist', () => {
- assert.equal(Polymer.dom(element.root).querySelectorAll('gr-button').length,
- element._actions.length);
+
+ test('_isValidPath', () => {
+ assert.isFalse(element._isValidPath(''));
+ assert.isFalse(element._isValidPath('test/'));
+ assert.isFalse(element._isValidPath('/'));
+ assert.isTrue(element._isValidPath('test/path.cpp'));
+ assert.isTrue(element._isValidPath('test.js'));
});
-
- suite('edit button CUJ', () => {
- let navStubs;
- let openAutoCcmplete;
-
- setup(() => {
- navStubs = [
- sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'),
- sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'),
- ];
- openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete');
- });
-
- test('_isValidPath', () => {
- assert.isFalse(element._isValidPath(''));
- assert.isFalse(element._isValidPath('test/'));
- assert.isFalse(element._isValidPath('/'));
- assert.isTrue(element._isValidPath('test/path.cpp'));
- assert.isTrue(element._isValidPath('test.js'));
- });
-
- test('open', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#open'));
- element.patchNum = 1;
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element._hideAllDialogs.called);
- assert.isTrue(element.$.openDialog.disabled);
- assert.isFalse(queryStub.called);
- openAutoCcmplete.noDebounce = true;
- openAutoCcmplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.openDialog.disabled);
- MockInteractions.tap(element.$.openDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- for (const stub of navStubs) { assert.isTrue(stub.called); }
- assert.deepEqual(Gerrit.Nav.getEditUrlForDiff.lastCall.args,
- [element.change, 'src/test.cpp', element.patchNum]);
- assert.isTrue(closeDialogSpy.called);
- });
- });
-
- test('cancel', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#open'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.openDialog.disabled);
- openAutoCcmplete.noDebounce = true;
- openAutoCcmplete.text = 'src/test.cpp';
- assert.isFalse(element.$.openDialog.disabled);
- MockInteractions.tap(element.$.openDialog.shadowRoot
- .querySelector('gr-button'));
- for (const stub of navStubs) { assert.isFalse(stub.called); }
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- });
+
+ test('open', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#open'));
+ element.patchNum = 1;
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element._hideAllDialogs.called);
+ assert.isTrue(element.$.openDialog.disabled);
+ assert.isFalse(queryStub.called);
+ openAutoCcmplete.noDebounce = true;
+ openAutoCcmplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.openDialog.disabled);
+ MockInteractions.tap(element.$.openDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ for (const stub of navStubs) { assert.isTrue(stub.called); }
+ assert.deepEqual(Gerrit.Nav.getEditUrlForDiff.lastCall.args,
+ [element.change, 'src/test.cpp', element.patchNum]);
+ assert.isTrue(closeDialogSpy.called);
});
});
-
- suite('delete button CUJ', () => {
- let navStub;
- let deleteStub;
- let deleteAutocomplete;
-
- setup(() => {
- navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
- deleteAutocomplete =
- element.$.deleteDialog.querySelector('gr-autocomplete');
+
+ test('cancel', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#open'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.openDialog.disabled);
+ openAutoCcmplete.noDebounce = true;
+ openAutoCcmplete.text = 'src/test.cpp';
+ assert.isFalse(element.$.openDialog.disabled);
+ MockInteractions.tap(element.$.openDialog.shadowRoot
+ .querySelector('gr-button'));
+ for (const stub of navStubs) { assert.isFalse(stub.called); }
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
});
-
- test('delete', () => {
- deleteStub.returns(Promise.resolve({ok: true}));
- MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- assert.isFalse(queryStub.called);
- deleteAutocomplete.noDebounce = true;
- deleteAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(element.$.deleteDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(deleteStub.called);
-
- return deleteStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
- });
-
- test('delete fails', () => {
- deleteStub.returns(Promise.resolve({ok: false}));
- MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- assert.isFalse(queryStub.called);
- deleteAutocomplete.noDebounce = true;
- deleteAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(element.$.deleteDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(deleteStub.called);
-
- return deleteStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
- });
-
- test('cancel', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.deleteDialog.disabled);
- element.$.deleteDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
- assert.isFalse(element.$.deleteDialog.disabled);
- MockInteractions.tap(element.$.deleteDialog.shadowRoot
- .querySelector('gr-button'));
- assert.isFalse(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- });
- });
- });
-
- suite('rename button CUJ', () => {
- let navStub;
- let renameStub;
- let renameAutocomplete;
- const inputSelector = Polymer.Element ?
- '.newPathIronInput' :
- '.newPathInput';
-
- setup(() => {
- navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
- renameAutocomplete =
- element.$.renameDialog.querySelector('gr-autocomplete');
- });
-
- test('rename', () => {
- renameStub.returns(Promise.resolve({ok: true}));
- MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- assert.isFalse(queryStub.called);
- renameAutocomplete.noDebounce = true;
- renameAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isTrue(element.$.renameDialog.disabled);
-
- element.$.renameDialog.querySelector(inputSelector).bindValue =
- 'src/test.newPath';
-
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(element.$.renameDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(renameStub.called);
-
- return renameStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
- });
-
- test('rename fails', () => {
- renameStub.returns(Promise.resolve({ok: false}));
- MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- assert.isFalse(queryStub.called);
- renameAutocomplete.noDebounce = true;
- renameAutocomplete.text = 'src/test.cpp';
- assert.isTrue(queryStub.called);
- assert.isTrue(element.$.renameDialog.disabled);
-
- element.$.renameDialog.querySelector(inputSelector).bindValue =
- 'src/test.newPath';
-
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(element.$.renameDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(renameStub.called);
-
- return renameStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
- });
-
- test('cancel', () => {
- MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.renameDialog.disabled);
- element.$.renameDialog.querySelector('gr-autocomplete').text =
- 'src/test.cpp';
- element.$.renameDialog.querySelector(inputSelector).bindValue =
- 'src/test.newPath';
- assert.isFalse(element.$.renameDialog.disabled);
- MockInteractions.tap(element.$.renameDialog.shadowRoot
- .querySelector('gr-button'));
- assert.isFalse(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- assert.equal(element._newPath, 'src/test.newPath');
- });
- });
- });
-
- suite('restore button CUJ', () => {
- let navStub;
- let restoreStub;
-
- setup(() => {
- navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit');
- });
-
- test('restore hidden by default', () => {
- assert.isTrue(element.shadowRoot
- .querySelector('#restore').classList.contains('invisible'));
- });
-
- test('restore', () => {
- restoreStub.returns(Promise.resolve({ok: true}));
- element._path = 'src/test.cpp';
- MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- MockInteractions.tap(element.$.restoreDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(restoreStub.called);
- assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
- return restoreStub.lastCall.returnValue.then(() => {
- assert.equal(element._path, '');
- assert.isTrue(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- });
- });
- });
-
- test('restore fails', () => {
- restoreStub.returns(Promise.resolve({ok: false}));
- element._path = 'src/test.cpp';
- MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- MockInteractions.tap(element.$.restoreDialog.shadowRoot
- .querySelector('gr-button[primary]'));
- flushAsynchronousOperations();
-
- assert.isTrue(restoreStub.called);
- assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
- return restoreStub.lastCall.returnValue.then(() => {
- assert.isFalse(navStub.called);
- assert.isFalse(closeDialogSpy.called);
- });
- });
- });
-
- test('cancel', () => {
- element._path = 'src/test.cpp';
- MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
- return showDialogSpy.lastCall.returnValue.then(() => {
- MockInteractions.tap(element.$.restoreDialog.shadowRoot
- .querySelector('gr-button'));
- assert.isFalse(navStub.called);
- assert.isTrue(closeDialogSpy.called);
- assert.equal(element._path, 'src/test.cpp');
- });
- });
- });
-
- test('openOpenDialog', done => {
- element.openOpenDialog('test/path.cpp')
- .then(() => {
- assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
- assert.equal(
- element.$.openDialog.querySelector('gr-autocomplete').text,
- 'test/path.cpp');
- done();
- });
- });
-
- test('_getDialogFromEvent', () => {
- const spy = sandbox.spy(element, '_getDialogFromEvent');
- element.addEventListener('tap', element._getDialogFromEvent);
-
- MockInteractions.tap(element.$.openDialog);
- flushAsynchronousOperations();
- assert.equal(spy.lastCall.returnValue.id, 'openDialog');
-
- MockInteractions.tap(element.$.deleteDialog);
- flushAsynchronousOperations();
- assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
-
- MockInteractions.tap(
- element.$.deleteDialog.querySelector('gr-autocomplete'));
- flushAsynchronousOperations();
- assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
-
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.notOk(spy.lastCall.returnValue);
});
});
+
+ suite('delete button CUJ', () => {
+ let navStub;
+ let deleteStub;
+ let deleteAutocomplete;
+
+ setup(() => {
+ navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit');
+ deleteAutocomplete =
+ element.$.deleteDialog.querySelector('gr-autocomplete');
+ });
+
+ test('delete', () => {
+ deleteStub.returns(Promise.resolve({ok: true}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.deleteDialog.disabled);
+ assert.isFalse(queryStub.called);
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(element.$.deleteDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(deleteStub.called);
+
+ return deleteStub.lastCall.returnValue.then(() => {
+ assert.equal(element._path, '');
+ assert.isTrue(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('delete fails', () => {
+ deleteStub.returns(Promise.resolve({ok: false}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.deleteDialog.disabled);
+ assert.isFalse(queryStub.called);
+ deleteAutocomplete.noDebounce = true;
+ deleteAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(element.$.deleteDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(deleteStub.called);
+
+ return deleteStub.lastCall.returnValue.then(() => {
+ assert.isFalse(navStub.called);
+ assert.isFalse(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('cancel', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#delete'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.deleteDialog.disabled);
+ element.$.deleteDialog.querySelector('gr-autocomplete').text =
+ 'src/test.cpp';
+ assert.isFalse(element.$.deleteDialog.disabled);
+ MockInteractions.tap(element.$.deleteDialog.shadowRoot
+ .querySelector('gr-button'));
+ assert.isFalse(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
+ });
+ });
+ });
+
+ suite('rename button CUJ', () => {
+ let navStub;
+ let renameStub;
+ let renameAutocomplete;
+ const inputSelector = PolymerElement ?
+ '.newPathIronInput' :
+ '.newPathInput';
+
+ setup(() => {
+ navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+ renameAutocomplete =
+ element.$.renameDialog.querySelector('gr-autocomplete');
+ });
+
+ test('rename', () => {
+ renameStub.returns(Promise.resolve({ok: true}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.renameDialog.disabled);
+ assert.isFalse(queryStub.called);
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isTrue(element.$.renameDialog.disabled);
+
+ element.$.renameDialog.querySelector(inputSelector).bindValue =
+ 'src/test.newPath';
+
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(element.$.renameDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(renameStub.called);
+
+ return renameStub.lastCall.returnValue.then(() => {
+ assert.equal(element._path, '');
+ assert.isTrue(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('rename fails', () => {
+ renameStub.returns(Promise.resolve({ok: false}));
+ MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.renameDialog.disabled);
+ assert.isFalse(queryStub.called);
+ renameAutocomplete.noDebounce = true;
+ renameAutocomplete.text = 'src/test.cpp';
+ assert.isTrue(queryStub.called);
+ assert.isTrue(element.$.renameDialog.disabled);
+
+ element.$.renameDialog.querySelector(inputSelector).bindValue =
+ 'src/test.newPath';
+
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(element.$.renameDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(renameStub.called);
+
+ return renameStub.lastCall.returnValue.then(() => {
+ assert.isFalse(navStub.called);
+ assert.isFalse(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('cancel', () => {
+ MockInteractions.tap(element.shadowRoot.querySelector('#rename'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(element.$.renameDialog.disabled);
+ element.$.renameDialog.querySelector('gr-autocomplete').text =
+ 'src/test.cpp';
+ element.$.renameDialog.querySelector(inputSelector).bindValue =
+ 'src/test.newPath';
+ assert.isFalse(element.$.renameDialog.disabled);
+ MockInteractions.tap(element.$.renameDialog.shadowRoot
+ .querySelector('gr-button'));
+ assert.isFalse(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
+ assert.equal(element._newPath, 'src/test.newPath');
+ });
+ });
+ });
+
+ suite('restore button CUJ', () => {
+ let navStub;
+ let restoreStub;
+
+ setup(() => {
+ navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit');
+ });
+
+ test('restore hidden by default', () => {
+ assert.isTrue(element.shadowRoot
+ .querySelector('#restore').classList.contains('invisible'));
+ });
+
+ test('restore', () => {
+ restoreStub.returns(Promise.resolve({ok: true}));
+ element._path = 'src/test.cpp';
+ MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ MockInteractions.tap(element.$.restoreDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(restoreStub.called);
+ assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
+ return restoreStub.lastCall.returnValue.then(() => {
+ assert.equal(element._path, '');
+ assert.isTrue(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('restore fails', () => {
+ restoreStub.returns(Promise.resolve({ok: false}));
+ element._path = 'src/test.cpp';
+ MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ MockInteractions.tap(element.$.restoreDialog.shadowRoot
+ .querySelector('gr-button[primary]'));
+ flushAsynchronousOperations();
+
+ assert.isTrue(restoreStub.called);
+ assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
+ return restoreStub.lastCall.returnValue.then(() => {
+ assert.isFalse(navStub.called);
+ assert.isFalse(closeDialogSpy.called);
+ });
+ });
+ });
+
+ test('cancel', () => {
+ element._path = 'src/test.cpp';
+ MockInteractions.tap(element.shadowRoot.querySelector('#restore'));
+ return showDialogSpy.lastCall.returnValue.then(() => {
+ MockInteractions.tap(element.$.restoreDialog.shadowRoot
+ .querySelector('gr-button'));
+ assert.isFalse(navStub.called);
+ assert.isTrue(closeDialogSpy.called);
+ assert.equal(element._path, 'src/test.cpp');
+ });
+ });
+ });
+
+ test('openOpenDialog', done => {
+ element.openOpenDialog('test/path.cpp')
+ .then(() => {
+ assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
+ assert.equal(
+ element.$.openDialog.querySelector('gr-autocomplete').text,
+ 'test/path.cpp');
+ done();
+ });
+ });
+
+ test('_getDialogFromEvent', () => {
+ const spy = sandbox.spy(element, '_getDialogFromEvent');
+ element.addEventListener('tap', element._getDialogFromEvent);
+
+ MockInteractions.tap(element.$.openDialog);
+ flushAsynchronousOperations();
+ assert.equal(spy.lastCall.returnValue.id, 'openDialog');
+
+ MockInteractions.tap(element.$.deleteDialog);
+ flushAsynchronousOperations();
+ assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
+
+ MockInteractions.tap(
+ element.$.deleteDialog.querySelector('gr-autocomplete'));
+ flushAsynchronousOperations();
+ assert.equal(spy.lastCall.returnValue.id, 'deleteDialog');
+
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.notOk(spy.lastCall.returnValue);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index d59fcf7..10bff3c 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -14,56 +14,65 @@
* 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 GrEditFileControls extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-edit-file-controls'; }
- /**
- * Fired when an action in the overflow menu is tapped.
- *
- * @event file-action-tap
- */
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-dropdown/gr-dropdown.js';
+import '../gr-edit-constants.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-edit-file-controls_html.js';
- static get properties() {
- return {
- filePath: String,
- _allFileActions: {
- type: Array,
- value: () => Object.values(GrEditConstants.Actions),
- },
- _fileActions: {
- type: Array,
- computed: '_computeFileActions(_allFileActions)',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrEditFileControls extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleActionTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this._dispatchFileAction(e.detail.id, this.filePath);
- }
+ static get is() { return 'gr-edit-file-controls'; }
+ /**
+ * Fired when an action in the overflow menu is tapped.
+ *
+ * @event file-action-tap
+ */
- _dispatchFileAction(action, path) {
- this.dispatchEvent(new CustomEvent(
- 'file-action-tap',
- {detail: {action, path}, bubbles: true, composed: true}));
- }
-
- _computeFileActions(actions) {
- // TODO(kaspern): conditionally disable some actions based on file status.
- return actions.map(action => {
- return {
- name: action.label,
- id: action.id,
- };
- });
- }
+ static get properties() {
+ return {
+ filePath: String,
+ _allFileActions: {
+ type: Array,
+ value: () => Object.values(GrEditConstants.Actions),
+ },
+ _fileActions: {
+ type: Array,
+ computed: '_computeFileActions(_allFileActions)',
+ },
+ };
}
- customElements.define(GrEditFileControls.is, GrEditFileControls);
-})();
+ _handleActionTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this._dispatchFileAction(e.detail.id, this.filePath);
+ }
+
+ _dispatchFileAction(action, path) {
+ this.dispatchEvent(new CustomEvent(
+ 'file-action-tap',
+ {detail: {action, path}, bubbles: true, composed: true}));
+ }
+
+ _computeFileActions(actions) {
+ // TODO(kaspern): conditionally disable some actions based on file status.
+ return actions.map(action => {
+ return {
+ name: action.label,
+ id: action.id,
+ };
+ });
+ }
+}
+
+customElements.define(GrEditFileControls.is, GrEditFileControls);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
index f6c7803..7a7ba5d 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
@@ -1,30 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
-<link rel="import" href="../gr-edit-constants.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-edit-file-controls">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
align-items: center;
@@ -49,13 +41,5 @@
}
}
</style>
- <gr-dropdown
- id="actions"
- items="[[_fileActions]]"
- down-arrow
- vertical-offset="20"
- on-tap-item="_handleActionTap"
- link>Actions</gr-dropdown>
- </template>
- <script src="gr-edit-file-controls.js"></script>
-</dom-module>
+ <gr-dropdown id="actions" items="[[_fileActions]]" down-arrow="" vertical-offset="20" on-tap-item="_handleActionTap" link="">Actions</gr-dropdown>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
index 392a105..d694226 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -18,17 +18,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-edit-file-controls</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>
-<link rel="import" href="../gr-edit-constants.html">
-<link rel="import" href="gr-edit-file-controls.html">
+<script type="module" src="../gr-edit-constants.js"></script>
+<script type="module" src="./gr-edit-file-controls.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-edit-constants.js';
+import './gr-edit-file-controls.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,76 +42,79 @@
</template>
</test-fixture>
-<script>
- suite('gr-edit-file-controls tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let fileActionHandler;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- fileActionHandler = sandbox.stub();
- element.addEventListener('file-action-tap', fileActionHandler);
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('open tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="open"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.OPEN.id, path: 'foo'});
- });
-
- test('delete tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="delete"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.DELETE.id, path: 'foo'});
- });
-
- test('restore tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="restore"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.RESTORE.id, path: 'foo'});
- });
-
- test('rename tap emits event', () => {
- const actions = element.$.actions;
- element.filePath = 'foo';
- actions._open();
- flushAsynchronousOperations();
-
- MockInteractions.tap(actions.shadowRoot
- .querySelector('li [data-id="rename"]'));
- assert.isTrue(fileActionHandler.called);
- assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.RENAME.id, path: 'foo'});
- });
-
- test('computed properties', () => {
- assert.equal(element._allFileActions.length, 4);
- });
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-edit-constants.js';
+import './gr-edit-file-controls.js';
+suite('gr-edit-file-controls tests', () => {
+ let element;
+ let sandbox;
+ let fileActionHandler;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ fileActionHandler = sandbox.stub();
+ element.addEventListener('file-action-tap', fileActionHandler);
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('open tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="open"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.OPEN.id, path: 'foo'});
+ });
+
+ test('delete tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="delete"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.DELETE.id, path: 'foo'});
+ });
+
+ test('restore tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="restore"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.RESTORE.id, path: 'foo'});
+ });
+
+ test('rename tap emits event', () => {
+ const actions = element.$.actions;
+ element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(actions.shadowRoot
+ .querySelector('li [data-id="rename"]'));
+ assert.isTrue(fileActionHandler.called);
+ assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
+ {action: GrEditConstants.Actions.RENAME.id, path: 'foo'});
+ });
+
+ test('computed properties', () => {
+ assert.equal(element._allFileActions.length, 4);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index 64158d0..2303005 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -14,257 +14,277 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const RESTORED_MESSAGE = 'Content restored from a previous edit.';
- const SAVING_MESSAGE = 'Saving changes...';
- const SAVED_MESSAGE = 'All changes saved';
- const SAVE_FAILED_MSG = 'Failed to save changes';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-editable-label/gr-editable-label.js';
+import '../../shared/gr-fixed-panel/gr-fixed-panel.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-storage/gr-storage.js';
+import '../gr-default-editor/gr-default-editor.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-editor-view_html.js';
- const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
+const RESTORED_MESSAGE = 'Content restored from a previous edit.';
+const SAVING_MESSAGE = 'Saving changes...';
+const SAVED_MESSAGE = 'All changes saved';
+const SAVE_FAILED_MSG = 'Failed to save changes';
+
+const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @extends Polymer.Element
+ */
+class GrEditorView extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.PathListBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-editor-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.PathListMixin
- * @extends Polymer.Element
+ * Fired to notify the user of
+ *
+ * @event show-alert
*/
- class GrEditorView extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.PathListBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-editor-view'; }
+
+ static get properties() {
+ return {
/**
- * Fired when the title of the page should change.
- *
- * @event title-change
+ * URL params passed from the router.
*/
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
- /**
- * Fired to notify the user of
- *
- * @event show-alert
- */
-
- static get properties() {
- return {
- /**
- * URL params passed from the router.
- */
- params: {
- type: Object,
- observer: '_paramsChanged',
- },
-
- _change: Object,
- _changeEditDetail: Object,
- _changeNum: String,
- _patchNum: String,
- _path: String,
- _type: String,
- _content: String,
- _newContent: String,
- _saving: {
- type: Boolean,
- value: false,
- },
- _successfulSave: {
- type: Boolean,
- value: false,
- },
- _saveDisabled: {
- type: Boolean,
- value: true,
- computed: '_computeSaveDisabled(_content, _newContent, _saving)',
- },
- _prefs: Object,
- _lineNum: Number,
- };
- }
-
- get keyBindings() {
- return {
- 'ctrl+s meta+s': '_handleSaveShortcut',
- };
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('content-change',
- e => this._handleContentChange(e));
- }
-
- /** @override */
- attached() {
- super.attached();
- this._getEditPrefs().then(prefs => { this._prefs = prefs; });
- }
-
- get storageKey() {
- return `c${this._changeNum}_ps${this._patchNum}_${this._path}`;
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getEditPrefs() {
- return this.$.restAPI.getEditPreferences();
- }
-
- _paramsChanged(value) {
- if (value.view !== Gerrit.Nav.View.EDIT) {
- return;
- }
-
- this._changeNum = value.changeNum;
- this._path = value.path;
- this._patchNum = value.patchNum || this.EDIT_NAME;
- this._lineNum = value.lineNum;
-
- // NOTE: This may be called before attachment (e.g. while parentElement is
- // null). Fire title-change in an async so that, if attachment to the DOM
- // has been queued, the event can bubble up to the handler in gr-app.
- this.async(() => {
- const title = `Editing ${this.computeTruncatedPath(this._path)}`;
- this.fire('title-change', {title});
- });
-
- const promises = [];
-
- promises.push(this._getChangeDetail(this._changeNum));
- promises.push(
- this._getFileData(this._changeNum, this._path, this._patchNum));
- return Promise.all(promises);
- }
-
- _getChangeDetail(changeNum) {
- return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
- this._change = change;
- });
- }
-
- _handlePathChanged(e) {
- const path = e.detail;
- if (path === this._path) {
- return Promise.resolve();
- }
- return this.$.restAPI.renameFileInChangeEdit(this._changeNum,
- this._path, path).then(res => {
- if (!res.ok) { return; }
-
- this._successfulSave = true;
- this._viewEditInChangeView();
- });
- }
-
- _viewEditInChangeView() {
- const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
- Gerrit.Nav.navigateToChange(this._change, patch, null,
- patch !== this.EDIT_NAME);
- }
-
- _getFileData(changeNum, path, patchNum) {
- const storedContent =
- this.$.storage.getEditableContentItem(this.storageKey);
-
- return this.$.restAPI.getFileContent(changeNum, path, patchNum)
- .then(res => {
- if (storedContent && storedContent.message &&
- storedContent.message !== res.content) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: RESTORED_MESSAGE},
- bubbles: true,
- composed: true,
- }));
-
- this._newContent = storedContent.message;
- } else {
- this._newContent = res.content || '';
- }
- this._content = res.content || '';
-
- // A non-ok response may result if the file does not yet exist.
- // The `type` field of the response is only valid when the file
- // already exists.
- if (res.ok && res.type) {
- this._type = res.type;
- } else {
- this._type = '';
- }
- });
- }
-
- _saveEdit() {
- this._saving = true;
- this._showAlert(SAVING_MESSAGE);
- this.$.storage.eraseEditableContentItem(this.storageKey);
- return this.$.restAPI.saveChangeEdit(this._changeNum, this._path,
- this._newContent).then(res => {
- this._saving = false;
- this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG);
- if (!res.ok) { return; }
-
- this._content = this._newContent;
- this._successfulSave = true;
- });
- }
-
- _showAlert(message) {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message},
- bubbles: true,
- composed: true,
- }));
- }
-
- _computeSaveDisabled(content, newContent, saving) {
- // Polymer 2: check for undefined
- if ([
- content,
- newContent,
- saving,
- ].some(arg => arg === undefined)) {
- return true;
- }
-
- if (saving) {
- return true;
- }
- return content === newContent;
- }
-
- _handleCloseTap() {
- // TODO(kaspern): Add a confirm dialog if there are unsaved changes.
- this._viewEditInChangeView();
- }
-
- _handleContentChange(e) {
- this.debounce('store', () => {
- const content = e.detail.value;
- if (content) {
- this.set('_newContent', e.detail.value);
- this.$.storage.setEditableContentItem(this.storageKey, content);
- } else {
- this.$.storage.eraseEditableContentItem(this.storageKey);
- }
- }, STORAGE_DEBOUNCE_INTERVAL_MS);
- }
-
- _handleSaveShortcut(e) {
- e.preventDefault();
- if (!this._saveDisabled) {
- this._saveEdit();
- }
- }
+ _change: Object,
+ _changeEditDetail: Object,
+ _changeNum: String,
+ _patchNum: String,
+ _path: String,
+ _type: String,
+ _content: String,
+ _newContent: String,
+ _saving: {
+ type: Boolean,
+ value: false,
+ },
+ _successfulSave: {
+ type: Boolean,
+ value: false,
+ },
+ _saveDisabled: {
+ type: Boolean,
+ value: true,
+ computed: '_computeSaveDisabled(_content, _newContent, _saving)',
+ },
+ _prefs: Object,
+ _lineNum: Number,
+ };
}
- customElements.define(GrEditorView.is, GrEditorView);
-})();
+ get keyBindings() {
+ return {
+ 'ctrl+s meta+s': '_handleSaveShortcut',
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('content-change',
+ e => this._handleContentChange(e));
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._getEditPrefs().then(prefs => { this._prefs = prefs; });
+ }
+
+ get storageKey() {
+ return `c${this._changeNum}_ps${this._patchNum}_${this._path}`;
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getEditPrefs() {
+ return this.$.restAPI.getEditPreferences();
+ }
+
+ _paramsChanged(value) {
+ if (value.view !== Gerrit.Nav.View.EDIT) {
+ return;
+ }
+
+ this._changeNum = value.changeNum;
+ this._path = value.path;
+ this._patchNum = value.patchNum || this.EDIT_NAME;
+ this._lineNum = value.lineNum;
+
+ // NOTE: This may be called before attachment (e.g. while parentElement is
+ // null). Fire title-change in an async so that, if attachment to the DOM
+ // has been queued, the event can bubble up to the handler in gr-app.
+ this.async(() => {
+ const title = `Editing ${this.computeTruncatedPath(this._path)}`;
+ this.fire('title-change', {title});
+ });
+
+ const promises = [];
+
+ promises.push(this._getChangeDetail(this._changeNum));
+ promises.push(
+ this._getFileData(this._changeNum, this._path, this._patchNum));
+ return Promise.all(promises);
+ }
+
+ _getChangeDetail(changeNum) {
+ return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
+ this._change = change;
+ });
+ }
+
+ _handlePathChanged(e) {
+ const path = e.detail;
+ if (path === this._path) {
+ return Promise.resolve();
+ }
+ return this.$.restAPI.renameFileInChangeEdit(this._changeNum,
+ this._path, path).then(res => {
+ if (!res.ok) { return; }
+
+ this._successfulSave = true;
+ this._viewEditInChangeView();
+ });
+ }
+
+ _viewEditInChangeView() {
+ const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
+ Gerrit.Nav.navigateToChange(this._change, patch, null,
+ patch !== this.EDIT_NAME);
+ }
+
+ _getFileData(changeNum, path, patchNum) {
+ const storedContent =
+ this.$.storage.getEditableContentItem(this.storageKey);
+
+ return this.$.restAPI.getFileContent(changeNum, path, patchNum)
+ .then(res => {
+ if (storedContent && storedContent.message &&
+ storedContent.message !== res.content) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: RESTORED_MESSAGE},
+ bubbles: true,
+ composed: true,
+ }));
+
+ this._newContent = storedContent.message;
+ } else {
+ this._newContent = res.content || '';
+ }
+ this._content = res.content || '';
+
+ // A non-ok response may result if the file does not yet exist.
+ // The `type` field of the response is only valid when the file
+ // already exists.
+ if (res.ok && res.type) {
+ this._type = res.type;
+ } else {
+ this._type = '';
+ }
+ });
+ }
+
+ _saveEdit() {
+ this._saving = true;
+ this._showAlert(SAVING_MESSAGE);
+ this.$.storage.eraseEditableContentItem(this.storageKey);
+ return this.$.restAPI.saveChangeEdit(this._changeNum, this._path,
+ this._newContent).then(res => {
+ this._saving = false;
+ this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG);
+ if (!res.ok) { return; }
+
+ this._content = this._newContent;
+ this._successfulSave = true;
+ });
+ }
+
+ _showAlert(message) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message},
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ _computeSaveDisabled(content, newContent, saving) {
+ // Polymer 2: check for undefined
+ if ([
+ content,
+ newContent,
+ saving,
+ ].some(arg => arg === undefined)) {
+ return true;
+ }
+
+ if (saving) {
+ return true;
+ }
+ return content === newContent;
+ }
+
+ _handleCloseTap() {
+ // TODO(kaspern): Add a confirm dialog if there are unsaved changes.
+ this._viewEditInChangeView();
+ }
+
+ _handleContentChange(e) {
+ this.debounce('store', () => {
+ const content = e.detail.value;
+ if (content) {
+ this.set('_newContent', e.detail.value);
+ this.$.storage.setEditableContentItem(this.storageKey, content);
+ } else {
+ this.$.storage.eraseEditableContentItem(this.storageKey);
+ }
+ }, STORAGE_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _handleSaveShortcut(e) {
+ e.preventDefault();
+ if (!this._saveDisabled) {
+ this._saveEdit();
+ }
+ }
+}
+
+customElements.define(GrEditorView.is, GrEditorView);
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
index 1ae74e1..72d29dd 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
@@ -1,39 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
-<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../gr-default-editor/gr-default-editor.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-editor-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
background-color: var(--view-background-color);
@@ -93,28 +76,16 @@
}
}
</style>
- <gr-fixed-panel keep-on-scroll>
+ <gr-fixed-panel keep-on-scroll="">
<header>
<span class="controlGroup">
<span>Edit mode</span>
<span class="separator"></span>
- <gr-editable-label
- label-text="File path"
- value="[[_path]]"
- placeholder="File path..."
- on-changed="_handlePathChanged"></gr-editable-label>
+ <gr-editable-label label-text="File path" value="[[_path]]" placeholder="File path..." on-changed="_handlePathChanged"></gr-editable-label>
</span>
<span class="controlGroup rightControls">
- <gr-button
- id="close"
- link
- on-click="_handleCloseTap">Close</gr-button>
- <gr-button
- id="save"
- disabled$="[[_saveDisabled]]"
- primary
- link
- on-click="_saveEdit">Save</gr-button>
+ <gr-button id="close" link="" on-click="_handleCloseTap">Close</gr-button>
+ <gr-button id="save" disabled\$="[[_saveDisabled]]" primary="" link="" on-click="_saveEdit">Save</gr-button>
</span>
</header>
</gr-fixed-panel>
@@ -129,6 +100,4 @@
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-storage id="storage"></gr-storage>
- </template>
- <script src="gr-editor-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index 1d264bc..8c0d491 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -18,16 +18,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-editor-view</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>
-<link rel="import" href="gr-editor-view.html">
+<script type="module" src="./gr-editor-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-editor-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,380 +40,382 @@
</template>
</test-fixture>
-<script>
- suite('gr-editor-view tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let savePathStub;
- let saveFileStub;
- let changeDetailStub;
- let navigateStub;
- const mockParams = {
- changeNum: '42',
- path: 'foo/bar.baz',
- patchNum: 'edit',
- };
-
- setup(() => {
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getEditPreferences() { return Promise.resolve({}); },
- });
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
- saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit');
- changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail');
- navigateStub = sandbox.stub(element, '_viewEditInChangeView');
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-editor-view.js';
+suite('gr-editor-view tests', () => {
+ let element;
+ let sandbox;
+ let savePathStub;
+ let saveFileStub;
+ let changeDetailStub;
+ let navigateStub;
+ const mockParams = {
+ changeNum: '42',
+ path: 'foo/bar.baz',
+ patchNum: 'edit',
+ };
+
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getEditPreferences() { return Promise.resolve({}); },
});
-
- teardown(() => { sandbox.restore(); });
-
- suite('_paramsChanged', () => {
- test('incorrect view returns immediately', () => {
- element._paramsChanged(
- Object.assign({}, mockParams, {view: Gerrit.Nav.View.DIFF}));
- assert.notOk(element._changeNum);
- });
-
- test('good params proceed', () => {
- changeDetailStub.returns(Promise.resolve({}));
- const fileStub = sandbox.stub(element, '_getFileData', () => {
- element._content = 'text';
- element._newContent = 'text';
- element._type = 'application/octet-stream';
- });
-
- const promises = element._paramsChanged(
- Object.assign({}, mockParams, {view: Gerrit.Nav.View.EDIT}));
-
- flushAsynchronousOperations();
- assert.equal(element._changeNum, mockParams.changeNum);
- assert.equal(element._path, mockParams.path);
- assert.deepEqual(changeDetailStub.lastCall.args[0],
- mockParams.changeNum);
- assert.deepEqual(fileStub.lastCall.args,
- [mockParams.changeNum, mockParams.path, mockParams.patchNum]);
-
- return promises.then(() => {
- assert.equal(element._content, 'text');
- assert.equal(element._newContent, 'text');
- assert.equal(element._type, 'application/octet-stream');
- });
- });
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
+ saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit');
+ changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail');
+ navigateStub = sandbox.stub(element, '_viewEditInChangeView');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ suite('_paramsChanged', () => {
+ test('incorrect view returns immediately', () => {
+ element._paramsChanged(
+ Object.assign({}, mockParams, {view: Gerrit.Nav.View.DIFF}));
+ assert.notOk(element._changeNum);
});
-
- test('edit file path', () => {
- element._changeNum = mockParams.changeNum;
- element._path = mockParams.path;
- savePathStub.onFirstCall().returns(Promise.resolve({}));
- savePathStub.onSecondCall().returns(Promise.resolve({ok: true}));
-
- // Calling with the same path should not navigate.
- return element._handlePathChanged({detail: mockParams.path}).then(() => {
- assert.isFalse(savePathStub.called);
- // !ok response
- element._handlePathChanged({detail: 'newPath'}).then(() => {
- assert.isTrue(savePathStub.called);
- assert.isFalse(navigateStub.called);
- // ok response
- element._handlePathChanged({detail: 'newPath'}).then(() => {
- assert.isTrue(navigateStub.called);
- assert.isTrue(element._successfulSave);
- });
- });
+
+ test('good params proceed', () => {
+ changeDetailStub.returns(Promise.resolve({}));
+ const fileStub = sandbox.stub(element, '_getFileData', () => {
+ element._content = 'text';
+ element._newContent = 'text';
+ element._type = 'application/octet-stream';
});
- });
-
- test('reacts to content-change event', () => {
- const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
- element._newContent = 'test';
- element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
- bubbles: true, composed: true,
- detail: {value: 'new content value'},
- }));
- element.flushDebouncer('store');
+
+ const promises = element._paramsChanged(
+ Object.assign({}, mockParams, {view: Gerrit.Nav.View.EDIT}));
+
flushAsynchronousOperations();
-
- assert.equal(element._newContent, 'new content value');
- assert.isTrue(storeStub.called);
- assert.equal(storeStub.lastCall.args[1], 'new content value');
- });
-
- suite('edit file content', () => {
- const originalText = 'file text';
- const newText = 'file text changed';
-
- setup(() => {
- element._changeNum = mockParams.changeNum;
- element._path = mockParams.path;
- element._content = originalText;
- element._newContent = originalText;
- flushAsynchronousOperations();
- });
-
- test('initial load', () => {
- assert.equal(element.$.file.fileContent, originalText);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
- });
-
- test('file modification and save, !ok response', () => {
- const saveSpy = sandbox.spy(element, '_saveEdit');
- const eraseStub = sandbox.stub(element.$.storage,
- 'eraseEditableContentItem');
- const alertStub = sandbox.stub(element, '_showAlert');
- saveFileStub.returns(Promise.resolve({ok: false}));
- element._newContent = newText;
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.save.hasAttribute('disabled'));
- assert.isFalse(element._saving);
-
- MockInteractions.tap(element.$.save);
- assert.isTrue(saveSpy.called);
- assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
- assert.isTrue(element._saving);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
-
- return saveSpy.lastCall.returnValue.then(() => {
- assert.isTrue(saveFileStub.called);
- assert.isTrue(eraseStub.called);
- assert.isFalse(element._saving);
- assert.equal(alertStub.lastCall.args[0], 'Failed to save changes');
- assert.deepEqual(saveFileStub.lastCall.args,
- [mockParams.changeNum, mockParams.path, newText]);
- assert.isFalse(navigateStub.called);
- assert.isFalse(element.$.save.hasAttribute('disabled'));
- assert.notEqual(element._content, element._newContent);
- });
- });
-
- test('file modification and save', () => {
- const saveSpy = sandbox.spy(element, '_saveEdit');
- const alertStub = sandbox.stub(element, '_showAlert');
- saveFileStub.returns(Promise.resolve({ok: true}));
- element._newContent = newText;
- flushAsynchronousOperations();
-
- assert.isFalse(element._saving);
- assert.isFalse(element.$.save.hasAttribute('disabled'));
-
- MockInteractions.tap(element.$.save);
- assert.isTrue(saveSpy.called);
- assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
- assert.isTrue(element._saving);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
-
- return saveSpy.lastCall.returnValue.then(() => {
- assert.isTrue(saveFileStub.called);
- assert.isFalse(element._saving);
- assert.equal(alertStub.lastCall.args[0], 'All changes saved');
- assert.isFalse(navigateStub.called);
- assert.isTrue(element.$.save.hasAttribute('disabled'));
- assert.equal(element._content, element._newContent);
- assert.isTrue(element._successfulSave);
- });
- });
-
- test('file modification and close', () => {
- const closeSpy = sandbox.spy(element, '_handleCloseTap');
- element._newContent = newText;
- flushAsynchronousOperations();
-
- assert.isFalse(element.$.save.hasAttribute('disabled'));
-
- MockInteractions.tap(element.$.close);
- assert.isTrue(closeSpy.called);
- assert.isFalse(saveFileStub.called);
- assert.isTrue(navigateStub.called);
- });
- });
-
- suite('_getFileData', () => {
- setup(() => {
- element._newContent = 'initial';
- element._content = 'initial';
- element._type = 'initial';
- sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null);
- });
-
- test('res.ok', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- content: 'new content',
- }));
-
- // Ensure no data is set with a bad response.
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, 'new content');
- assert.equal(element._content, 'new content');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('!res.ok', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({}));
-
- // Ensure no data is set with a bad response.
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, '');
- assert.equal(element._content, '');
- assert.equal(element._type, '');
- });
- });
-
- test('content is undefined', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- }));
-
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, '');
- assert.equal(element._content, '');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('content and type is undefined', () => {
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- }));
-
- return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, '');
- assert.equal(element._content, '');
- assert.equal(element._type, '');
- });
- });
- });
-
- test('_showAlert', done => {
- element.addEventListener('show-alert', e => {
- assert.deepEqual(e.detail, {message: 'test message'});
- assert.isTrue(e.bubbles);
- done();
- });
-
- element._showAlert('test message');
- });
-
- test('_viewEditInChangeView respects _patchNum', () => {
- navigateStub.restore();
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element._patchNum = element.EDIT_NAME;
- element._viewEditInChangeView();
- assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
- element._patchNum = '1';
- element._viewEditInChangeView();
- assert.equal(navStub.lastCall.args[1], '1');
- element._successfulSave = true;
- element._viewEditInChangeView();
- assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
- });
-
- suite('keyboard shortcuts', () => {
- // Used as the spy on the handler for each entry in keyBindings.
- let handleSpy;
-
- suite('_handleSaveShortcut', () => {
- let saveStub;
- setup(() => {
- handleSpy = sandbox.spy(element, '_handleSaveShortcut');
- saveStub = sandbox.stub(element, '_saveEdit');
- });
-
- test('save enabled', () => {
- element._content = '';
- element._newContent = '_test';
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
- flushAsynchronousOperations();
-
- assert.isTrue(handleSpy.calledOnce);
- assert.isTrue(saveStub.calledOnce);
-
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
- flushAsynchronousOperations();
-
- assert.equal(handleSpy.callCount, 2);
- assert.equal(saveStub.callCount, 2);
- });
-
- test('save disabled', () => {
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
- flushAsynchronousOperations();
-
- assert.isTrue(handleSpy.calledOnce);
- assert.isFalse(saveStub.called);
-
- MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
- flushAsynchronousOperations();
-
- assert.equal(handleSpy.callCount, 2);
- assert.isFalse(saveStub.called);
- });
- });
- });
-
- suite('gr-storage caching', () => {
- test('local edit exists', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({message: 'pending edit'});
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- content: 'old content',
- }));
-
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
-
- return element._getFileData(1, 'test', 1).then(() => {
- flushAsynchronousOperations();
-
- assert.isTrue(alertStub.called);
- assert.equal(element._newContent, 'pending edit');
- assert.equal(element._content, 'old content');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('local edit exists, is same as remote edit', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({message: 'pending edit'});
- sandbox.stub(element.$.restAPI, 'getFileContent')
- .returns(Promise.resolve({
- ok: true,
- type: 'text/javascript',
- content: 'pending edit',
- }));
-
- const alertStub = sandbox.stub();
- element.addEventListener('show-alert', alertStub);
-
- return element._getFileData(1, 'test', 1).then(() => {
- flushAsynchronousOperations();
-
- assert.isFalse(alertStub.called);
- assert.equal(element._newContent, 'pending edit');
- assert.equal(element._content, 'pending edit');
- assert.equal(element._type, 'text/javascript');
- });
- });
-
- test('storage key computation', () => {
- element._changeNum = 1;
- element._patchNum = 1;
- element._path = 'test';
- assert.equal(element.storageKey, 'c1_ps1_test');
+ assert.equal(element._changeNum, mockParams.changeNum);
+ assert.equal(element._path, mockParams.path);
+ assert.deepEqual(changeDetailStub.lastCall.args[0],
+ mockParams.changeNum);
+ assert.deepEqual(fileStub.lastCall.args,
+ [mockParams.changeNum, mockParams.path, mockParams.patchNum]);
+
+ return promises.then(() => {
+ assert.equal(element._content, 'text');
+ assert.equal(element._newContent, 'text');
+ assert.equal(element._type, 'application/octet-stream');
});
});
});
+
+ test('edit file path', () => {
+ element._changeNum = mockParams.changeNum;
+ element._path = mockParams.path;
+ savePathStub.onFirstCall().returns(Promise.resolve({}));
+ savePathStub.onSecondCall().returns(Promise.resolve({ok: true}));
+
+ // Calling with the same path should not navigate.
+ return element._handlePathChanged({detail: mockParams.path}).then(() => {
+ assert.isFalse(savePathStub.called);
+ // !ok response
+ element._handlePathChanged({detail: 'newPath'}).then(() => {
+ assert.isTrue(savePathStub.called);
+ assert.isFalse(navigateStub.called);
+ // ok response
+ element._handlePathChanged({detail: 'newPath'}).then(() => {
+ assert.isTrue(navigateStub.called);
+ assert.isTrue(element._successfulSave);
+ });
+ });
+ });
+ });
+
+ test('reacts to content-change event', () => {
+ const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
+ element._newContent = 'test';
+ element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
+ bubbles: true, composed: true,
+ detail: {value: 'new content value'},
+ }));
+ element.flushDebouncer('store');
+ flushAsynchronousOperations();
+
+ assert.equal(element._newContent, 'new content value');
+ assert.isTrue(storeStub.called);
+ assert.equal(storeStub.lastCall.args[1], 'new content value');
+ });
+
+ suite('edit file content', () => {
+ const originalText = 'file text';
+ const newText = 'file text changed';
+
+ setup(() => {
+ element._changeNum = mockParams.changeNum;
+ element._path = mockParams.path;
+ element._content = originalText;
+ element._newContent = originalText;
+ flushAsynchronousOperations();
+ });
+
+ test('initial load', () => {
+ assert.equal(element.$.file.fileContent, originalText);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+ });
+
+ test('file modification and save, !ok response', () => {
+ const saveSpy = sandbox.spy(element, '_saveEdit');
+ const eraseStub = sandbox.stub(element.$.storage,
+ 'eraseEditableContentItem');
+ const alertStub = sandbox.stub(element, '_showAlert');
+ saveFileStub.returns(Promise.resolve({ok: false}));
+ element._newContent = newText;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+ assert.isFalse(element._saving);
+
+ MockInteractions.tap(element.$.save);
+ assert.isTrue(saveSpy.called);
+ assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
+ assert.isTrue(element._saving);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+
+ return saveSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(saveFileStub.called);
+ assert.isTrue(eraseStub.called);
+ assert.isFalse(element._saving);
+ assert.equal(alertStub.lastCall.args[0], 'Failed to save changes');
+ assert.deepEqual(saveFileStub.lastCall.args,
+ [mockParams.changeNum, mockParams.path, newText]);
+ assert.isFalse(navigateStub.called);
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+ assert.notEqual(element._content, element._newContent);
+ });
+ });
+
+ test('file modification and save', () => {
+ const saveSpy = sandbox.spy(element, '_saveEdit');
+ const alertStub = sandbox.stub(element, '_showAlert');
+ saveFileStub.returns(Promise.resolve({ok: true}));
+ element._newContent = newText;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element._saving);
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+
+ MockInteractions.tap(element.$.save);
+ assert.isTrue(saveSpy.called);
+ assert.equal(alertStub.lastCall.args[0], 'Saving changes...');
+ assert.isTrue(element._saving);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+
+ return saveSpy.lastCall.returnValue.then(() => {
+ assert.isTrue(saveFileStub.called);
+ assert.isFalse(element._saving);
+ assert.equal(alertStub.lastCall.args[0], 'All changes saved');
+ assert.isFalse(navigateStub.called);
+ assert.isTrue(element.$.save.hasAttribute('disabled'));
+ assert.equal(element._content, element._newContent);
+ assert.isTrue(element._successfulSave);
+ });
+ });
+
+ test('file modification and close', () => {
+ const closeSpy = sandbox.spy(element, '_handleCloseTap');
+ element._newContent = newText;
+ flushAsynchronousOperations();
+
+ assert.isFalse(element.$.save.hasAttribute('disabled'));
+
+ MockInteractions.tap(element.$.close);
+ assert.isTrue(closeSpy.called);
+ assert.isFalse(saveFileStub.called);
+ assert.isTrue(navigateStub.called);
+ });
+ });
+
+ suite('_getFileData', () => {
+ setup(() => {
+ element._newContent = 'initial';
+ element._content = 'initial';
+ element._type = 'initial';
+ sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null);
+ });
+
+ test('res.ok', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ content: 'new content',
+ }));
+
+ // Ensure no data is set with a bad response.
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, 'new content');
+ assert.equal(element._content, 'new content');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('!res.ok', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({}));
+
+ // Ensure no data is set with a bad response.
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, '');
+ assert.equal(element._content, '');
+ assert.equal(element._type, '');
+ });
+ });
+
+ test('content is undefined', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ }));
+
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, '');
+ assert.equal(element._content, '');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('content and type is undefined', () => {
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ }));
+
+ return element._getFileData('1', 'test/path', 'edit').then(() => {
+ assert.equal(element._newContent, '');
+ assert.equal(element._content, '');
+ assert.equal(element._type, '');
+ });
+ });
+ });
+
+ test('_showAlert', done => {
+ element.addEventListener('show-alert', e => {
+ assert.deepEqual(e.detail, {message: 'test message'});
+ assert.isTrue(e.bubbles);
+ done();
+ });
+
+ element._showAlert('test message');
+ });
+
+ test('_viewEditInChangeView respects _patchNum', () => {
+ navigateStub.restore();
+ const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
+ element._patchNum = element.EDIT_NAME;
+ element._viewEditInChangeView();
+ assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
+ element._patchNum = '1';
+ element._viewEditInChangeView();
+ assert.equal(navStub.lastCall.args[1], '1');
+ element._successfulSave = true;
+ element._viewEditInChangeView();
+ assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
+ });
+
+ suite('keyboard shortcuts', () => {
+ // Used as the spy on the handler for each entry in keyBindings.
+ let handleSpy;
+
+ suite('_handleSaveShortcut', () => {
+ let saveStub;
+ setup(() => {
+ handleSpy = sandbox.spy(element, '_handleSaveShortcut');
+ saveStub = sandbox.stub(element, '_saveEdit');
+ });
+
+ test('save enabled', () => {
+ element._content = '';
+ element._newContent = '_test';
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleSpy.calledOnce);
+ assert.isTrue(saveStub.calledOnce);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
+ flushAsynchronousOperations();
+
+ assert.equal(handleSpy.callCount, 2);
+ assert.equal(saveStub.callCount, 2);
+ });
+
+ test('save disabled', () => {
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleSpy.calledOnce);
+ assert.isFalse(saveStub.called);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's');
+ flushAsynchronousOperations();
+
+ assert.equal(handleSpy.callCount, 2);
+ assert.isFalse(saveStub.called);
+ });
+ });
+ });
+
+ suite('gr-storage caching', () => {
+ test('local edit exists', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({message: 'pending edit'});
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ content: 'old content',
+ }));
+
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+
+ return element._getFileData(1, 'test', 1).then(() => {
+ flushAsynchronousOperations();
+
+ assert.isTrue(alertStub.called);
+ assert.equal(element._newContent, 'pending edit');
+ assert.equal(element._content, 'old content');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('local edit exists, is same as remote edit', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({message: 'pending edit'});
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ content: 'pending edit',
+ }));
+
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+
+ return element._getFileData(1, 'test', 1).then(() => {
+ flushAsynchronousOperations();
+
+ assert.isFalse(alertStub.called);
+ assert.equal(element._newContent, 'pending edit');
+ assert.equal(element._content, 'pending edit');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('storage key computation', () => {
+ element._changeNum = 1;
+ element._patchNum = 1;
+ element._path = 'test';
+ assert.equal(element.storageKey, 'c1_ps1_test');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index ea5a180..785e8f9 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -14,531 +14,568 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../scripts/util.js';
+import '../scripts/bundled-polymer.js';
+import '../behaviors/base-url-behavior/base-url-behavior.js';
+import '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../styles/shared-styles.js';
+import '../styles/themes/app-theme.js';
+import './admin/gr-admin-view/gr-admin-view.js';
+import './documentation/gr-documentation-search/gr-documentation-search.js';
+import './change-list/gr-change-list-view/gr-change-list-view.js';
+import './change-list/gr-dashboard-view/gr-dashboard-view.js';
+import './change/gr-change-view/gr-change-view.js';
+import './core/gr-error-manager/gr-error-manager.js';
+import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js';
+import './core/gr-main-header/gr-main-header.js';
+import './core/gr-navigation/gr-navigation.js';
+import './core/gr-reporting/gr-reporting.js';
+import './core/gr-router/gr-router.js';
+import './core/gr-smart-search/gr-smart-search.js';
+import './diff/gr-diff-view/gr-diff-view.js';
+import './edit/gr-editor-view/gr-editor-view.js';
+import './plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './plugins/gr-endpoint-param/gr-endpoint-param.js';
+import './plugins/gr-external-style/gr-external-style.js';
+import './plugins/gr-plugin-host/gr-plugin-host.js';
+import './settings/gr-cla-view/gr-cla-view.js';
+import './settings/gr-registration-dialog/gr-registration-dialog.js';
+import './settings/gr-settings-view/gr-settings-view.js';
+import './shared/gr-fixed-panel/gr-fixed-panel.js';
+import './shared/gr-lib-loader/gr-lib-loader.js';
+import './shared/gr-rest-api-interface/gr-rest-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 moment from 'moment/src/moment.js';
+self.moment = moment;
+import {htmlTemplate} from './gr-app-element_html.js';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrAppElement extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-app-element'; }
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the URL location changes.
+ *
+ * @event location-change
*/
- class GrAppElement extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-app-element'; }
- /**
- * Fired when the URL location changes.
- *
- * @event location-change
- */
- static get properties() {
- return {
+ static get properties() {
+ return {
+ /**
+ * @type {{ query: string, view: string, screen: string }}
+ */
+ params: Object,
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+
+ _account: {
+ type: Object,
+ observer: '_accountChanged',
+ },
+
/**
- * @type {{ query: string, view: string, screen: string }}
+ * The last time the g key was pressed in milliseconds (or a keydown event
+ * was handled if the key is held down).
+ *
+ * @type {number|null}
*/
- params: Object,
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
+ _lastGKeyPressTimestamp: {
+ type: Number,
+ value: null,
+ },
- _account: {
- type: Object,
- observer: '_accountChanged',
- },
+ /**
+ * @type {{ plugin: Object }}
+ */
+ _serverConfig: Object,
+ _version: String,
+ _showChangeListView: Boolean,
+ _showDashboardView: Boolean,
+ _showChangeView: Boolean,
+ _showDiffView: Boolean,
+ _showSettingsView: Boolean,
+ _showAdminView: Boolean,
+ _showCLAView: Boolean,
+ _showEditorView: Boolean,
+ _showPluginScreen: Boolean,
+ _showDocumentationSearch: Boolean,
+ /** @type {?} */
+ _viewState: Object,
+ /** @type {?} */
+ _lastError: Object,
+ _lastSearchPage: String,
+ _path: String,
+ _pluginScreenName: {
+ type: String,
+ computed: '_computePluginScreenName(params)',
+ },
+ _settingsUrl: String,
+ _feedbackUrl: String,
+ // Used to allow searching on mobile
+ mobileSearch: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * The last time the g key was pressed in milliseconds (or a keydown event
- * was handled if the key is held down).
- *
- * @type {number|null}
- */
- _lastGKeyPressTimestamp: {
- type: Number,
- value: null,
- },
+ /**
+ * Other elements in app must open this URL when
+ * user login is required.
+ */
+ _loginUrl: {
+ type: String,
+ value: '/login',
+ },
+ };
+ }
- /**
- * @type {{ plugin: Object }}
- */
- _serverConfig: Object,
- _version: String,
- _showChangeListView: Boolean,
- _showDashboardView: Boolean,
- _showChangeView: Boolean,
- _showDiffView: Boolean,
- _showSettingsView: Boolean,
- _showAdminView: Boolean,
- _showCLAView: Boolean,
- _showEditorView: Boolean,
- _showPluginScreen: Boolean,
- _showDocumentationSearch: Boolean,
- /** @type {?} */
- _viewState: Object,
- /** @type {?} */
- _lastError: Object,
- _lastSearchPage: String,
- _path: String,
- _pluginScreenName: {
- type: String,
- computed: '_computePluginScreenName(params)',
- },
- _settingsUrl: String,
- _feedbackUrl: String,
- // Used to allow searching on mobile
- mobileSearch: {
- type: Boolean,
- value: false,
- },
+ static get observers() {
+ return [
+ '_viewChanged(params.view)',
+ '_paramsChanged(params.*)',
+ ];
+ }
- /**
- * Other elements in app must open this URL when
- * user login is required.
- */
- _loginUrl: {
- type: String,
- value: '/login',
- },
- };
- }
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
+ [this.Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard',
+ [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
+ [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
+ [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
+ [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges',
+ };
+ }
- static get observers() {
- return [
- '_viewChanged(params.view)',
- '_paramsChanged(params.*)',
- ];
- }
+ /** @override */
+ created() {
+ super.created();
+ this._bindKeyboardShortcuts();
+ this.addEventListener('page-error',
+ e => this._handlePageError(e));
+ this.addEventListener('title-change',
+ e => this._handleTitleChange(e));
+ this.addEventListener('location-change',
+ e => this._handleLocationChange(e));
+ this.addEventListener('rpc-log',
+ e => this._handleRpcLog(e));
+ this.addEventListener('shortcut-triggered',
+ e => this._handleShortcutTriggered(e));
+ }
- keyboardShortcuts() {
- return {
- [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
- [this.Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard',
- [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
- [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
- [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
- [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges',
- };
- }
+ /** @override */
+ ready() {
+ super.ready();
+ this._updateLoginUrl();
+ this.$.reporting.appStarted();
+ this.$.router.start();
- /** @override */
- created() {
- super.created();
- this._bindKeyboardShortcuts();
- this.addEventListener('page-error',
- e => this._handlePageError(e));
- this.addEventListener('title-change',
- e => this._handleTitleChange(e));
- this.addEventListener('location-change',
- e => this._handleLocationChange(e));
- this.addEventListener('rpc-log',
- e => this._handleRpcLog(e));
- this.addEventListener('shortcut-triggered',
- e => this._handleShortcutTriggered(e));
- }
+ this.$.restAPI.getAccount().then(account => {
+ this._account = account;
+ });
+ this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
- /** @override */
- ready() {
- super.ready();
- this._updateLoginUrl();
- this.$.reporting.appStarted();
- this.$.router.start();
-
- this.$.restAPI.getAccount().then(account => {
- this._account = account;
- });
- this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
-
- if (config && config.gerrit && config.gerrit.report_bug_url) {
- this._feedbackUrl = config.gerrit.report_bug_url;
- }
- });
- this.$.restAPI.getVersion().then(version => {
- this._version = version;
- this._logWelcome();
- });
-
- if (window.localStorage.getItem('dark-theme')) {
- // No need to add the style module to element again as it's imported
- // by importHref already
- this.$.libLoader.getDarkTheme();
+ if (config && config.gerrit && config.gerrit.report_bug_url) {
+ this._feedbackUrl = config.gerrit.report_bug_url;
}
+ });
+ this.$.restAPI.getVersion().then(version => {
+ this._version = version;
+ this._logWelcome();
+ });
- // Note: this is evaluated here to ensure that it only happens after the
- // router has been initialized. @see Issue 7837
- this._settingsUrl = Gerrit.Nav.getUrlForSettings();
-
- this._viewState = {
- changeView: {
- changeNum: null,
- patchRange: null,
- selectedFileIndex: 0,
- showReplyDialog: false,
- diffMode: null,
- numFilesShown: null,
- scrollTop: 0,
- },
- changeListView: {
- query: null,
- offset: 0,
- selectedChangeIndex: 0,
- },
- dashboardView: {
- selectedChangeIndex: 0,
- },
- };
+ if (window.localStorage.getItem('dark-theme')) {
+ // No need to add the style module to element again as it's imported
+ // by importHref already
+ this.$.libLoader.getDarkTheme();
}
- _bindKeyboardShortcuts() {
- this.bindShortcut(this.Shortcut.SEND_REPLY,
- this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
- this.bindShortcut(this.Shortcut.EMOJI_DROPDOWN,
- this.DOC_ONLY, ':');
+ // Note: this is evaluated here to ensure that it only happens after the
+ // router has been initialized. @see Issue 7837
+ this._settingsUrl = Gerrit.Nav.getUrlForSettings();
- this.bindShortcut(
- this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
- this.bindShortcut(
- this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i');
- this.bindShortcut(
- this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
- this.bindShortcut(
- this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
- this.bindShortcut(
- this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
- this.bindShortcut(
- this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w');
+ this._viewState = {
+ changeView: {
+ changeNum: null,
+ patchRange: null,
+ selectedFileIndex: 0,
+ showReplyDialog: false,
+ diffMode: null,
+ numFilesShown: null,
+ scrollTop: 0,
+ },
+ changeListView: {
+ query: null,
+ offset: 0,
+ selectedChangeIndex: 0,
+ },
+ dashboardView: {
+ selectedChangeIndex: 0,
+ },
+ };
+ }
- this.bindShortcut(
- this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
- this.bindShortcut(
- this.Shortcut.CURSOR_PREV_CHANGE, 'k');
- this.bindShortcut(
- this.Shortcut.OPEN_CHANGE, 'o');
- this.bindShortcut(
- this.Shortcut.NEXT_PAGE, 'n', ']');
- this.bindShortcut(
- this.Shortcut.PREV_PAGE, 'p', '[');
- this.bindShortcut(
- this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
- this.bindShortcut(
- this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup');
- this.bindShortcut(
- this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
- this.bindShortcut(
- this.Shortcut.EDIT_TOPIC, 't');
+ _bindKeyboardShortcuts() {
+ this.bindShortcut(this.Shortcut.SEND_REPLY,
+ this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
+ this.bindShortcut(this.Shortcut.EMOJI_DROPDOWN,
+ this.DOC_ONLY, ':');
- this.bindShortcut(
- this.Shortcut.OPEN_REPLY_DIALOG, 'a');
- this.bindShortcut(
- this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
- this.bindShortcut(
- this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
- this.bindShortcut(
- this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
- this.bindShortcut(
- this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
- this.bindShortcut(
- this.Shortcut.UP_TO_DASHBOARD, 'u');
- this.bindShortcut(
- this.Shortcut.UP_TO_CHANGE, 'u');
- this.bindShortcut(
- this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
+ this.bindShortcut(
+ this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w');
- this.bindShortcut(
- this.Shortcut.NEXT_LINE, 'j', 'down');
- this.bindShortcut(
- this.Shortcut.PREV_LINE, 'k', 'up');
- if (this._isCursorManagerSupportMoveToVisibleLine()) {
- this.bindShortcut(
- this.Shortcut.VISIBLE_LINE, '.');
- }
- this.bindShortcut(
- this.Shortcut.NEXT_CHUNK, 'n');
- this.bindShortcut(
- this.Shortcut.PREV_CHUNK, 'p');
- this.bindShortcut(
- this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
- this.bindShortcut(
- this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
- this.bindShortcut(
- this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
- this.bindShortcut(
- this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
- this.bindShortcut(
- this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
- this.DOC_ONLY, 'shift+e');
- this.bindShortcut(
- this.Shortcut.LEFT_PANE, 'shift+left');
- this.bindShortcut(
- this.Shortcut.RIGHT_PANE, 'shift+right');
- this.bindShortcut(
- this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
- this.bindShortcut(
- this.Shortcut.NEW_COMMENT, 'c');
- this.bindShortcut(
- this.Shortcut.SAVE_COMMENT,
- 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
- this.bindShortcut(
- this.Shortcut.OPEN_DIFF_PREFS, ',');
- this.bindShortcut(
- this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_PREV_CHANGE, 'k');
+ this.bindShortcut(
+ this.Shortcut.OPEN_CHANGE, 'o');
+ this.bindShortcut(
+ this.Shortcut.NEXT_PAGE, 'n', ']');
+ this.bindShortcut(
+ this.Shortcut.PREV_PAGE, 'p', '[');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup');
+ this.bindShortcut(
+ this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
+ this.bindShortcut(
+ this.Shortcut.EDIT_TOPIC, 't');
- this.bindShortcut(
- this.Shortcut.NEXT_FILE, ']');
- this.bindShortcut(
- this.Shortcut.PREV_FILE, '[');
- this.bindShortcut(
- this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
- this.bindShortcut(
- this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
- this.bindShortcut(
- this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
- this.bindShortcut(
- this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
- this.bindShortcut(
- this.Shortcut.OPEN_FILE, 'o', 'enter');
- this.bindShortcut(
- this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
- this.bindShortcut(
- this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
- this.bindShortcut(
- this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
- this.bindShortcut(
- this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
- this.bindShortcut(
- this.Shortcut.TOGGLE_BLAME, 'b');
+ this.bindShortcut(
+ this.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ this.bindShortcut(
+ this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+ this.bindShortcut(
+ this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+ this.bindShortcut(
+ this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
+ this.bindShortcut(
+ this.Shortcut.UP_TO_DASHBOARD, 'u');
+ this.bindShortcut(
+ this.Shortcut.UP_TO_CHANGE, 'u');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
+ this.bindShortcut(
+ this.Shortcut.NEXT_LINE, 'j', 'down');
+ this.bindShortcut(
+ this.Shortcut.PREV_LINE, 'k', 'up');
+ if (this._isCursorManagerSupportMoveToVisibleLine()) {
this.bindShortcut(
- this.Shortcut.OPEN_FIRST_FILE, ']');
- this.bindShortcut(
- this.Shortcut.OPEN_LAST_FILE, '[');
-
- this.bindShortcut(
- this.Shortcut.SEARCH, '/');
+ this.Shortcut.VISIBLE_LINE, '.');
}
+ this.bindShortcut(
+ this.Shortcut.NEXT_CHUNK, 'n');
+ this.bindShortcut(
+ this.Shortcut.PREV_CHUNK, 'p');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+ this.bindShortcut(
+ this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+ this.bindShortcut(
+ this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
+ this.bindShortcut(
+ this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+ this.DOC_ONLY, 'shift+e');
+ this.bindShortcut(
+ this.Shortcut.LEFT_PANE, 'shift+left');
+ this.bindShortcut(
+ this.Shortcut.RIGHT_PANE, 'shift+right');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ this.bindShortcut(
+ this.Shortcut.NEW_COMMENT, 'c');
+ this.bindShortcut(
+ this.Shortcut.SAVE_COMMENT,
+ 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+ this.bindShortcut(
+ this.Shortcut.OPEN_DIFF_PREFS, ',');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
- _isCursorManagerSupportMoveToVisibleLine() {
- // This method is a copy-paste from the
- // method _isIntersectionObserverSupported of gr-cursor-manager.js
- // It is better share this method with gr-cursor-manager,
- // but doing it require a lot if changes instead of 1-line copied code
- return 'IntersectionObserver' in window;
+ this.bindShortcut(
+ this.Shortcut.NEXT_FILE, ']');
+ this.bindShortcut(
+ this.Shortcut.PREV_FILE, '[');
+ this.bindShortcut(
+ this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+ this.bindShortcut(
+ this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+ this.bindShortcut(
+ this.Shortcut.OPEN_FILE, 'o', 'enter');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
+ this.bindShortcut(
+ this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_BLAME, 'b');
+
+ this.bindShortcut(
+ this.Shortcut.OPEN_FIRST_FILE, ']');
+ this.bindShortcut(
+ this.Shortcut.OPEN_LAST_FILE, '[');
+
+ this.bindShortcut(
+ this.Shortcut.SEARCH, '/');
+ }
+
+ _isCursorManagerSupportMoveToVisibleLine() {
+ // This method is a copy-paste from the
+ // method _isIntersectionObserverSupported of gr-cursor-manager.js
+ // It is better share this method with gr-cursor-manager,
+ // but doing it require a lot if changes instead of 1-line copied code
+ return 'IntersectionObserver' in window;
+ }
+
+ _accountChanged(account) {
+ if (!account) { return; }
+
+ // Preferences are cached when a user is logged in; warm them.
+ this.$.restAPI.getPreferences();
+ this.$.restAPI.getDiffPreferences();
+ this.$.restAPI.getEditPreferences();
+ this.$.errorManager.knownAccountId =
+ this._account && this._account._account_id || null;
+ }
+
+ _viewChanged(view) {
+ this.$.errorView.classList.remove('show');
+ this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH);
+ this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD);
+ this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE);
+ this.set('_showDiffView', view === Gerrit.Nav.View.DIFF);
+ this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS);
+ this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN ||
+ view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO);
+ this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
+ this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
+ const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
+ this.set('_showPluginScreen', false);
+ // Navigation within plugin screens does not restamp gr-endpoint-decorator
+ // because _showPluginScreen value does not change. To force restamp,
+ // change _showPluginScreen value between true and false.
+ if (isPluginScreen) {
+ this.async(() => this.set('_showPluginScreen', true), 1);
}
-
- _accountChanged(account) {
- if (!account) { return; }
-
- // Preferences are cached when a user is logged in; warm them.
- this.$.restAPI.getPreferences();
- this.$.restAPI.getDiffPreferences();
- this.$.restAPI.getEditPreferences();
- this.$.errorManager.knownAccountId =
- this._account && this._account._account_id || null;
- }
-
- _viewChanged(view) {
- this.$.errorView.classList.remove('show');
- this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH);
- this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD);
- this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE);
- this.set('_showDiffView', view === Gerrit.Nav.View.DIFF);
- this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS);
- this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN ||
- view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO);
- this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
- this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
- const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN;
- this.set('_showPluginScreen', false);
- // Navigation within plugin screens does not restamp gr-endpoint-decorator
- // because _showPluginScreen value does not change. To force restamp,
- // change _showPluginScreen value between true and false.
- if (isPluginScreen) {
- this.async(() => this.set('_showPluginScreen', true), 1);
- }
- this.set('_showDocumentationSearch',
- view === Gerrit.Nav.View.DOCUMENTATION_SEARCH);
- if (this.params.justRegistered) {
- this.$.registrationOverlay.open();
- this.$.registrationDialog.loadData().then(() => {
- this.$.registrationOverlay.refit();
- });
- }
- this.$.header.unfloat();
- }
-
- _handleShortcutTriggered(event) {
- const {event: e, goKey} = event.detail;
- // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
- let key = `${e.key}:${e.type}`;
- if (goKey) key = 'g+' + key;
- if (e.shiftKey) key = 'shift+' + key;
- if (e.ctrlKey) key = 'ctrl+' + key;
- if (e.metaKey) key = 'meta+' + key;
- if (e.altKey) key = 'alt+' + key;
- this.$.reporting.reportInteraction('shortcut-triggered', {
- key,
- from: event.path && event.path[0]
- && event.path[0].nodeName || 'unknown',
+ this.set('_showDocumentationSearch',
+ view === Gerrit.Nav.View.DOCUMENTATION_SEARCH);
+ if (this.params.justRegistered) {
+ this.$.registrationOverlay.open();
+ this.$.registrationDialog.loadData().then(() => {
+ this.$.registrationOverlay.refit();
});
}
+ this.$.header.unfloat();
+ }
- _handlePageError(e) {
- const props = [
- '_showChangeListView',
- '_showDashboardView',
- '_showChangeView',
- '_showDiffView',
- '_showSettingsView',
- '_showAdminView',
- ];
- for (const showProp of props) {
- this.set(showProp, false);
- }
+ _handleShortcutTriggered(event) {
+ const {event: e, goKey} = event.detail;
+ // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
+ let key = `${e.key}:${e.type}`;
+ if (goKey) key = 'g+' + key;
+ if (e.shiftKey) key = 'shift+' + key;
+ if (e.ctrlKey) key = 'ctrl+' + key;
+ if (e.metaKey) key = 'meta+' + key;
+ if (e.altKey) key = 'alt+' + key;
+ this.$.reporting.reportInteraction('shortcut-triggered', {
+ key,
+ from: event.path && event.path[0]
+ && event.path[0].nodeName || 'unknown',
+ });
+ }
- this.$.errorView.classList.add('show');
- const response = e.detail.response;
- const err = {text: [response.status, response.statusText].join(' ')};
- if (response.status === 404) {
- err.emoji = '¯\\_(ツ)_/¯';
+ _handlePageError(e) {
+ const props = [
+ '_showChangeListView',
+ '_showDashboardView',
+ '_showChangeView',
+ '_showDiffView',
+ '_showSettingsView',
+ '_showAdminView',
+ ];
+ for (const showProp of props) {
+ this.set(showProp, false);
+ }
+
+ this.$.errorView.classList.add('show');
+ const response = e.detail.response;
+ const err = {text: [response.status, response.statusText].join(' ')};
+ if (response.status === 404) {
+ err.emoji = '¯\\_(ツ)_/¯';
+ this._lastError = err;
+ } else {
+ err.emoji = 'o_O';
+ response.text().then(text => {
+ err.moreInfo = text;
this._lastError = err;
- } else {
- err.emoji = 'o_O';
- response.text().then(text => {
- err.moreInfo = text;
- this._lastError = err;
- });
- }
- }
-
- _handleLocationChange(e) {
- this._updateLoginUrl();
-
- const hash = e.detail.hash.substring(1);
- let pathname = e.detail.pathname;
- if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
- pathname += '@' + hash;
- }
- this.set('_path', pathname);
- }
-
- _updateLoginUrl() {
- const baseUrl = this.getBaseUrl();
- if (baseUrl) {
- // Strip the canonical path from the path since needing canonical in
- // the path is uneeded and breaks the url.
- this._loginUrl = baseUrl + '/login/' + encodeURIComponent(
- '/' + window.location.pathname.substring(baseUrl.length) +
- window.location.search +
- window.location.hash);
- } else {
- this._loginUrl = '/login/' + encodeURIComponent(
- window.location.pathname +
- window.location.search +
- window.location.hash);
- }
- }
-
- _paramsChanged(paramsRecord) {
- const params = paramsRecord.base;
- const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD];
- if (viewsToCheck.includes(params.view)) {
- this.set('_lastSearchPage', location.pathname);
- }
- }
-
- _handleTitleChange(e) {
- if (e.detail.title) {
- document.title = e.detail.title + ' · Gerrit Code Review';
- } else {
- document.title = '';
- }
- }
-
- _showKeyboardShortcuts(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
- this.$.keyboardShortcuts.open();
- }
-
- _handleKeyboardShortcutDialogClose() {
- this.$.keyboardShortcuts.close();
- }
-
- _handleAccountDetailUpdate(e) {
- this.$.mainHeader.reload();
- if (this.params.view === Gerrit.Nav.View.SETTINGS) {
- this.shadowRoot.querySelector('gr-settings-view').reloadAccountDetail();
- }
- }
-
- _handleRegistrationDialogClose(e) {
- this.params.justRegistered = false;
- this.$.registrationOverlay.close();
- }
-
- _goToOpenedChanges() {
- Gerrit.Nav.navigateToStatusSearch('open');
- }
-
- _goToUserDashboard() {
- Gerrit.Nav.navigateToUserDashboard();
- }
-
- _goToMergedChanges() {
- Gerrit.Nav.navigateToStatusSearch('merged');
- }
-
- _goToAbandonedChanges() {
- Gerrit.Nav.navigateToStatusSearch('abandoned');
- }
-
- _goToWatchedChanges() {
- // The query is hardcoded, and doesn't respect custom menu entries
- Gerrit.Nav.navigateToSearchQuery('is:watched is:open');
- }
-
- _computePluginScreenName({plugin, screen}) {
- if (!plugin || !screen) return '';
- return `${plugin}-screen-${screen}`;
- }
-
- _logWelcome() {
- console.group('Runtime Info');
- console.log('Gerrit UI (PolyGerrit)');
- console.log(`Gerrit Server Version: ${this._version}`);
- if (window.VERSION_INFO) {
- console.log(`UI Version Info: ${window.VERSION_INFO}`);
- }
- if (this._feedbackUrl) {
- console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
- }
- console.groupEnd();
- }
-
- /**
- * Intercept RPC log events emitted by REST API interfaces.
- * Note: the REST API interface cannot use gr-reporting directly because
- * that would create a cyclic dependency.
- */
- _handleRpcLog(e) {
- this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
- e.detail.elapsed);
- }
-
- _mobileSearchToggle(e) {
- this.mobileSearch = !this.mobileSearch;
- }
-
- getThemeEndpoint() {
- // For now, we only have dark mode and light mode
- return window.localStorage.getItem('dark-theme') ?
- 'app-theme-dark' :
- 'app-theme-light';
+ });
}
}
- customElements.define(GrAppElement.is, GrAppElement);
-})();
+ _handleLocationChange(e) {
+ this._updateLoginUrl();
+
+ const hash = e.detail.hash.substring(1);
+ let pathname = e.detail.pathname;
+ if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
+ pathname += '@' + hash;
+ }
+ this.set('_path', pathname);
+ }
+
+ _updateLoginUrl() {
+ const baseUrl = this.getBaseUrl();
+ if (baseUrl) {
+ // Strip the canonical path from the path since needing canonical in
+ // the path is uneeded and breaks the url.
+ this._loginUrl = baseUrl + '/login/' + encodeURIComponent(
+ '/' + window.location.pathname.substring(baseUrl.length) +
+ window.location.search +
+ window.location.hash);
+ } else {
+ this._loginUrl = '/login/' + encodeURIComponent(
+ window.location.pathname +
+ window.location.search +
+ window.location.hash);
+ }
+ }
+
+ _paramsChanged(paramsRecord) {
+ const params = paramsRecord.base;
+ const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD];
+ if (viewsToCheck.includes(params.view)) {
+ this.set('_lastSearchPage', location.pathname);
+ }
+ }
+
+ _handleTitleChange(e) {
+ if (e.detail.title) {
+ document.title = e.detail.title + ' · Gerrit Code Review';
+ } else {
+ document.title = '';
+ }
+ }
+
+ _showKeyboardShortcuts(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ this.$.keyboardShortcuts.open();
+ }
+
+ _handleKeyboardShortcutDialogClose() {
+ this.$.keyboardShortcuts.close();
+ }
+
+ _handleAccountDetailUpdate(e) {
+ this.$.mainHeader.reload();
+ if (this.params.view === Gerrit.Nav.View.SETTINGS) {
+ this.shadowRoot.querySelector('gr-settings-view').reloadAccountDetail();
+ }
+ }
+
+ _handleRegistrationDialogClose(e) {
+ this.params.justRegistered = false;
+ this.$.registrationOverlay.close();
+ }
+
+ _goToOpenedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('open');
+ }
+
+ _goToUserDashboard() {
+ Gerrit.Nav.navigateToUserDashboard();
+ }
+
+ _goToMergedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('merged');
+ }
+
+ _goToAbandonedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('abandoned');
+ }
+
+ _goToWatchedChanges() {
+ // The query is hardcoded, and doesn't respect custom menu entries
+ Gerrit.Nav.navigateToSearchQuery('is:watched is:open');
+ }
+
+ _computePluginScreenName({plugin, screen}) {
+ if (!plugin || !screen) return '';
+ return `${plugin}-screen-${screen}`;
+ }
+
+ _logWelcome() {
+ console.group('Runtime Info');
+ console.log('Gerrit UI (PolyGerrit)');
+ console.log(`Gerrit Server Version: ${this._version}`);
+ if (window.VERSION_INFO) {
+ console.log(`UI Version Info: ${window.VERSION_INFO}`);
+ }
+ if (this._feedbackUrl) {
+ console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
+ }
+ console.groupEnd();
+ }
+
+ /**
+ * Intercept RPC log events emitted by REST API interfaces.
+ * Note: the REST API interface cannot use gr-reporting directly because
+ * that would create a cyclic dependency.
+ */
+ _handleRpcLog(e) {
+ this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
+ e.detail.elapsed);
+ }
+
+ _mobileSearchToggle(e) {
+ this.mobileSearch = !this.mobileSearch;
+ }
+
+ getThemeEndpoint() {
+ // For now, we only have dark mode and light mode
+ return window.localStorage.getItem('dark-theme') ?
+ 'app-theme-dark' :
+ 'app-theme-light';
+ }
+}
+
+customElements.define(GrAppElement.is, GrAppElement);
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.js
index 62f2967..3951d9d 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.js
+++ b/polygerrit-ui/app/elements/gr-app-element_html.js
@@ -1,54 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 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.
--->
-<script src="/bower_components/moment/moment.js"></script>
-<script src="../scripts/util.js"></script>
-
-<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/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../styles/shared-styles.html">
-<link rel="import" href="../styles/themes/app-theme.html">
-<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
-<link rel="import" href="./documentation/gr-documentation-search/gr-documentation-search.html">
-<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
-<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
-<link rel="import" href="./change/gr-change-view/gr-change-view.html">
-<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
-<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
-<link rel="import" href="./core/gr-main-header/gr-main-header.html">
-<link rel="import" href="./core/gr-navigation/gr-navigation.html">
-<link rel="import" href="./core/gr-reporting/gr-reporting.html">
-<link rel="import" href="./core/gr-router/gr-router.html">
-<link rel="import" href="./core/gr-smart-search/gr-smart-search.html">
-<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
-<link rel="import" href="./edit/gr-editor-view/gr-editor-view.html">
-<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="./plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
-<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
-<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
-<link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
-<link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
-<link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html">
-<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html">
-<link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-app-element">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
background-color: var(--background-color-tertiary);
@@ -126,56 +94,33 @@
</style>
<gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
<gr-fixed-panel id="header">
- <gr-main-header
- id="mainHeader"
- search-query="{{params.query}}"
- on-mobile-search="_mobileSearchToggle"
- login-url="[[_loginUrl]]"
- >
+ <gr-main-header id="mainHeader" search-query="{{params.query}}" on-mobile-search="_mobileSearchToggle" login-url="[[_loginUrl]]">
</gr-main-header>
</gr-fixed-panel>
<main>
- <gr-smart-search
- id="search"
- search-query="{{params.query}}"
- hidden="[[!mobileSearch]]">
+ <gr-smart-search id="search" search-query="{{params.query}}" hidden="[[!mobileSearch]]">
</gr-smart-search>
<template is="dom-if" if="[[_showChangeListView]]" restamp="true">
- <gr-change-list-view
- params="[[params]]"
- account="[[_account]]"
- view-state="{{_viewState.changeListView}}"></gr-change-list-view>
+ <gr-change-list-view params="[[params]]" account="[[_account]]" view-state="{{_viewState.changeListView}}"></gr-change-list-view>
</template>
<template is="dom-if" if="[[_showDashboardView]]" restamp="true">
- <gr-dashboard-view
- account="[[_account]]"
- params="[[params]]"
- view-state="{{_viewState.dashboardView}}"></gr-dashboard-view>
+ <gr-dashboard-view account="[[_account]]" params="[[params]]" view-state="{{_viewState.dashboardView}}"></gr-dashboard-view>
</template>
<template is="dom-if" if="[[_showChangeView]]" restamp="true">
- <gr-change-view
- params="[[params]]"
- view-state="{{_viewState.changeView}}"
- back-page="[[_lastSearchPage]]"></gr-change-view>
+ <gr-change-view params="[[params]]" view-state="{{_viewState.changeView}}" back-page="[[_lastSearchPage]]"></gr-change-view>
</template>
<template is="dom-if" if="[[_showEditorView]]" restamp="true">
- <gr-editor-view
- params="[[params]]"></gr-editor-view>
+ <gr-editor-view params="[[params]]"></gr-editor-view>
</template>
<template is="dom-if" if="[[_showDiffView]]" restamp="true">
- <gr-diff-view
- params="[[params]]"
- change-view-state="{{_viewState.changeView}}"></gr-diff-view>
+ <gr-diff-view params="[[params]]" change-view-state="{{_viewState.changeView}}"></gr-diff-view>
</template>
<template is="dom-if" if="[[_showSettingsView]]" restamp="true">
- <gr-settings-view
- params="[[params]]"
- on-account-detail-update="_handleAccountDetailUpdate">
+ <gr-settings-view params="[[params]]" on-account-detail-update="_handleAccountDetailUpdate">
</gr-settings-view>
</template>
<template is="dom-if" if="[[_showAdminView]]" restamp="true">
- <gr-admin-view path="[[_path]]"
- params=[[params]]></gr-admin-view>
+ <gr-admin-view path="[[_path]]" params="[[params]]"></gr-admin-view>
</template>
<template is="dom-if" if="[[_showPluginScreen]]" restamp="true">
<gr-endpoint-decorator name="[[_pluginScreenName]]">
@@ -186,8 +131,7 @@
<gr-cla-view></gr-cla-view>
</template>
<template is="dom-if" if="[[_showDocumentationSearch]]" restamp="true">
- <gr-documentation-search
- params="[[params]]">
+ <gr-documentation-search params="[[params]]">
</gr-documentation-search>
</template>
<div id="errorView" class="errorView">
@@ -198,32 +142,23 @@
</main>
<footer r="contentinfo">
<div>
- Powered by <a href="https://www.gerritcodereview.com/" rel="noopener"
- target="_blank">Gerrit Code Review</a>
+ Powered by <a href="https://www.gerritcodereview.com/" rel="noopener" target="_blank">Gerrit Code Review</a>
([[_version]])
<gr-endpoint-decorator name="footer-left"></gr-endpoint-decorator>
</div>
<div>
<template is="dom-if" if="[[_feedbackUrl]]">
- <a class="feedback"
- href$="[[_feedbackUrl]]"
- rel="noopener"
- target="_blank">Report bug</a> |
+ <a class="feedback" href\$="[[_feedbackUrl]]" rel="noopener" target="_blank">Report bug</a> |
</template>
- Press “?” for keyboard shortcuts
+ Press “?” for keyboard shortcuts
<gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator>
</div>
</footer>
- <gr-overlay id="keyboardShortcuts" with-backdrop>
- <gr-keyboard-shortcuts-dialog
- on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
+ <gr-overlay id="keyboardShortcuts" with-backdrop="">
+ <gr-keyboard-shortcuts-dialog on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
</gr-overlay>
- <gr-overlay id="registrationOverlay" with-backdrop>
- <gr-registration-dialog
- id="registrationDialog"
- settings-url="[[_settingsUrl]]"
- on-account-detail-update="_handleAccountDetailUpdate"
- on-close="_handleRegistrationDialogClose">
+ <gr-overlay id="registrationOverlay" with-backdrop="">
+ <gr-registration-dialog id="registrationDialog" settings-url="[[_settingsUrl]]" on-account-detail-update="_handleAccountDetailUpdate" on-close="_handleRegistrationDialogClose">
</gr-registration-dialog>
</gr-overlay>
<gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
@@ -231,12 +166,9 @@
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting"></gr-reporting>
<gr-router id="router"></gr-router>
- <gr-plugin-host id="plugins"
- config="[[_serverConfig]]">
+ <gr-plugin-host id="plugins" config="[[_serverConfig]]">
</gr-plugin-host>
<gr-lib-loader id="libLoader"></gr-lib-loader>
<gr-external-style id="externalStyleForAll" name="app-theme"></gr-external-style>
<gr-external-style id="externalStyleForTheme" name="[[getThemeEndpoint()]]"></gr-external-style>
- </template>
- <script src="gr-app-element.js" crossorigin="anonymous"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
new file mode 100644
index 0000000..1483f7a
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -0,0 +1 @@
+<script src='./gr-app.js' type='module'></script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index da54ac4..ac5a04d 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.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,15 +14,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+/* TODO(taoalpha): Remove once all legacyUndefinedCheck removed. */
+/*
+ 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 './gr-app-init.js';
- /** @extends Polymer.Element */
- class GrApp extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-app'; }
- }
+import './font-roboto-local-loader.js';
+import '../scripts/bundled-polymer.js';
+import 'polymer-resin/standalone/polymer-resin.js';
+import '../behaviors/safe-types-behavior/safe-types-behavior.js';
+import './gr-app-element.js';
+import './change-list/gr-embed-dashboard/gr-embed-dashboard.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-app_html.js';
- customElements.define(GrApp.is, GrApp);
-})();
+security.polymer_resin.install({
+ allowedIdentifierPrefixes: [''],
+ reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
+ safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
+});
+
+/** @extends Polymer.Element */
+class GrApp extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-app'; }
+}
+
+customElements.define(GrApp.is, GrApp);
diff --git a/polygerrit-ui/app/elements/gr-app_html.js b/polygerrit-ui/app/elements/gr-app_html.js
index 2a28bc1..fcf773f 100644
--- a/polygerrit-ui/app/elements/gr-app_html.js
+++ b/polygerrit-ui/app/elements/gr-app_html.js
@@ -1,38 +1,21 @@
-<!--
-@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.
--->
-<script src="gr-app-init.js"></script>
-<script src="./font-roboto-local-loader.js" type="module" />
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="/bower_components/polymer-resin/standalone/polymer-resin.html">
-<!-- TODO(taoalpha): Remove once all legacyUndefinedCheck removed. -->
-<link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
-<script>
- security.polymer_resin.install({
- allowedIdentifierPrefixes: [''],
- reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
- safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
- });
-</script>
-
-<link rel="import" href="./gr-app-element.html">
-<dom-module id="gr-app">
- <template>
+export const htmlTemplate = html`
<gr-app-element id="app-element"></gr-app-element>
- </template>
- <script src="gr-app.js" crossorigin="anonymous"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 47a8e06..447aae4 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_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-app</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-app.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-app.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../test/test-pre-setup.js';
+import '../test/common-test-setup.js';
+import './gr-app.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,74 +40,76 @@
</template>
</test-fixture>
-<script>
- suite('gr-app tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../test/test-pre-setup.js';
+import '../test/common-test-setup.js';
+import './gr-app.js';
+suite('gr-app tests', () => {
+ let sandbox;
+ let element;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-reporting', {
- appStarted: sandbox.stub(),
- });
- stub('gr-account-dropdown', {
- _getTopContent: sinon.stub(),
- });
- stub('gr-router', {
- start: sandbox.stub(),
- });
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve({}); },
- getAccountCapabilities() { return Promise.resolve({}); },
- getConfig() {
- return Promise.resolve({
- plugin: {},
- auth: {
- auth_type: undefined,
- },
- });
- },
- getPreferences() { return Promise.resolve({my: []}); },
- getDiffPreferences() { return Promise.resolve({}); },
- getEditPreferences() { return Promise.resolve({}); },
- getVersion() { return Promise.resolve(42); },
- probePath() { return Promise.resolve(42); },
- });
-
- element = fixture('basic');
- flush(done);
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-reporting', {
+ appStarted: sandbox.stub(),
+ });
+ stub('gr-account-dropdown', {
+ _getTopContent: sinon.stub(),
+ });
+ stub('gr-router', {
+ start: sandbox.stub(),
+ });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve({}); },
+ getAccountCapabilities() { return Promise.resolve({}); },
+ getConfig() {
+ return Promise.resolve({
+ plugin: {},
+ auth: {
+ auth_type: undefined,
+ },
+ });
+ },
+ getPreferences() { return Promise.resolve({my: []}); },
+ getDiffPreferences() { return Promise.resolve({}); },
+ getEditPreferences() { return Promise.resolve({}); },
+ getVersion() { return Promise.resolve(42); },
+ probePath() { return Promise.resolve(42); },
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
+ flush(done);
+ });
- const appElement = () => element.$['app-element'];
+ teardown(() => {
+ sandbox.restore();
+ });
- test('reporting', () => {
- assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
- });
+ const appElement = () => element.$['app-element'];
- test('reporting called before router start', () => {
- const element = appElement();
- const appStartedStub = element.$.reporting.appStarted;
- const routerStartStub = element.$.router.start;
- sinon.assert.callOrder(appStartedStub, routerStartStub);
- });
+ test('reporting', () => {
+ assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
+ });
- test('passes config to gr-plugin-host', () => {
- const config = appElement().$.restAPI.getConfig;
- return config.lastCall.returnValue.then(config => {
- assert.deepEqual(appElement().$.plugins.config, config);
- });
- });
+ test('reporting called before router start', () => {
+ const element = appElement();
+ const appStartedStub = element.$.reporting.appStarted;
+ const routerStartStub = element.$.router.start;
+ sinon.assert.callOrder(appStartedStub, routerStartStub);
+ });
- test('_paramsChanged sets search page', () => {
- appElement()._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}});
- assert.notOk(appElement()._lastSearchPage);
- appElement()._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
- assert.ok(appElement()._lastSearchPage);
+ test('passes config to gr-plugin-host', () => {
+ const config = appElement().$.restAPI.getConfig;
+ return config.lastCall.returnValue.then(config => {
+ assert.deepEqual(appElement().$.plugins.config, config);
});
});
+
+ test('_paramsChanged sets search page', () => {
+ appElement()._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}});
+ assert.notOk(appElement()._lastSearchPage);
+ appElement()._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
+ assert.ok(appElement()._lastSearchPage);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html
deleted file mode 100644
index 756c435..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 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.
--->
-
-<script src="gr-admin-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
index f10f922..21e46b8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -19,54 +19,63 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-api</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="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="gr-admin-api.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="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script>
+<script type="module" src="./gr-admin-api.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-admin-api.js';
+void(0);
+</script>
-<script>
- suite('gr-admin-api tests', async () => {
- await readyToTest();
- let sandbox;
- let adminApi;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-admin-api.js';
+suite('gr-admin-api tests', () => {
+ let sandbox;
+ let adminApi;
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- adminApi = plugin.admin();
- });
-
- teardown(() => {
- adminApi = null;
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(adminApi);
- });
-
- test('addMenuLink', () => {
- adminApi.addMenuLink('text', 'url');
- const links = adminApi.getMenuLinks();
- assert.equal(links.length, 1);
- assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
- });
-
- test('addMenuLinkWithCapability', () => {
- adminApi.addMenuLink('text', 'url', 'capability');
- const links = adminApi.getMenuLinks();
- assert.equal(links.length, 1);
- assert.deepEqual(links[0],
- {text: 'text', url: 'url', capability: 'capability'});
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ adminApi = plugin.admin();
});
+
+ teardown(() => {
+ adminApi = null;
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(adminApi);
+ });
+
+ test('addMenuLink', () => {
+ adminApi.addMenuLink('text', 'url');
+ const links = adminApi.getMenuLinks();
+ assert.equal(links.length, 1);
+ assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null});
+ });
+
+ test('addMenuLinkWithCapability', () => {
+ adminApi.addMenuLink('text', 'url', 'capability');
+ const links = adminApi.getMenuLinks();
+ assert.equal(links.length, 1);
+ assert.deepEqual(links[0],
+ {text: 'text', url: 'url', capability: 'capability'});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
deleted file mode 100644
index ece8677..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-attribute-helper">
- <script src="gr-attribute-helper.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
index 0cff8e9..09620ef 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -14,6 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-attribute-helper">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
index 2dfd036..cfb51f0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -19,28 +19,37 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-attribute-helper</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-attribute-helper.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-attribute-helper.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-attribute-helper.js';
+void(0);
+</script>
<dom-element id="some-element">
- <script>
- Polymer({
- is: 'some-element',
- properties: {
- fooBar: {
- type: Object,
- notify: true,
- },
- },
- });
- </script>
+ <script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-attribute-helper.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({
+ is: 'some-element',
+ properties: {
+ fooBar: {
+ type: Object,
+ notify: true,
+ },
+ },
+});
+</script>
</dom-element>
@@ -50,54 +59,56 @@
</template>
</test-fixture>
-<script>
- suite('gr-attribute-helper tests', async () => {
- await readyToTest();
- let element;
- let instance;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-attribute-helper.js';
+suite('gr-attribute-helper tests', () => {
+ let element;
+ let instance;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- instance = new GrAttributeHelper(element);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('resolved on value change from undefined', () => {
- const promise = instance.get('fooBar').then(value => {
- assert.equal(value, 'foo! bar!');
- });
- element.fooBar = 'foo! bar!';
- return promise;
- });
-
- test('resolves to current attribute value', () => {
- element.fooBar = 'foo-foo-bar';
- const promise = instance.get('fooBar').then(value => {
- assert.equal(value, 'foo-foo-bar');
- });
- element.fooBar = 'no bar';
- return promise;
- });
-
- test('bind', () => {
- const stub = sandbox.stub();
- element.fooBar = 'bar foo';
- const unbind = instance.bind('fooBar', stub);
- element.fooBar = 'partridge in a foo tree';
- element.fooBar = 'five gold bars';
- assert.equal(stub.callCount, 3);
- assert.deepEqual(stub.args[0], ['bar foo']);
- assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
- assert.deepEqual(stub.args[2], ['five gold bars']);
- stub.reset();
- unbind();
- instance.fooBar = 'ladies dancing';
- assert.isFalse(stub.called);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ instance = new GrAttributeHelper(element);
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('resolved on value change from undefined', () => {
+ const promise = instance.get('fooBar').then(value => {
+ assert.equal(value, 'foo! bar!');
+ });
+ element.fooBar = 'foo! bar!';
+ return promise;
+ });
+
+ test('resolves to current attribute value', () => {
+ element.fooBar = 'foo-foo-bar';
+ const promise = instance.get('fooBar').then(value => {
+ assert.equal(value, 'foo-foo-bar');
+ });
+ element.fooBar = 'no bar';
+ return promise;
+ });
+
+ test('bind', () => {
+ const stub = sandbox.stub();
+ element.fooBar = 'bar foo';
+ const unbind = instance.bind('fooBar', stub);
+ element.fooBar = 'partridge in a foo tree';
+ element.fooBar = 'five gold bars';
+ assert.equal(stub.callCount, 3);
+ assert.deepEqual(stub.args[0], ['bar foo']);
+ assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
+ assert.deepEqual(stub.args[2], ['five gold bars']);
+ stub.reset();
+ unbind();
+ instance.fooBar = 'ladies dancing';
+ assert.isFalse(stub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
deleted file mode 100644
index dd532e1..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@license
-Copyright (C) 2018 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-change-metadata-api">
- <script src="gr-change-metadata-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
index 80abf23..daf48f0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -14,6 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
deleted file mode 100644
index 8b9000f..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-dom-hooks">
- <script src="gr-dom-hooks.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index fb9adb5..9497493 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-dom-hooks">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
index 524b1b9..8e23b0d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-dom-hooks</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-dom-hooks.html"/>
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.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-dom-hooks.js"></script>
+<script type="module" src="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-dom-hooks.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,131 +42,134 @@
</template>
</test-fixture>
-<script>
- suite('gr-dom-hooks tests', async () => {
- await readyToTest();
- const PUBLIC_METHODS =[
- 'onAttached',
- 'onDetached',
- 'getLastAttached',
- 'getAllAttached',
- 'getModuleName',
- ];
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-dom-hooks.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+suite('gr-dom-hooks tests', () => {
+ const PUBLIC_METHODS =[
+ 'onAttached',
+ 'onDetached',
+ 'getLastAttached',
+ 'getAllAttached',
+ 'getModuleName',
+ ];
- let instance;
- let sandbox;
- let hook;
- let hookInternal;
+ let instance;
+ let sandbox;
+ let hook;
+ let hookInternal;
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrDomHooksManager(plugin);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ instance = new GrDomHooksManager(plugin);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('placeholder', () => {
+ setup(()=>{
+ sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
+ hookInternal = instance.getDomHook('foo-bar');
+ hook = hookInternal.getPublicAPI();
});
- teardown(() => {
- sandbox.restore();
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
});
- suite('placeholder', () => {
- setup(()=>{
- sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
- hookInternal = instance.getDomHook('foo-bar');
- hook = hookInternal.getPublicAPI();
- });
-
- test('public hook API has only public methods', () => {
- assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
- });
-
- test('registers placeholder class', () => {
- assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
- 'testplugin-autogenerated-foo-bar'));
- });
-
- test('getModuleName()', () => {
- const hookName = Object.keys(instance._hooks).pop();
- assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
- assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar');
- });
+ test('registers placeholder class', () => {
+ assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
+ 'testplugin-autogenerated-foo-bar'));
});
- suite('custom element', () => {
- setup(() => {
- hookInternal = instance.getDomHook('foo-bar', 'my-el');
- hook = hookInternal.getPublicAPI();
- });
-
- test('public hook API has only public methods', () => {
- assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
- });
-
- test('getModuleName()', () => {
- const hookName = Object.keys(instance._hooks).pop();
- assert.equal(hookName, 'foo-bar my-el');
- assert.equal(hook.getModuleName(), 'my-el');
- });
-
- test('onAttached', () => {
- const onAttachedSpy = sandbox.spy();
- hook.onAttached(onAttachedSpy);
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- hookInternal.handleInstanceAttached(el1);
- hookInternal.handleInstanceAttached(el2);
- assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
- assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
- });
-
- test('onDetached', () => {
- const onDetachedSpy = sandbox.spy();
- hook.onDetached(onDetachedSpy);
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- hookInternal.handleInstanceDetached(el1);
- assert.isTrue(onDetachedSpy.firstCall.calledWithExactly(el1));
- hookInternal.handleInstanceDetached(el2);
- assert.isTrue(onDetachedSpy.secondCall.calledWithExactly(el2));
- });
-
- test('getAllAttached', () => {
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- el1.textContent = 'one';
- el2.textContent = 'two';
- hookInternal.handleInstanceAttached(el1);
- hookInternal.handleInstanceAttached(el2);
- assert.deepEqual([el1, el2], hook.getAllAttached());
- hookInternal.handleInstanceDetached(el1);
- assert.deepEqual([el2], hook.getAllAttached());
- });
-
- test('getLastAttached', () => {
- const beforeAttachedPromise = hook.getLastAttached().then(
- el => assert.strictEqual(el1, el));
- const [el1, el2] = [
- document.createElement(hook.getModuleName()),
- document.createElement(hook.getModuleName()),
- ];
- el1.textContent = 'one';
- el2.textContent = 'two';
- hookInternal.handleInstanceAttached(el1);
- hookInternal.handleInstanceAttached(el2);
- const afterAttachedPromise = hook.getLastAttached().then(
- el => assert.strictEqual(el2, el));
- return Promise.all([
- beforeAttachedPromise,
- afterAttachedPromise,
- ]);
- });
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
+ assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar');
});
});
+
+ suite('custom element', () => {
+ setup(() => {
+ hookInternal = instance.getDomHook('foo-bar', 'my-el');
+ hook = hookInternal.getPublicAPI();
+ });
+
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
+ });
+
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'foo-bar my-el');
+ assert.equal(hook.getModuleName(), 'my-el');
+ });
+
+ test('onAttached', () => {
+ const onAttachedSpy = sandbox.spy();
+ hook.onAttached(onAttachedSpy);
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
+ assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
+ });
+
+ test('onDetached', () => {
+ const onDetachedSpy = sandbox.spy();
+ hook.onDetached(onDetachedSpy);
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ hookInternal.handleInstanceDetached(el1);
+ assert.isTrue(onDetachedSpy.firstCall.calledWithExactly(el1));
+ hookInternal.handleInstanceDetached(el2);
+ assert.isTrue(onDetachedSpy.secondCall.calledWithExactly(el2));
+ });
+
+ test('getAllAttached', () => {
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.deepEqual([el1, el2], hook.getAllAttached());
+ hookInternal.handleInstanceDetached(el1);
+ assert.deepEqual([el2], hook.getAllAttached());
+ });
+
+ test('getLastAttached', () => {
+ const beforeAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el1, el));
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ const afterAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el2, el));
+ return Promise.all([
+ beforeAttachedPromise,
+ afterAttachedPromise,
+ ]);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 1c10642..e1624b9 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -14,155 +14,163 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const INIT_PROPERTIES_TIMEOUT_MS = 10000;
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {importHref} from '../../../scripts/import-href.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-endpoint-decorator_html.js';
- /** @extends Polymer.Element */
- class GrEndpointDecorator extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-endpoint-decorator'; }
+const INIT_PROPERTIES_TIMEOUT_MS = 10000;
- static get properties() {
- return {
- name: String,
- /** @type {!Map} */
- _domHooks: {
- type: Map,
- value() { return new Map(); },
- },
- /**
- * This map prevents importing the same endpoint twice.
- * Without caching, if a plugin is loaded after the loaded plugins
- * callback fires, it will be imported twice and appear twice on the page.
- *
- * @type {!Map}
- */
- _initializedPlugins: {
- type: Map,
- value() { return new Map(); },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrEndpointDecorator extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @override */
- detached() {
- super.detached();
- for (const [el, domHook] of this._domHooks) {
- domHook.handleInstanceDetached(el);
- }
- }
+ static get is() { return 'gr-endpoint-decorator'; }
- /**
- * @suppress {checkTypes}
- */
- _import(url) {
- return new Promise((resolve, reject) => {
- Polymer.importHref(url, resolve, reject);
- });
- }
+ static get properties() {
+ return {
+ name: String,
+ /** @type {!Map} */
+ _domHooks: {
+ type: Map,
+ value() { return new Map(); },
+ },
+ /**
+ * This map prevents importing the same endpoint twice.
+ * Without caching, if a plugin is loaded after the loaded plugins
+ * callback fires, it will be imported twice and appear twice on the page.
+ *
+ * @type {!Map}
+ */
+ _initializedPlugins: {
+ type: Map,
+ value() { return new Map(); },
+ },
+ };
+ }
- _initDecoration(name, plugin) {
- const el = document.createElement(name);
- return this._initProperties(el, plugin,
- this.getContentChildren().find(
- el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
- .then(el => this._appendChild(el));
- }
-
- _initReplacement(name, plugin) {
- this.getContentChildNodes()
- .filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM')
- .forEach(node => node.remove());
- const el = document.createElement(name);
- return this._initProperties(el, plugin).then(
- el => this._appendChild(el));
- }
-
- _getEndpointParams() {
- return Array.from(
- Polymer.dom(this).querySelectorAll('gr-endpoint-param'));
- }
-
- /**
- * @param {!Element} el
- * @param {!Object} plugin
- * @param {!Element=} opt_content
- * @return {!Promise<Element>}
- */
- _initProperties(el, plugin, opt_content) {
- el.plugin = plugin;
- if (opt_content) {
- el.content = opt_content;
- }
- const expectProperties = this._getEndpointParams().map(paramEl => {
- const helper = plugin.attributeHelper(paramEl);
- const paramName = paramEl.getAttribute('name');
- return helper.get('value').then(
- value => helper.bind('value',
- value => plugin.attributeHelper(el).set(paramName, value))
- );
- });
- let timeoutId;
- const timeout = new Promise(
- resolve => timeoutId = setTimeout(() => {
- console.warn(
- 'Timeout waiting for endpoint properties initialization: ' +
- `plugin ${plugin.getPluginName()}, endpoint ${this.name}`);
- }, INIT_PROPERTIES_TIMEOUT_MS));
- return Promise.race([timeout, Promise.all(expectProperties)])
- .then(() => {
- clearTimeout(timeoutId);
- return el;
- });
- }
-
- _appendChild(el) {
- return Polymer.dom(this.root).appendChild(el);
- }
-
- _initModule({moduleName, plugin, type, domHook}) {
- const name = plugin.getPluginName() + '.' + moduleName;
- if (this._initializedPlugins.get(name)) {
- return;
- }
- let initPromise;
- switch (type) {
- case 'decorate':
- initPromise = this._initDecoration(moduleName, plugin);
- break;
- case 'replace':
- initPromise = this._initReplacement(moduleName, plugin);
- break;
- }
- if (!initPromise) {
- console.warn('Unable to initialize module ' + name);
- }
- this._initializedPlugins.set(name, true);
- initPromise.then(el => {
- domHook.handleInstanceAttached(el);
- this._domHooks.set(el, domHook);
- });
- }
-
- /** @override */
- ready() {
- super.ready();
- Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
- Gerrit.awaitPluginsLoaded()
- .then(() => Promise.all(
- Gerrit._endpoints.getPlugins(this.name).map(
- pluginUrl => this._import(pluginUrl)))
- )
- .then(() =>
- Gerrit._endpoints
- .getDetails(this.name)
- .forEach(this._initModule, this)
- );
+ /** @override */
+ detached() {
+ super.detached();
+ for (const [el, domHook] of this._domHooks) {
+ domHook.handleInstanceDetached(el);
}
}
- customElements.define(GrEndpointDecorator.is, GrEndpointDecorator);
-})();
+ /**
+ * @suppress {checkTypes}
+ */
+ _import(url) {
+ return new Promise((resolve, reject) => {
+ importHref(url, resolve, reject);
+ });
+ }
+
+ _initDecoration(name, plugin) {
+ const el = document.createElement(name);
+ return this._initProperties(el, plugin,
+ this.getContentChildren().find(
+ el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
+ .then(el => this._appendChild(el));
+ }
+
+ _initReplacement(name, plugin) {
+ this.getContentChildNodes()
+ .filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM')
+ .forEach(node => node.remove());
+ const el = document.createElement(name);
+ return this._initProperties(el, plugin).then(
+ el => this._appendChild(el));
+ }
+
+ _getEndpointParams() {
+ return Array.from(
+ dom(this).querySelectorAll('gr-endpoint-param'));
+ }
+
+ /**
+ * @param {!Element} el
+ * @param {!Object} plugin
+ * @param {!Element=} opt_content
+ * @return {!Promise<Element>}
+ */
+ _initProperties(el, plugin, opt_content) {
+ el.plugin = plugin;
+ if (opt_content) {
+ el.content = opt_content;
+ }
+ const expectProperties = this._getEndpointParams().map(paramEl => {
+ const helper = plugin.attributeHelper(paramEl);
+ const paramName = paramEl.getAttribute('name');
+ return helper.get('value').then(
+ value => helper.bind('value',
+ value => plugin.attributeHelper(el).set(paramName, value))
+ );
+ });
+ let timeoutId;
+ const timeout = new Promise(
+ resolve => timeoutId = setTimeout(() => {
+ console.warn(
+ 'Timeout waiting for endpoint properties initialization: ' +
+ `plugin ${plugin.getPluginName()}, endpoint ${this.name}`);
+ }, INIT_PROPERTIES_TIMEOUT_MS));
+ return Promise.race([timeout, Promise.all(expectProperties)])
+ .then(() => {
+ clearTimeout(timeoutId);
+ return el;
+ });
+ }
+
+ _appendChild(el) {
+ return dom(this.root).appendChild(el);
+ }
+
+ _initModule({moduleName, plugin, type, domHook}) {
+ const name = plugin.getPluginName() + '.' + moduleName;
+ if (this._initializedPlugins.get(name)) {
+ return;
+ }
+ let initPromise;
+ switch (type) {
+ case 'decorate':
+ initPromise = this._initDecoration(moduleName, plugin);
+ break;
+ case 'replace':
+ initPromise = this._initReplacement(moduleName, plugin);
+ break;
+ }
+ if (!initPromise) {
+ console.warn('Unable to initialize module ' + name);
+ }
+ this._initializedPlugins.set(name, true);
+ initPromise.then(el => {
+ domHook.handleInstanceAttached(el);
+ this._domHooks.set(el, domHook);
+ });
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
+ Gerrit.awaitPluginsLoaded()
+ .then(() => Promise.all(
+ Gerrit._endpoints.getPlugins(this.name).map(
+ pluginUrl => this._import(pluginUrl)))
+ )
+ .then(() =>
+ Gerrit._endpoints
+ .getDetails(this.name)
+ .forEach(this._initModule, this)
+ );
+ }
+}
+
+customElements.define(GrEndpointDecorator.is, GrEndpointDecorator);
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
index 1b27c0a..1644c07 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
@@ -1,26 +1,21 @@
-<!--
-@license
-Copyright (C) 2017 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-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-endpoint-decorator">
- <template>
+export const htmlTemplate = html`
<slot></slot>
- </template>
- <script src="gr-endpoint-decorator.js"></script>
-</dom-module>
\ No newline at end of file
+`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index e0d91fa..fcac174 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-endpoint-decorator</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-endpoint-decorator.html">
-<link rel="import" href="../gr-endpoint-param/gr-endpoint-param.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-endpoint-decorator.js"></script>
+<script type="module" src="../gr-endpoint-param/gr-endpoint-param.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-endpoint-decorator.js';
+import '../gr-endpoint-param/gr-endpoint-param.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -46,149 +52,153 @@
</template>
</test-fixture>
-<script>
- suite('gr-endpoint-decorator', async () => {
- await readyToTest();
- let container;
- let sandbox;
- let plugin;
- let decorationHook;
- let replacementHook;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-endpoint-decorator.js';
+import '../gr-endpoint-param/gr-endpoint-param.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-endpoint-decorator', () => {
+ let container;
+ let sandbox;
+ let plugin;
+ let decorationHook;
+ let replacementHook;
- setup(done => {
- sandbox = sinon.sandbox.create();
- stub('gr-endpoint-decorator', {
- _import: sandbox.stub().returns(Promise.resolve()),
- });
- Gerrit._testOnly_resetPlugins();
- container = fixture('basic');
- Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html');
- // Decoration
- decorationHook = plugin.registerCustomComponent('first', 'some-module');
- // Replacement
- replacementHook = plugin.registerCustomComponent(
- 'second', 'other-module', {replace: true});
- // Mimic all plugins loaded.
- Gerrit._loadPlugins([]);
- flush(done);
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
});
+ Gerrit._testOnly_resetPlugins();
+ container = fixture('basic');
+ Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html');
+ // Decoration
+ decorationHook = plugin.registerCustomComponent('first', 'some-module');
+ // Replacement
+ replacementHook = plugin.registerCustomComponent(
+ 'second', 'other-module', {replace: true});
+ // Mimic all plugins loaded.
+ Gerrit._loadPlugins([]);
+ flush(done);
+ });
- teardown(() => {
- sandbox.restore();
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('imports plugin-provided modules into endpoints', () => {
+ const endpoints =
+ Array.from(container.querySelectorAll('gr-endpoint-decorator'));
+ assert.equal(endpoints.length, 3);
+ endpoints.forEach(element => {
+ assert.isTrue(
+ element._import.calledWith(new URL('http://some/plugin/url.html')));
});
+ });
- test('imports plugin-provided modules into endpoints', () => {
- const endpoints =
- Array.from(container.querySelectorAll('gr-endpoint-decorator'));
- assert.equal(endpoints.length, 3);
- endpoints.forEach(element => {
- assert.isTrue(
- element._import.calledWith(new URL('http://some/plugin/url.html')));
- });
- });
+ test('decoration', () => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="first"]');
+ const modules = Array.from(dom(element.root).children).filter(
+ element => element.nodeName === 'SOME-MODULE');
+ assert.equal(modules.length, 1);
+ const [module] = modules;
+ assert.isOk(module);
+ assert.equal(module['someparam'], 'barbar');
+ return decorationHook.getLastAttached().then(element => {
+ assert.strictEqual(element, module);
+ })
+ .then(() => {
+ element.remove();
+ assert.equal(decorationHook.getAllAttached().length, 0);
+ });
+ });
- test('decoration', () => {
+ test('replacement', () => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="second"]');
+ const module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'OTHER-MODULE');
+ assert.isOk(module);
+ assert.equal(module['someparam'], 'foofoo');
+ return replacementHook.getLastAttached()
+ .then(element => {
+ assert.strictEqual(element, module);
+ })
+ .then(() => {
+ element.remove();
+ assert.equal(replacementHook.getAllAttached().length, 0);
+ });
+ });
+
+ test('late registration', done => {
+ plugin.registerCustomComponent('banana', 'noob-noob');
+ flush(() => {
const element =
- container.querySelector('gr-endpoint-decorator[name="first"]');
- const modules = Array.from(Polymer.dom(element.root).children).filter(
- element => element.nodeName === 'SOME-MODULE');
- assert.equal(modules.length, 1);
- const [module] = modules;
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'NOOB-NOOB');
assert.isOk(module);
- assert.equal(module['someparam'], 'barbar');
- return decorationHook.getLastAttached().then(element => {
- assert.strictEqual(element, module);
- })
- .then(() => {
- element.remove();
- assert.equal(decorationHook.getAllAttached().length, 0);
- });
+ done();
});
+ });
- test('replacement', () => {
+ test('two modules', done => {
+ plugin.registerCustomComponent('banana', 'mod-one');
+ plugin.registerCustomComponent('banana', 'mod-two');
+ flush(() => {
const element =
- container.querySelector('gr-endpoint-decorator[name="second"]');
- const module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'OTHER-MODULE');
- assert.isOk(module);
- assert.equal(module['someparam'], 'foofoo');
- return replacementHook.getLastAttached()
- .then(element => {
- assert.strictEqual(element, module);
- })
- .then(() => {
- element.remove();
- assert.equal(replacementHook.getAllAttached().length, 0);
- });
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const module1 = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'MOD-ONE');
+ assert.isOk(module1);
+ const module2 = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'MOD-TWO');
+ assert.isOk(module2);
+ done();
});
+ });
- test('late registration', done => {
- plugin.registerCustomComponent('banana', 'noob-noob');
+ test('late param setup', done => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const param = dom(element).querySelector('gr-endpoint-param');
+ param['value'] = undefined;
+ plugin.registerCustomComponent('banana', 'noob-noob');
+ flush(() => {
+ let module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'NOOB-NOOB');
+ // Module waits for param to be defined.
+ assert.isNotOk(module);
+ const value = {abc: 'def'};
+ param.value = value;
flush(() => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const module = Array.from(Polymer.dom(element.root).children).find(
+ module = Array.from(dom(element.root).children).find(
element => element.nodeName === 'NOOB-NOOB');
assert.isOk(module);
- done();
- });
- });
-
- test('two modules', done => {
- plugin.registerCustomComponent('banana', 'mod-one');
- plugin.registerCustomComponent('banana', 'mod-two');
- flush(() => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const module1 = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'MOD-ONE');
- assert.isOk(module1);
- const module2 = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'MOD-TWO');
- assert.isOk(module2);
- done();
- });
- });
-
- test('late param setup', done => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const param = Polymer.dom(element).querySelector('gr-endpoint-param');
- param['value'] = undefined;
- plugin.registerCustomComponent('banana', 'noob-noob');
- flush(() => {
- let module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'NOOB-NOOB');
- // Module waits for param to be defined.
- assert.isNotOk(module);
- const value = {abc: 'def'};
- param.value = value;
- flush(() => {
- module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'NOOB-NOOB');
- assert.isOk(module);
- assert.strictEqual(module['someParam'], value);
- done();
- });
- });
- });
-
- test('param is bound', done => {
- const element =
- container.querySelector('gr-endpoint-decorator[name="banana"]');
- const param = Polymer.dom(element).querySelector('gr-endpoint-param');
- const value1 = {abc: 'def'};
- const value2 = {def: 'abc'};
- param.value = value1;
- plugin.registerCustomComponent('banana', 'noob-noob');
- flush(() => {
- const module = Array.from(Polymer.dom(element.root).children).find(
- element => element.nodeName === 'NOOB-NOOB');
- assert.strictEqual(module['someParam'], value1);
- param.value = value2;
- assert.strictEqual(module['someParam'], value2);
+ assert.strictEqual(module['someParam'], value);
done();
});
});
});
+
+ test('param is bound', done => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="banana"]');
+ const param = dom(element).querySelector('gr-endpoint-param');
+ const value1 = {abc: 'def'};
+ const value2 = {def: 'abc'};
+ param.value = value1;
+ plugin.registerCustomComponent('banana', 'noob-noob');
+ flush(() => {
+ const module = Array.from(dom(element.root).children).find(
+ element => element.nodeName === 'NOOB-NOOB');
+ assert.strictEqual(module['someParam'], value1);
+ param.value = value2;
+ assert.strictEqual(module['someParam'], value2);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
deleted file mode 100644
index 6a5b558..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-endpoint-param">
- <script src="gr-endpoint-param.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index bcad7f9..9574391 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -14,41 +14,43 @@
* 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 GrEndpointParam extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-endpoint-param'; }
+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';
- static get properties() {
- return {
- name: String,
- value: {
- type: Object,
- notify: true,
- observer: '_valueChanged',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrEndpointParam extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-endpoint-param'; }
- _valueChanged(newValue, oldValue) {
- /* In polymer 2 the following change was made:
- "Property change notifications (property-changed events) aren't fired when
- the value changes as a result of a binding from the host"
- (see https://polymer-library.polymer-project.org/2.0/docs/about_20).
- To workaround this problem, we fire the event from the observer.
- In some cases this fire the event twice, but our code is
- ready for it.
- */
- const detail = {
- value: newValue,
- };
- this.dispatchEvent(new CustomEvent('value-changed', {detail}));
- }
+ static get properties() {
+ return {
+ name: String,
+ value: {
+ type: Object,
+ notify: true,
+ observer: '_valueChanged',
+ },
+ };
}
- customElements.define(GrEndpointParam.is, GrEndpointParam);
-})();
+ _valueChanged(newValue, oldValue) {
+ /* In polymer 2 the following change was made:
+ "Property change notifications (property-changed events) aren't fired when
+ the value changes as a result of a binding from the host"
+ (see https://polymer-library.polymer-project.org/2.0/docs/about_20).
+ To workaround this problem, we fire the event from the observer.
+ In some cases this fire the event twice, but our code is
+ ready for it.
+ */
+ const detail = {
+ value: newValue,
+ };
+ this.dispatchEvent(new CustomEvent('value-changed', {detail}));
+ }
+}
+
+customElements.define(GrEndpointParam.is, GrEndpointParam);
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
deleted file mode 100644
index 15db861..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-
-<dom-module id="gr-event-helper">
- <script src="gr-event-helper.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
index 481a467..66d42d3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-event-helper">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
index bb08cfb..8eebe33 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -19,33 +19,42 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-event-helper</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-event-helper.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-event-helper.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-event-helper.js';
+void(0);
+</script>
<dom-element id="some-element">
- <script>
- Polymer({
- is: 'some-element',
+ <script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-event-helper.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({
+ is: 'some-element',
- properties: {
- fooBar: {
- type: Object,
- notify: true,
- },
- },
+ properties: {
+ fooBar: {
+ type: Object,
+ notify: true,
+ },
+ },
- behaviors: [
- Gerrit.FireBehavior,
- ],
- });
- </script>
+ behaviors: [
+ Gerrit.FireBehavior,
+ ],
+});
+</script>
</dom-element>
@@ -55,85 +64,88 @@
</template>
</test-fixture>
-<script>
- suite('gr-event-helper tests', async () => {
- await readyToTest();
- let element;
- let instance;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-event-helper.js';
+import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+suite('gr-event-helper tests', () => {
+ let element;
+ let instance;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- instance = new GrEventHelper(element);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('onTap()', done => {
- instance.onTap(() => {
- done();
- });
- MockInteractions.tap(element);
- });
-
- test('onTap() cancel', () => {
- const tapStub = sandbox.stub();
- Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub);
- instance.onTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('onClick() cancel', () => {
- const tapStub = sandbox.stub();
- element.parentElement.addEventListener('click', tapStub);
- instance.onTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('captureTap()', done => {
- instance.captureTap(() => {
- done();
- });
- MockInteractions.tap(element);
- });
-
- test('captureClick()', done => {
- instance.captureClick(() => {
- done();
- });
- MockInteractions.tap(element);
- });
-
- test('captureTap() cancels tap()', () => {
- const tapStub = sandbox.stub();
- Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub);
- instance.captureTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('captureClick() cancels click()', () => {
- const tapStub = sandbox.stub();
- element.addEventListener('click', tapStub);
- instance.captureTap(() => false);
- MockInteractions.tap(element);
- flushAsynchronousOperations();
- assert.isFalse(tapStub.called);
- });
-
- test('on()', done => {
- instance.on('foo', () => {
- done();
- });
- element.fire('foo');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ instance = new GrEventHelper(element);
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('onTap()', done => {
+ instance.onTap(() => {
+ done();
+ });
+ MockInteractions.tap(element);
+ });
+
+ test('onTap() cancel', () => {
+ const tapStub = sandbox.stub();
+ addListener(element.parentElement, 'tap', tapStub);
+ instance.onTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('onClick() cancel', () => {
+ const tapStub = sandbox.stub();
+ element.parentElement.addEventListener('click', tapStub);
+ instance.onTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('captureTap()', done => {
+ instance.captureTap(() => {
+ done();
+ });
+ MockInteractions.tap(element);
+ });
+
+ test('captureClick()', done => {
+ instance.captureClick(() => {
+ done();
+ });
+ MockInteractions.tap(element);
+ });
+
+ test('captureTap() cancels tap()', () => {
+ const tapStub = sandbox.stub();
+ addListener(element.parentElement, 'tap', tapStub);
+ instance.captureTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('captureClick() cancels click()', () => {
+ const tapStub = sandbox.stub();
+ element.addEventListener('click', tapStub);
+ instance.captureTap(() => false);
+ MockInteractions.tap(element);
+ flushAsynchronousOperations();
+ assert.isFalse(tapStub.called);
+ });
+
+ test('on()', done => {
+ instance.on('foo', () => {
+ done();
+ });
+ element.fire('foo');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 7e239f9..ba4dd58 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -14,84 +14,92 @@
* 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 GrExternalStyle extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-external-style'; }
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {importHref} from '../../../scripts/import-href.js';
+import {updateStyles} from '@polymer/polymer/lib/mixins/element-mixin.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-external-style_html.js';
- static get properties() {
- return {
- name: String,
- _urlsImported: {
- type: Array,
- value() { return []; },
- },
- _stylesApplied: {
- type: Array,
- value() { return []; },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrExternalStyle extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _importHref(url, resolve, reject) {
- // It is impossible to mock es6-module imported function.
- // The _importHref function is mocked in test.
- Polymer.importHref(url, resolve, reject);
- }
+ static get is() { return 'gr-external-style'; }
- /**
- * @suppress {checkTypes}
- */
- _import(url) {
- if (this._urlsImported.includes(url)) { return Promise.resolve(); }
- this._urlsImported.push(url);
- return new Promise((resolve, reject) => {
- this._importHref(url, resolve, reject);
- });
- }
-
- _applyStyle(name) {
- if (this._stylesApplied.includes(name)) { return; }
- this._stylesApplied.push(name);
-
- const s = document.createElement('style');
- s.setAttribute('include', name);
- const cs = document.createElement('custom-style');
- cs.appendChild(s);
- // When using Shadow DOM <custom-style> must be added to the <body>.
- // Within <gr-external-style> itself the styles would have no effect.
- const topEl = document.getElementsByTagName('body')[0];
- topEl.insertBefore(cs, topEl.firstChild);
- Polymer.updateStyles();
- }
-
- _importAndApply() {
- Promise.all(Gerrit._endpoints.getPlugins(this.name).map(
- pluginUrl => this._import(pluginUrl))
- ).then(() => {
- const moduleNames = Gerrit._endpoints.getModules(this.name);
- for (const name of moduleNames) {
- this._applyStyle(name);
- }
- });
- }
-
- /** @override */
- attached() {
- super.attached();
- this._importAndApply();
- }
-
- /** @override */
- ready() {
- super.ready();
- Gerrit.awaitPluginsLoaded().then(() => this._importAndApply());
- }
+ static get properties() {
+ return {
+ name: String,
+ _urlsImported: {
+ type: Array,
+ value() { return []; },
+ },
+ _stylesApplied: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrExternalStyle.is, GrExternalStyle);
-})();
+ _importHref(url, resolve, reject) {
+ // It is impossible to mock es6-module imported function.
+ // The _importHref function is mocked in test.
+ importHref(url, resolve, reject);
+ }
+
+ /**
+ * @suppress {checkTypes}
+ */
+ _import(url) {
+ if (this._urlsImported.includes(url)) { return Promise.resolve(); }
+ this._urlsImported.push(url);
+ return new Promise((resolve, reject) => {
+ this._importHref(url, resolve, reject);
+ });
+ }
+
+ _applyStyle(name) {
+ if (this._stylesApplied.includes(name)) { return; }
+ this._stylesApplied.push(name);
+
+ const s = document.createElement('style');
+ s.setAttribute('include', name);
+ const cs = document.createElement('custom-style');
+ cs.appendChild(s);
+ // When using Shadow DOM <custom-style> must be added to the <body>.
+ // Within <gr-external-style> itself the styles would have no effect.
+ const topEl = document.getElementsByTagName('body')[0];
+ topEl.insertBefore(cs, topEl.firstChild);
+ updateStyles();
+ }
+
+ _importAndApply() {
+ Promise.all(Gerrit._endpoints.getPlugins(this.name).map(
+ pluginUrl => this._import(pluginUrl))
+ ).then(() => {
+ const moduleNames = Gerrit._endpoints.getModules(this.name);
+ for (const name of moduleNames) {
+ this._applyStyle(name);
+ }
+ });
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this._importAndApply();
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ Gerrit.awaitPluginsLoaded().then(() => this._importAndApply());
+ }
+}
+
+customElements.define(GrExternalStyle.is, GrExternalStyle);
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
index 6a55349..1644c07 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
@@ -1,26 +1,21 @@
-<!--
-@license
-Copyright (C) 2017 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-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-external-style">
- <template>
+export const htmlTemplate = html`
<slot></slot>
- </template>
- <script src="gr-external-style.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
index 808de43..7b76f63 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-external-style</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-external-style.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-external-style.js"></script>
<test-fixture id="basic">
<template>
@@ -33,98 +33,100 @@
</template>
</test-fixture>
-<script>
- suite('gr-external-style integration tests', async () => {
- await readyToTest();
- const TEST_URL = 'http://some/plugin/url.html';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-external-style.js';
+suite('gr-external-style integration tests', () => {
+ const TEST_URL = 'http://some/plugin/url.html';
- let sandbox;
- let element;
- let plugin;
- let importHrefStub;
+ let sandbox;
+ let element;
+ let plugin;
+ let importHrefStub;
- const installPlugin = () => {
- if (plugin) { return; }
- Gerrit.install(p => {
- plugin = p;
- }, '0.1', TEST_URL);
- };
+ const installPlugin = () => {
+ if (plugin) { return; }
+ Gerrit.install(p => {
+ plugin = p;
+ }, '0.1', TEST_URL);
+ };
- const createElement = () => {
- element = fixture('basic');
- sandbox.spy(element, '_applyStyle');
- };
+ const createElement = () => {
+ element = fixture('basic');
+ sandbox.spy(element, '_applyStyle');
+ };
- /**
- * Installs the plugin, creates the element, registers style module.
- */
- const lateRegister = () => {
- installPlugin();
- createElement();
- plugin.registerStyleModule('foo', 'some-module');
- };
+ /**
+ * Installs the plugin, creates the element, registers style module.
+ */
+ const lateRegister = () => {
+ installPlugin();
+ createElement();
+ plugin.registerStyleModule('foo', 'some-module');
+ };
- /**
- * Installs the plugin, registers style module, creates the element.
- */
- const earlyRegister = () => {
- installPlugin();
- plugin.registerStyleModule('foo', 'some-module');
- createElement();
- };
+ /**
+ * Installs the plugin, registers style module, creates the element.
+ */
+ const earlyRegister = () => {
+ installPlugin();
+ plugin.registerStyleModule('foo', 'some-module');
+ createElement();
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
- importHrefStub = sandbox.stub().callsArg(1);
- stub('gr-external-style', {
- _importHref: (url, resolve, reject) => {
- importHrefStub(url, resolve, reject);
- },
- });
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ importHrefStub = sandbox.stub().callsArg(1);
+ stub('gr-external-style', {
+ _importHref: (url, resolve, reject) => {
+ importHrefStub(url, resolve, reject);
+ },
});
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('imports plugin-provided module', async () => {
- lateRegister();
- await new Promise(flush);
- assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL)));
- });
-
- test('applies plugin-provided styles', async () => {
- lateRegister();
- await new Promise(flush);
- assert.isTrue(element._applyStyle.calledWith('some-module'));
- });
-
- test('does not double import', async () => {
- earlyRegister();
- await new Promise(flush);
- plugin.registerStyleModule('foo', 'some-module');
- await new Promise(flush);
- const urlsImported =
- element._urlsImported.filter(url => url.toString() === TEST_URL);
- assert.strictEqual(urlsImported.length, 1);
- });
-
- test('does not double apply', async () => {
- earlyRegister();
- await new Promise(flush);
- plugin.registerStyleModule('foo', 'some-module');
- await new Promise(flush);
- const stylesApplied =
- element._stylesApplied.filter(name => name === 'some-module');
- assert.strictEqual(stylesApplied.length, 1);
- });
-
- test('loads and applies preloaded modules', async () => {
- earlyRegister();
- await new Promise(flush);
- assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL)));
- assert.isTrue(element._applyStyle.calledWith('some-module'));
- });
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('imports plugin-provided module', async () => {
+ lateRegister();
+ await new Promise(flush);
+ assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL)));
+ });
+
+ test('applies plugin-provided styles', async () => {
+ lateRegister();
+ await new Promise(flush);
+ assert.isTrue(element._applyStyle.calledWith('some-module'));
+ });
+
+ test('does not double import', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ plugin.registerStyleModule('foo', 'some-module');
+ await new Promise(flush);
+ const urlsImported =
+ element._urlsImported.filter(url => url.toString() === TEST_URL);
+ assert.strictEqual(urlsImported.length, 1);
+ });
+
+ test('does not double apply', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ plugin.registerStyleModule('foo', 'some-module');
+ await new Promise(flush);
+ const stylesApplied =
+ element._stylesApplied.filter(name => name === 'some-module');
+ assert.strictEqual(stylesApplied.length, 1);
+ });
+
+ test('loads and applies preloaded modules', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL)));
+ assert.isTrue(element._applyStyle.calledWith('some-module'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
deleted file mode 100644
index f277899..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-@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.
--->
-
-<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="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-plugin-host">
- <script src="gr-plugin-host.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index da050fb..1236f97 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -14,59 +14,63 @@
* 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 GrPluginHost extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-plugin-host'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.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';
- static get properties() {
- return {
- config: {
- type: Object,
- observer: '_configChanged',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrPluginHost extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-plugin-host'; }
- _configChanged(config) {
- const plugins = config.plugin;
- const htmlPlugins = (plugins.html_resource_paths || []);
- const jsPlugins =
- this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
- const shouldLoadTheme = config.default_theme &&
- !Gerrit._isPluginPreloaded('preloaded:gerrit-theme');
- const themeToLoad =
- shouldLoadTheme ? [config.default_theme] : [];
-
- // Theme should be loaded first if has one to have better UX
- const pluginsPending =
- themeToLoad.concat(jsPlugins, htmlPlugins);
-
- const pluginOpts = {};
-
- if (shouldLoadTheme) {
- // Theme needs to be loaded synchronous.
- pluginOpts[config.default_theme] = {sync: true};
- }
-
- Gerrit._loadPlugins(pluginsPending, pluginOpts);
- }
-
- /**
- * Omit .js plugins that have .html counterparts.
- * For example, if plugin provides foo.js and foo.html, skip foo.js.
- */
- _handleMigrations(jsPlugins, htmlPlugins) {
- return jsPlugins.filter(url => {
- const counterpart = url.replace(/\.js$/, '.html');
- return !htmlPlugins.includes(counterpart);
- });
- }
+ static get properties() {
+ return {
+ config: {
+ type: Object,
+ observer: '_configChanged',
+ },
+ };
}
- customElements.define(GrPluginHost.is, GrPluginHost);
-})();
+ _configChanged(config) {
+ const plugins = config.plugin;
+ const htmlPlugins = (plugins.html_resource_paths || []);
+ const jsPlugins =
+ this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
+ const shouldLoadTheme = config.default_theme &&
+ !Gerrit._isPluginPreloaded('preloaded:gerrit-theme');
+ const themeToLoad =
+ shouldLoadTheme ? [config.default_theme] : [];
+
+ // Theme should be loaded first if has one to have better UX
+ const pluginsPending =
+ themeToLoad.concat(jsPlugins, htmlPlugins);
+
+ const pluginOpts = {};
+
+ if (shouldLoadTheme) {
+ // Theme needs to be loaded synchronous.
+ pluginOpts[config.default_theme] = {sync: true};
+ }
+
+ Gerrit._loadPlugins(pluginsPending, pluginOpts);
+ }
+
+ /**
+ * Omit .js plugins that have .html counterparts.
+ * For example, if plugin provides foo.js and foo.html, skip foo.js.
+ */
+ _handleMigrations(jsPlugins, htmlPlugins) {
+ return jsPlugins.filter(url => {
+ const counterpart = url.replace(/\.js$/, '.html');
+ return !htmlPlugins.includes(counterpart);
+ });
+ }
+}
+
+customElements.define(GrPluginHost.is, GrPluginHost);
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index defd242..894c8b4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_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-plugin-host</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-plugin-host.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-plugin-host.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-plugin-host.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,62 +40,64 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-host 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-plugin-host.js';
+suite('gr-plugin-host tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(document.body, 'appendChild');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('load plugins should be called', () => {
- sandbox.stub(Gerrit, '_loadPlugins');
- element.config = {
- plugin: {
- html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
- js_resource_paths: ['plugins/42'],
- },
- };
- assert.isTrue(Gerrit._loadPlugins.calledOnce);
- assert.isTrue(Gerrit._loadPlugins.calledWith([
- 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
- ], {}));
- });
-
- test('theme plugins should be loaded if enabled', () => {
- sandbox.stub(Gerrit, '_loadPlugins');
- element.config = {
- default_theme: 'gerrit-theme.html',
- plugin: {
- html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
- js_resource_paths: ['plugins/42'],
- },
- };
- assert.isTrue(Gerrit._loadPlugins.calledOnce);
- assert.isTrue(Gerrit._loadPlugins.calledWith([
- 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
- ], {'gerrit-theme.html': {sync: true}}));
- });
-
- test('skip theme if preloaded', () => {
- sandbox.stub(Gerrit, '_isPluginPreloaded')
- .withArgs('preloaded:gerrit-theme')
- .returns(true);
- sandbox.stub(Gerrit, '_loadPlugins');
- element.config = {
- default_theme: '/oof',
- plugin: {},
- };
- assert.isTrue(Gerrit._loadPlugins.calledOnce);
- assert.isTrue(Gerrit._loadPlugins.calledWith([], {}));
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(document.body, 'appendChild');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('load plugins should be called', () => {
+ sandbox.stub(Gerrit, '_loadPlugins');
+ element.config = {
+ plugin: {
+ html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+ js_resource_paths: ['plugins/42'],
+ },
+ };
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([
+ 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+ ], {}));
+ });
+
+ test('theme plugins should be loaded if enabled', () => {
+ sandbox.stub(Gerrit, '_loadPlugins');
+ element.config = {
+ default_theme: 'gerrit-theme.html',
+ plugin: {
+ html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+ js_resource_paths: ['plugins/42'],
+ },
+ };
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([
+ 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+ ], {'gerrit-theme.html': {sync: true}}));
+ });
+
+ test('skip theme if preloaded', () => {
+ sandbox.stub(Gerrit, '_isPluginPreloaded')
+ .withArgs('preloaded:gerrit-theme')
+ .returns(true);
+ sandbox.stub(Gerrit, '_loadPlugins');
+ element.config = {
+ default_theme: '/oof',
+ plugin: {},
+ };
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([], {}));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
index 30bf6c8..db44cea 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -14,13 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import '../../shared/gr-overlay/gr-overlay.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-plugin-popup_html.js';
+
(function(window) {
'use strict';
/** @extends Polymer.Element */
- class GrPluginPopup extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
+ class GrPluginPopup extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
static get is() { return 'gr-plugin-popup'; }
get opened() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
index d084445..779cbad 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
@@ -1,31 +1,26 @@
-<!--
-@license
-Copyright (C) 2017 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-overlay/gr-overlay.html">
-
-<dom-module id="gr-plugin-popup">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <gr-overlay id="overlay" with-backdrop>
+ <gr-overlay id="overlay" with-backdrop="">
<slot></slot>
</gr-overlay>
- </template>
- <script src="gr-plugin-popup.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
index 1617cd5..00bbd52 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_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-plugin-popup</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-plugin-popup.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-plugin-popup.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-plugin-popup.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,39 +40,41 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-popup 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-plugin-popup.js';
+suite('gr-plugin-popup tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- stub('gr-overlay', {
- open: sandbox.stub().returns(Promise.resolve()),
- close: sandbox.stub(),
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(element);
- });
-
- test('open uses open() from gr-overlay', done => {
- element.open().then(() => {
- assert.isTrue(element.$.overlay.open.called);
- done();
- });
- });
-
- test('close uses close() from gr-overlay', () => {
- element.close();
- assert.isTrue(element.$.overlay.close.called);
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-overlay', {
+ open: sandbox.stub().returns(Promise.resolve()),
+ close: sandbox.stub(),
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(element);
+ });
+
+ test('open uses open() from gr-overlay', done => {
+ element.open().then(() => {
+ assert.isTrue(element.$.overlay.open.called);
+ done();
+ });
+ });
+
+ test('close uses close() from gr-overlay', () => {
+ element.close();
+ assert.isTrue(element.$.overlay.close.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
deleted file mode 100644
index a8bb06b..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="gr-plugin-popup.html">
-
-<dom-module id="gr-popup-interface">
- <script src="gr-popup-interface.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
index c3588a1..e9d3e36 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -14,6 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import './gr-plugin-popup.js';
+import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-popup-interface">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
@@ -35,7 +47,7 @@
}
GrPopupInterface.prototype._getElement = function() {
- return Polymer.dom(this._popup);
+ return dom(this._popup);
};
/**
@@ -52,12 +64,12 @@
.then(hookEl => {
const popup = document.createElement('gr-plugin-popup');
if (this._moduleName) {
- const el = Polymer.dom(popup).appendChild(
+ const el = dom(popup).appendChild(
document.createElement(this._moduleName));
el.plugin = this.plugin;
}
- this._popup = Polymer.dom(hookEl).appendChild(popup);
- Polymer.dom.flush();
+ this._popup = dom(hookEl).appendChild(popup);
+ flush();
return this._popup.open().then(() => this);
});
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
index c1593b7..aeef29a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-popup-interface</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-popup-interface.html"/>
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.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-popup-interface.js"></script>
+<script type="module" src="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-popup-interface.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="container">
<template>
@@ -40,85 +46,94 @@
<template>
<div id="barfoo">some test module</div>
</template>
- <script>
- Polymer({is: 'gr-user-test-popup'});
- </script>
+ <script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-popup-interface.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({is: 'gr-user-test-popup'});
+</script>
</dom-module>
-<script>
- suite('gr-popup-interface tests', async () => {
- await readyToTest();
- let container;
- let instance;
- let plugin;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-popup-interface.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-popup-interface tests', () => {
+ let container;
+ let instance;
+ let plugin;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ container = fixture('container');
+ sandbox.stub(plugin, 'hook').returns({
+ getLastAttached() {
+ return Promise.resolve(container);
+ },
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('manual', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- container = fixture('container');
- sandbox.stub(plugin, 'hook').returns({
- getLastAttached() {
- return Promise.resolve(container);
- },
+ instance = new GrPopupInterface(plugin);
+ });
+
+ test('open', done => {
+ instance.open().then(api => {
+ assert.strictEqual(api, instance);
+ const manual = document.createElement('div');
+ manual.id = 'foobar';
+ manual.innerHTML = 'manual content';
+ api._getElement().appendChild(manual);
+ flushAsynchronousOperations();
+ assert.equal(
+ container.querySelector('#foobar').textContent, 'manual content');
+ done();
});
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('manual', () => {
- setup(() => {
- instance = new GrPopupInterface(plugin);
- });
-
- test('open', done => {
- instance.open().then(api => {
- assert.strictEqual(api, instance);
- const manual = document.createElement('div');
- manual.id = 'foobar';
- manual.innerHTML = 'manual content';
- api._getElement().appendChild(manual);
- flushAsynchronousOperations();
- assert.equal(
- container.querySelector('#foobar').textContent, 'manual content');
- done();
- });
- });
-
- test('close', done => {
- instance.open().then(api => {
- assert.isTrue(api._getElement().node.opened);
- api.close();
- assert.isFalse(api._getElement().node.opened);
- done();
- });
- });
- });
-
- suite('components', () => {
- setup(() => {
- instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
- });
-
- test('open', done => {
- instance.open().then(api => {
- assert.isNotNull(
- Polymer.dom(container).querySelector('gr-user-test-popup'));
- done();
- });
- });
-
- test('close', done => {
- instance.open().then(api => {
- assert.isTrue(api._getElement().node.opened);
- api.close();
- assert.isFalse(api._getElement().node.opened);
- done();
- });
+ test('close', done => {
+ instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ done();
});
});
});
+
+ suite('components', () => {
+ setup(() => {
+ instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
+ });
+
+ test('open', done => {
+ instance.open().then(api => {
+ assert.isNotNull(
+ dom(container).querySelector('gr-user-test-popup'));
+ done();
+ });
+ });
+
+ test('close', done => {
+ instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
index 593c1e0..f9a2bdf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
@@ -1,35 +1,35 @@
-<!--
-@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.
+ */
+import '../../../scripts/bundled-polymer.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="../../admin/gr-repo-command/gr-repo-command.html">
-
-<dom-module id="gr-plugin-repo-command">
- <template>
+import '../../admin/gr-repo-command/gr-repo-command.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+Polymer({
+ _template: html`
<gr-repo-command title="[[title]]">
</gr-repo-command>
- </template>
- <script>
- Polymer({
- is: 'gr-plugin-repo-command',
- properties: {
- title: String,
- repoName: String,
- config: Object,
- },
- });
- </script>
-</dom-module>
+`,
+
+ is: 'gr-plugin-repo-command',
+
+ properties: {
+ title: String,
+ repoName: String,
+ config: Object,
+ },
+});
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
deleted file mode 100644
index b3f6aec..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="gr-plugin-repo-command.html">
-
-<dom-module id="gr-repo-api">
- <script src="gr-repo-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
index b59cce6..6c1a3c8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import './gr-plugin-repo-command.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-repo-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index adc1de5..c177715 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-api</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-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="gr-repo-api.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-endpoint-decorator/gr-endpoint-decorator.js"></script>
+<script type="module" src="./gr-repo-api.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-repo-api.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,52 +43,55 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-api tests', async () => {
- await readyToTest();
- let sandbox;
- let repoApi;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-repo-api.js';
+suite('gr-repo-api tests', () => {
+ let sandbox;
+ let repoApi;
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- repoApi = plugin.project();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ repoApi = plugin.project();
+ });
- teardown(() => {
- repoApi = null;
- sandbox.restore();
- });
+ teardown(() => {
+ repoApi = null;
+ sandbox.restore();
+ });
- test('exists', () => {
- assert.isOk(repoApi);
- });
+ test('exists', () => {
+ assert.isOk(repoApi);
+ });
- test('works', done => {
- const attachedStub = sandbox.stub();
- const tapStub = sandbox.stub();
- repoApi
- .createCommand('foo', attachedStub)
- .onTap(tapStub);
- const element = fixture('basic');
- flush(() => {
- assert.isTrue(attachedStub.called);
- const pluginCommand = element.shadowRoot
- .querySelector('gr-plugin-repo-command');
- assert.isOk(pluginCommand);
- const command = pluginCommand.shadowRoot
- .querySelector('gr-repo-command');
- assert.isOk(command);
- assert.equal(command.title, 'foo');
- assert.isFalse(tapStub.called);
- MockInteractions.tap(command.shadowRoot
- .querySelector('gr-button'));
- assert.isTrue(tapStub.called);
- done();
- });
+ test('works', done => {
+ const attachedStub = sandbox.stub();
+ const tapStub = sandbox.stub();
+ repoApi
+ .createCommand('foo', attachedStub)
+ .onTap(tapStub);
+ const element = fixture('basic');
+ flush(() => {
+ assert.isTrue(attachedStub.called);
+ const pluginCommand = element.shadowRoot
+ .querySelector('gr-plugin-repo-command');
+ assert.isOk(pluginCommand);
+ const command = pluginCommand.shadowRoot
+ .querySelector('gr-repo-command');
+ assert.isOk(command);
+ assert.equal(command.title, 'foo');
+ assert.isFalse(tapStub.called);
+ MockInteractions.tap(command.shadowRoot
+ .querySelector('gr-button'));
+ assert.isTrue(tapStub.called);
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
deleted file mode 100644
index 999ecfa..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Settings
-
-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="../../settings/gr-settings-view/gr-settings-item.html">
-<link rel="import" href="../../settings/gr-settings-view/gr-settings-menu-item.html">
-
-<dom-module id="gr-settings-api">
- <script src="gr-settings-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
index 5ed4c1a..a8bfccdd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -14,6 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../settings/gr-settings-view/gr-settings-item.js';
+import '../../settings/gr-settings-view/gr-settings-menu-item.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-settings-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
index 2efd182..b0dbc3c 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-api</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-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="gr-settings-api.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-endpoint-decorator/gr-endpoint-decorator.js"></script>
+<script type="module" src="./gr-settings-api.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-settings-api.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -39,51 +45,54 @@
</template>
</test-fixture>
-<script>
- suite('gr-settings-api tests', async () => {
- await readyToTest();
- let sandbox;
- let settingsApi;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-settings-api.js';
+suite('gr-settings-api tests', () => {
+ let sandbox;
+ let settingsApi;
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- settingsApi = plugin.settings();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ settingsApi = plugin.settings();
+ });
- teardown(() => {
- settingsApi = null;
- sandbox.restore();
- });
+ teardown(() => {
+ settingsApi = null;
+ sandbox.restore();
+ });
- test('exists', () => {
- assert.isOk(settingsApi);
- });
+ test('exists', () => {
+ assert.isOk(settingsApi);
+ });
- test('works', done => {
- settingsApi
- .title('foo')
- .token('bar')
- .module('some-settings-screen')
- .build();
- const element = fixture('basic');
- flush(() => {
- const [menuItemEl, itemEl] = element;
- const menuItem = menuItemEl.shadowRoot
- .querySelector('gr-settings-menu-item');
- assert.isOk(menuItem);
- assert.equal(menuItem.title, 'foo');
- assert.equal(menuItem.href, '#x/testplugin/bar');
- const item = itemEl.shadowRoot
- .querySelector('gr-settings-item');
- assert.isOk(item);
- assert.equal(item.title, 'foo');
- assert.equal(item.anchor, 'x/testplugin/bar');
- done();
- });
+ test('works', done => {
+ settingsApi
+ .title('foo')
+ .token('bar')
+ .module('some-settings-screen')
+ .build();
+ const element = fixture('basic');
+ flush(() => {
+ const [menuItemEl, itemEl] = element;
+ const menuItem = menuItemEl.shadowRoot
+ .querySelector('gr-settings-menu-item');
+ assert.isOk(menuItem);
+ assert.equal(menuItem.title, 'foo');
+ assert.equal(menuItem.href, '#x/testplugin/bar');
+ const item = itemEl.shadowRoot
+ .querySelector('gr-settings-item');
+ assert.isOk(item);
+ assert.equal(item.title, 'foo');
+ assert.equal(item.anchor, 'x/testplugin/bar');
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html
deleted file mode 100644
index 74b87c8..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<script src="gr-styles-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
index feb59fe..a69db8d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
@@ -19,31 +19,75 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-api</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="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="gr-styles-api.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="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script>
+<script type="module" src="./gr-styles-api.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-styles-api.js';
+void(0);
+</script>
<dom-module id="gr-style-test-element">
<template>
<div id="wrapper"></div>
</template>
- <script>
- Polymer({is: 'gr-style-test-element'});
- </script>
+ <script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-styles-api.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+Polymer({is: 'gr-style-test-element'});
+</script>
</dom-module>
-<script>
- suite('gr-styles-api tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
+import './gr-styles-api.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-styles-api tests', () => {
+ let sandbox;
+ let stylesApi;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ stylesApi = plugin.styles();
+ });
+
+ teardown(() => {
+ stylesApi = null;
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(stylesApi);
+ });
+
+ test('css', () => {
+ const styleObject = stylesApi.css('background: red');
+ assert.isDefined(styleObject);
+ });
+
+ suite('GrStyleObject tests', () => {
let sandbox;
let stylesApi;
+ let displayInlineStyle;
+ let displayNoneStyle;
setup(() => {
sandbox = sinon.sandbox.create();
@@ -52,133 +96,104 @@
'http://test.com/plugins/testplugin/static/test.js');
Gerrit._loadPlugins([]);
stylesApi = plugin.styles();
+ displayInlineStyle = stylesApi.css('display: inline');
+ displayNoneStyle = stylesApi.css('display: none');
});
teardown(() => {
+ displayInlineStyle = null;
+ displayNoneStyle = null;
stylesApi = null;
sandbox.restore();
});
- test('exists', () => {
- assert.isOk(stylesApi);
+ function createNestedElements(parentElement) {
+ /* parentElement
+ * |--- element1
+ * |--- element2
+ * |--- element3
+ **/
+ const element1 = document.createElement('div');
+ const element2 = document.createElement('div');
+ const element3 = document.createElement('div');
+ dom(parentElement).appendChild(element1);
+ dom(parentElement).appendChild(element2);
+ dom(element2).appendChild(element3);
+
+ return [element1, element2, element3];
+ }
+
+ test('getClassName - body level elements', () => {
+ const bodyLevelElements = createNestedElements(document.body);
+
+ testGetClassName(bodyLevelElements);
});
- test('css', () => {
- const styleObject = stylesApi.css('background: red');
- assert.isDefined(styleObject);
+ test('getClassName - elements inside polymer element', () => {
+ const polymerElement = document.createElement('gr-style-test-element');
+ dom(document.body).appendChild(polymerElement);
+ const contentElements = createNestedElements(polymerElement.$.wrapper);
+
+ testGetClassName(contentElements);
});
- suite('GrStyleObject tests', () => {
- let sandbox;
- let stylesApi;
- let displayInlineStyle;
- let displayNoneStyle;
+ function testGetClassName(elements) {
+ assertAllElementsHaveDefaultStyle(elements);
- setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- stylesApi = plugin.styles();
- displayInlineStyle = stylesApi.css('display: inline');
- displayNoneStyle = stylesApi.css('display: none');
- });
+ const className1 = displayInlineStyle.getClassName(elements[0]);
+ const className2 = displayNoneStyle.getClassName(elements[1]);
+ const className3 = displayInlineStyle.getClassName(elements[2]);
- teardown(() => {
- displayInlineStyle = null;
- displayNoneStyle = null;
- stylesApi = null;
- sandbox.restore();
- });
+ assert.notEqual(className2, className1);
+ assert.equal(className3, className1);
- function createNestedElements(parentElement) {
- /* parentElement
- * |--- element1
- * |--- element2
- * |--- element3
- **/
- const element1 = document.createElement('div');
- const element2 = document.createElement('div');
- const element3 = document.createElement('div');
- Polymer.dom(parentElement).appendChild(element1);
- Polymer.dom(parentElement).appendChild(element2);
- Polymer.dom(element2).appendChild(element3);
+ assertAllElementsHaveDefaultStyle(elements);
- return [element1, element2, element3];
+ elements[0].classList.add(className1);
+ elements[1].classList.add(className2);
+ elements[2].classList.add(className1);
+
+ assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
+ }
+
+ test('apply - body level elements', () => {
+ const bodyLevelElements = createNestedElements(document.body);
+
+ testApply(bodyLevelElements);
+ });
+
+ test('apply - elements inside polymer element', () => {
+ const polymerElement = document.createElement('gr-style-test-element');
+ dom(document.body).appendChild(polymerElement);
+ const contentElements = createNestedElements(polymerElement.$.wrapper);
+
+ testApply(contentElements);
+ });
+
+ function testApply(elements) {
+ assertAllElementsHaveDefaultStyle(elements);
+ displayInlineStyle.apply(elements[0]);
+ displayNoneStyle.apply(elements[1]);
+ displayInlineStyle.apply(elements[2]);
+ assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
+ }
+
+ function assertAllElementsHaveDefaultStyle(elements) {
+ for (const element of elements) {
+ assert.equal(getComputedStyle(element).getPropertyValue('display'),
+ 'block');
}
+ }
- test('getClassName - body level elements', () => {
- const bodyLevelElements = createNestedElements(document.body);
-
- testGetClassName(bodyLevelElements);
- });
-
- test('getClassName - elements inside polymer element', () => {
- const polymerElement = document.createElement('gr-style-test-element');
- Polymer.dom(document.body).appendChild(polymerElement);
- const contentElements = createNestedElements(polymerElement.$.wrapper);
-
- testGetClassName(contentElements);
- });
-
- function testGetClassName(elements) {
- assertAllElementsHaveDefaultStyle(elements);
-
- const className1 = displayInlineStyle.getClassName(elements[0]);
- const className2 = displayNoneStyle.getClassName(elements[1]);
- const className3 = displayInlineStyle.getClassName(elements[2]);
-
- assert.notEqual(className2, className1);
- assert.equal(className3, className1);
-
- assertAllElementsHaveDefaultStyle(elements);
-
- elements[0].classList.add(className1);
- elements[1].classList.add(className2);
- elements[2].classList.add(className1);
-
- assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
- }
-
- test('apply - body level elements', () => {
- const bodyLevelElements = createNestedElements(document.body);
-
- testApply(bodyLevelElements);
- });
-
- test('apply - elements inside polymer element', () => {
- const polymerElement = document.createElement('gr-style-test-element');
- Polymer.dom(document.body).appendChild(polymerElement);
- const contentElements = createNestedElements(polymerElement.$.wrapper);
-
- testApply(contentElements);
- });
-
- function testApply(elements) {
- assertAllElementsHaveDefaultStyle(elements);
- displayInlineStyle.apply(elements[0]);
- displayNoneStyle.apply(elements[1]);
- displayInlineStyle.apply(elements[2]);
- assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
- }
-
- function assertAllElementsHaveDefaultStyle(elements) {
- for (const element of elements) {
- assert.equal(getComputedStyle(element).getPropertyValue('display'),
- 'block');
+ function assertDisplayPropertyValues(elements, expectedDisplayValues) {
+ for (const key in elements) {
+ if (elements.hasOwnProperty(key)) {
+ assert.equal(
+ getComputedStyle(elements[key]).getPropertyValue('display'),
+ expectedDisplayValues[key]);
}
}
-
- function assertDisplayPropertyValues(elements, expectedDisplayValues) {
- for (const key in elements) {
- if (elements.hasOwnProperty(key)) {
- assert.equal(
- getComputedStyle(elements[key]).getPropertyValue('display'),
- expectedDisplayValues[key]);
- }
- }
- }
- });
+ }
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
index f0eacd2..411a7c8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
@@ -1,24 +1,25 @@
-<!--
-@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.
+ */
+import '../../../scripts/bundled-polymer.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">
-
-<dom-module id="gr-custom-plugin-header">
- <template>
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+Polymer({
+ _template: html`
<style>
img {
width: 1em;
@@ -30,17 +31,15 @@
}
</style>
<span>
- <img src="[[logoUrl]]" hidden$="[[!logoUrl]]">
+ <img src="[[logoUrl]]" hidden\$="[[!logoUrl]]">
<span class="title">[[title]]</span>
</span>
- </template>
- <script>
- Polymer({
- is: 'gr-custom-plugin-header',
- properties: {
- logoUrl: String,
- title: String,
- },
- });
- </script>
-</dom-module>
+`,
+
+ is: 'gr-custom-plugin-header',
+
+ properties: {
+ logoUrl: String,
+ title: String,
+ },
+});
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
deleted file mode 100644
index ef1c9d4..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="gr-custom-plugin-header.html">
-
-<dom-module id="gr-theme-api">
- <script src="gr-theme-api.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index d145f52f..8da680b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -14,6 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+import './gr-custom-plugin-header.js';
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-theme-api">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index 80853f5..6428f90 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -19,16 +19,22 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-theme-api</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-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="gr-theme-api.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-endpoint-decorator/gr-endpoint-decorator.js"></script>
+<script type="module" src="./gr-theme-api.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-theme-api.js';
+void(0);
+</script>
<test-fixture id="header-title">
<template>
@@ -38,50 +44,53 @@
</template>
</test-fixture>
-<script>
- suite('gr-theme-api tests', async () => {
- await readyToTest();
- let sandbox;
- let theme;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-endpoint-decorator/gr-endpoint-decorator.js';
+import './gr-theme-api.js';
+suite('gr-theme-api tests', () => {
+ let sandbox;
+ let theme;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ theme = plugin.theme();
+ });
+
+ teardown(() => {
+ theme = null;
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(theme);
+ });
+
+ suite('header-title', () => {
+ let customHeader;
setup(() => {
- sandbox = sinon.sandbox.create();
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- theme = plugin.theme();
- });
-
- teardown(() => {
- theme = null;
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(theme);
- });
-
- suite('header-title', () => {
- let customHeader;
-
- setup(() => {
- fixture('header-title');
- stub('gr-custom-plugin-header', {
- /** @override */
- ready() { customHeader = this; },
- });
- Gerrit._loadPlugins([]);
+ fixture('header-title');
+ stub('gr-custom-plugin-header', {
+ /** @override */
+ ready() { customHeader = this; },
});
+ Gerrit._loadPlugins([]);
+ });
- test('sets logo and title', done => {
- theme.setHeaderLogoAndTitle('foo.jpg', 'bar');
- flush(() => {
- assert.isNotNull(customHeader);
- assert.equal(customHeader.logoUrl, 'foo.jpg');
- assert.equal(customHeader.title, 'bar');
- done();
- });
+ test('sets logo and title', done => {
+ theme.setHeaderLogoAndTitle('foo.jpg', 'bar');
+ flush(() => {
+ assert.isNotNull(customHeader);
+ assert.equal(customHeader.logoUrl, 'foo.jpg');
+ assert.equal(customHeader.title, 'bar');
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 7bf641d..c425318 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -14,195 +14,208 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../shared/gr-avatar/gr-avatar.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/gr-form-styles.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-account-info_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccountInfo extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-info'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when account details are changed.
+ *
+ * @event account-detail-update
*/
- class GrAccountInfo extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-info'; }
- /**
- * Fired when account details are changed.
- *
- * @event account-detail-update
- */
- static get properties() {
- return {
- usernameMutable: {
- type: Boolean,
- notify: true,
- computed: '_computeUsernameMutable(_serverConfig, _account.username)',
- },
- nameMutable: {
- type: Boolean,
- notify: true,
- computed: '_computeNameMutable(_serverConfig)',
- },
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- computed: '_computeHasUnsavedChanges(_hasNameChange, ' +
- '_hasUsernameChange, _hasStatusChange)',
- },
+ static get properties() {
+ return {
+ usernameMutable: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeUsernameMutable(_serverConfig, _account.username)',
+ },
+ nameMutable: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeNameMutable(_serverConfig)',
+ },
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ computed: '_computeHasUnsavedChanges(_hasNameChange, ' +
+ '_hasUsernameChange, _hasStatusChange)',
+ },
- _hasNameChange: Boolean,
- _hasUsernameChange: Boolean,
- _hasStatusChange: Boolean,
- _loading: {
- type: Boolean,
- value: false,
- },
- _saving: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- _account: Object,
- _serverConfig: Object,
- _username: {
- type: String,
- observer: '_usernameChanged',
- },
- _avatarChangeUrl: {
- type: String,
- value: '',
- },
- };
+ _hasNameChange: Boolean,
+ _hasUsernameChange: Boolean,
+ _hasStatusChange: Boolean,
+ _loading: {
+ type: Boolean,
+ value: false,
+ },
+ _saving: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ _account: Object,
+ _serverConfig: Object,
+ _username: {
+ type: String,
+ observer: '_usernameChanged',
+ },
+ _avatarChangeUrl: {
+ type: String,
+ value: '',
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_nameChanged(_account.name)',
+ '_statusChanged(_account.status)',
+ ];
+ }
+
+ loadData() {
+ const promises = [];
+
+ this._loading = true;
+
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
+ }));
+
+ promises.push(this.$.restAPI.getAccount().then(account => {
+ this._hasNameChange = false;
+ this._hasUsernameChange = false;
+ this._hasStatusChange = false;
+ // Provide predefined value for username to trigger computation of
+ // username mutability.
+ account.username = account.username || '';
+ this._account = account;
+ this._username = account.username;
+ }));
+
+ promises.push(this.$.restAPI.getAvatarChangeUrl().then(url => {
+ this._avatarChangeUrl = url;
+ }));
+
+ return Promise.all(promises).then(() => {
+ this._loading = false;
+ });
+ }
+
+ save() {
+ if (!this.hasUnsavedChanges) {
+ return Promise.resolve();
}
- static get observers() {
- return [
- '_nameChanged(_account.name)',
- '_statusChanged(_account.status)',
- ];
+ this._saving = true;
+ // Set only the fields that have changed.
+ // Must be done in sequence to avoid race conditions (@see Issue 5721)
+ return this._maybeSetName()
+ .then(this._maybeSetUsername.bind(this))
+ .then(this._maybeSetStatus.bind(this))
+ .then(() => {
+ this._hasNameChange = false;
+ this._hasStatusChange = false;
+ this._saving = false;
+ this.fire('account-detail-update');
+ });
+ }
+
+ _maybeSetName() {
+ return this._hasNameChange && this.nameMutable ?
+ this.$.restAPI.setAccountName(this._account.name) :
+ Promise.resolve();
+ }
+
+ _maybeSetUsername() {
+ return this._hasUsernameChange && this.usernameMutable ?
+ this.$.restAPI.setAccountUsername(this._username) :
+ Promise.resolve();
+ }
+
+ _maybeSetStatus() {
+ return this._hasStatusChange ?
+ this.$.restAPI.setAccountStatus(this._account.status) :
+ Promise.resolve();
+ }
+
+ _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) {
+ return nameChanged || usernameChanged || statusChanged;
+ }
+
+ _computeUsernameMutable(config, username) {
+ // Polymer 2: check for undefined
+ if ([
+ config,
+ username,
+ ].some(arg => arg === undefined)) {
+ return undefined;
}
- loadData() {
- const promises = [];
+ // Username may not be changed once it is set.
+ return config.auth.editable_account_fields.includes('USER_NAME') &&
+ !username;
+ }
- this._loading = true;
+ _computeNameMutable(config) {
+ return config.auth.editable_account_fields.includes('FULL_NAME');
+ }
- promises.push(this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
- }));
+ _statusChanged() {
+ if (this._loading) { return; }
+ this._hasStatusChange = true;
+ }
- promises.push(this.$.restAPI.getAccount().then(account => {
- this._hasNameChange = false;
- this._hasUsernameChange = false;
- this._hasStatusChange = false;
- // Provide predefined value for username to trigger computation of
- // username mutability.
- account.username = account.username || '';
- this._account = account;
- this._username = account.username;
- }));
+ _usernameChanged() {
+ if (this._loading || !this._account) { return; }
+ this._hasUsernameChange =
+ (this._account.username || '') !== (this._username || '');
+ }
- promises.push(this.$.restAPI.getAvatarChangeUrl().then(url => {
- this._avatarChangeUrl = url;
- }));
+ _nameChanged() {
+ if (this._loading) { return; }
+ this._hasNameChange = true;
+ }
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- }
-
- save() {
- if (!this.hasUnsavedChanges) {
- return Promise.resolve();
- }
-
- this._saving = true;
- // Set only the fields that have changed.
- // Must be done in sequence to avoid race conditions (@see Issue 5721)
- return this._maybeSetName()
- .then(this._maybeSetUsername.bind(this))
- .then(this._maybeSetStatus.bind(this))
- .then(() => {
- this._hasNameChange = false;
- this._hasStatusChange = false;
- this._saving = false;
- this.fire('account-detail-update');
- });
- }
-
- _maybeSetName() {
- return this._hasNameChange && this.nameMutable ?
- this.$.restAPI.setAccountName(this._account.name) :
- Promise.resolve();
- }
-
- _maybeSetUsername() {
- return this._hasUsernameChange && this.usernameMutable ?
- this.$.restAPI.setAccountUsername(this._username) :
- Promise.resolve();
- }
-
- _maybeSetStatus() {
- return this._hasStatusChange ?
- this.$.restAPI.setAccountStatus(this._account.status) :
- Promise.resolve();
- }
-
- _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) {
- return nameChanged || usernameChanged || statusChanged;
- }
-
- _computeUsernameMutable(config, username) {
- // Polymer 2: check for undefined
- if ([
- config,
- username,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- // Username may not be changed once it is set.
- return config.auth.editable_account_fields.includes('USER_NAME') &&
- !username;
- }
-
- _computeNameMutable(config) {
- return config.auth.editable_account_fields.includes('FULL_NAME');
- }
-
- _statusChanged() {
- if (this._loading) { return; }
- this._hasStatusChange = true;
- }
-
- _usernameChanged() {
- if (this._loading || !this._account) { return; }
- this._hasUsernameChange =
- (this._account.username || '') !== (this._username || '');
- }
-
- _nameChanged() {
- if (this._loading) { return; }
- this._hasNameChange = true;
- }
-
- _handleKeydown(e) {
- if (e.keyCode === 13) { // Enter
- e.stopPropagation();
- this.save();
- }
- }
-
- _hideAvatarChangeUrl(avatarChangeUrl) {
- if (!avatarChangeUrl) {
- return 'hide';
- }
-
- return '';
+ _handleKeydown(e) {
+ if (e.keyCode === 13) { // Enter
+ e.stopPropagation();
+ this.save();
}
}
- customElements.define(GrAccountInfo.is, GrAccountInfo);
-})();
+ _hideAvatarChangeUrl(avatarChangeUrl) {
+ if (!avatarChangeUrl) {
+ return 'hide';
+ }
+
+ return '';
+ }
+}
+
+customElements.define(GrAccountInfo.is, GrAccountInfo);
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
index e685030..6e37c25 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_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="/bower_components/iron-input/iron-input.html">
-
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-
-<dom-module id="gr-account-info">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
gr-avatar {
height: 120px;
@@ -47,14 +35,13 @@
<section>
<span class="title"></span>
<span class="value">
- <gr-avatar account="[[_account]]"
- image-size="120"></gr-avatar>
+ <gr-avatar account="[[_account]]" image-size="120"></gr-avatar>
</span>
</section>
- <section class$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">
+ <section class\$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]">
<span class="title"></span>
<span class="value">
- <a href$="[[_avatarChangeUrl]]">
+ <a href\$="[[_avatarChangeUrl]]">
Change avatar
</a>
</span>
@@ -70,68 +57,35 @@
<section>
<span class="title">Registered</span>
<span class="value">
- <gr-date-formatter
- has-tooltip
- date-str="[[_account.registered_on]]"></gr-date-formatter>
+ <gr-date-formatter has-tooltip="" date-str="[[_account.registered_on]]"></gr-date-formatter>
</span>
</section>
<section id="usernameSection">
<span class="title">Username</span>
- <span
- hidden$="[[usernameMutable]]"
- class="value">[[_username]]</span>
- <span
- hidden$="[[!usernameMutable]]"
- class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_username}}">
- <input
- is="iron-input"
- id="usernameInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- bind-value="{{_username}}">
+ <span hidden\$="[[usernameMutable]]" class="value">[[_username]]</span>
+ <span hidden\$="[[!usernameMutable]]" class="value">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_username}}">
+ <input is="iron-input" id="usernameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_username}}">
</iron-input>
</span>
</section>
<section id="nameSection">
<span class="title">Full name</span>
- <span
- hidden$="[[nameMutable]]"
- class="value">[[_account.name]]</span>
- <span
- hidden$="[[!nameMutable]]"
- class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_account.name}}">
- <input
- is="iron-input"
- id="nameInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- bind-value="{{_account.name}}">
+ <span hidden\$="[[nameMutable]]" class="value">[[_account.name]]</span>
+ <span hidden\$="[[!nameMutable]]" class="value">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_account.name}}">
+ <input is="iron-input" id="nameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.name}}">
</iron-input>
</span>
</section>
<section>
<span class="title">Status (e.g. "Vacation")</span>
<span class="value">
- <iron-input
- on-keydown="_handleKeydown"
- bind-value="{{_account.status}}">
- <input
- is="iron-input"
- id="statusInput"
- disabled="[[_saving]]"
- on-keydown="_handleKeydown"
- bind-value="{{_account.status}}">
+ <iron-input on-keydown="_handleKeydown" bind-value="{{_account.status}}">
+ <input is="iron-input" id="statusInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.status}}">
</iron-input>
</span>
</section>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-account-info.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index 80c7399..640ae37 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_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-info</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-info.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-info.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-account-info.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,310 +40,313 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-info tests', async () => {
- await readyToTest();
- let element;
- let account;
- let config;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-account-info.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-account-info tests', () => {
+ let element;
+ let account;
+ let config;
+ let sandbox;
- function valueOf(title) {
- const sections = Polymer.dom(element.root).querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title) {
+ const sections = dom(element.root).querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- setup(done => {
- sandbox = sinon.sandbox.create();
- account = {
- _account_id: 123,
- name: 'user name',
- email: 'user@email',
- username: 'user username',
- registered: '2000-01-01 00:00:00.000000000',
- };
- config = {auth: {editable_account_fields: []}};
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ account = {
+ _account_id: 123,
+ name: 'user name',
+ email: 'user@email',
+ username: 'user username',
+ registered: '2000-01-01 00:00:00.000000000',
+ };
+ config = {auth: {editable_account_fields: []}};
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(account); },
- getConfig() { return Promise.resolve(config); },
- getPreferences() {
- return Promise.resolve({time_format: 'HHMM_12'});
- },
- });
- element = fixture('basic');
- // Allow the element to render.
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(account); },
+ getConfig() { return Promise.resolve(config); },
+ getPreferences() {
+ return Promise.resolve({time_format: 'HHMM_12'});
+ },
});
+ element = fixture('basic');
+ // Allow the element to render.
+ element.loadData().then(() => { flush(done); });
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('basic account info render', () => {
- assert.isFalse(element._loading);
+ test('basic account info render', () => {
+ assert.isFalse(element._loading);
- assert.equal(valueOf('ID').textContent, account._account_id);
- assert.equal(valueOf('Email').textContent, account.email);
- assert.equal(valueOf('Username').textContent, account.username);
- });
+ assert.equal(valueOf('ID').textContent, account._account_id);
+ assert.equal(valueOf('Email').textContent, account.email);
+ assert.equal(valueOf('Username').textContent, account.username);
+ });
- test('full name render (immutable)', () => {
- const section = element.$.nameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ test('full name render (immutable)', () => {
+ const section = element.$.nameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
- assert.isFalse(element.nameMutable);
- assert.isFalse(displaySpan.hasAttribute('hidden'));
- assert.equal(displaySpan.textContent, account.name);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
- });
+ assert.isFalse(element.nameMutable);
+ assert.isFalse(displaySpan.hasAttribute('hidden'));
+ assert.equal(displaySpan.textContent, account.name);
+ assert.isTrue(inputSpan.hasAttribute('hidden'));
+ });
- test('full name render (mutable)', () => {
+ test('full name render (mutable)', () => {
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: ['FULL_NAME']}});
+
+ const section = element.$.nameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isTrue(element.nameMutable);
+ assert.isTrue(displaySpan.hasAttribute('hidden'));
+ assert.equal(element.$.nameInput.bindValue, account.name);
+ assert.isFalse(inputSpan.hasAttribute('hidden'));
+ });
+
+ test('username render (immutable)', () => {
+ const section = element.$.usernameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isFalse(element.usernameMutable);
+ assert.isFalse(displaySpan.hasAttribute('hidden'));
+ assert.equal(displaySpan.textContent, account.username);
+ assert.isTrue(inputSpan.hasAttribute('hidden'));
+ });
+
+ test('username render (mutable)', () => {
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: ['USER_NAME']}});
+ element.set('_account.username', '');
+ element.set('_username', '');
+
+ const section = element.$.usernameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isTrue(element.usernameMutable);
+ assert.isTrue(displaySpan.hasAttribute('hidden'));
+ assert.equal(element.$.usernameInput.bindValue, account.username);
+ assert.isFalse(inputSpan.hasAttribute('hidden'));
+ });
+
+ suite('account info edit', () => {
+ let nameChangedSpy;
+ let usernameChangedSpy;
+ let statusChangedSpy;
+ let nameStub;
+ let usernameStub;
+ let statusStub;
+
+ setup(() => {
+ nameChangedSpy = sandbox.spy(element, '_nameChanged');
+ usernameChangedSpy = sandbox.spy(element, '_usernameChanged');
+ statusChangedSpy = sandbox.spy(element, '_statusChanged');
element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME']}});
+ {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
- const section = element.$.nameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+ name => Promise.resolve());
+ usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername',
+ username => Promise.resolve());
+ statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+ status => Promise.resolve());
+ });
+ test('name', done => {
assert.isTrue(element.nameMutable);
- assert.isTrue(displaySpan.hasAttribute('hidden'));
- assert.equal(element.$.nameInput.bindValue, account.name);
- assert.isFalse(inputSpan.hasAttribute('hidden'));
- });
+ assert.isFalse(element.hasUnsavedChanges);
- test('username render (immutable)', () => {
- const section = element.$.usernameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
+ element.set('_account.name', 'new name');
- assert.isFalse(element.usernameMutable);
- assert.isFalse(displaySpan.hasAttribute('hidden'));
- assert.equal(displaySpan.textContent, account.username);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
- });
+ assert.isTrue(nameChangedSpy.called);
+ assert.isFalse(statusChangedSpy.called);
+ assert.isTrue(element.hasUnsavedChanges);
- test('username render (mutable)', () => {
- element.set('_serverConfig',
- {auth: {editable_account_fields: ['USER_NAME']}});
- element.set('_account.username', '');
- element.set('_username', '');
-
- const section = element.$.usernameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
-
- assert.isTrue(element.usernameMutable);
- assert.isTrue(displaySpan.hasAttribute('hidden'));
- assert.equal(element.$.usernameInput.bindValue, account.username);
- assert.isFalse(inputSpan.hasAttribute('hidden'));
- });
-
- suite('account info edit', () => {
- let nameChangedSpy;
- let usernameChangedSpy;
- let statusChangedSpy;
- let nameStub;
- let usernameStub;
- let statusStub;
-
- setup(() => {
- nameChangedSpy = sandbox.spy(element, '_nameChanged');
- usernameChangedSpy = sandbox.spy(element, '_usernameChanged');
- statusChangedSpy = sandbox.spy(element, '_statusChanged');
- element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
-
- nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
- name => Promise.resolve());
- usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername',
- username => Promise.resolve());
- statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
- status => Promise.resolve());
- });
-
- test('name', done => {
- assert.isTrue(element.nameMutable);
- assert.isFalse(element.hasUnsavedChanges);
-
- element.set('_account.name', 'new name');
-
- assert.isTrue(nameChangedSpy.called);
- assert.isFalse(statusChangedSpy.called);
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isFalse(usernameStub.called);
- assert.isTrue(nameStub.called);
- assert.isFalse(statusStub.called);
- nameStub.lastCall.returnValue.then(() => {
- assert.equal(nameStub.lastCall.args[0], 'new name');
- done();
- });
- });
- });
-
- test('username', done => {
- element.set('_account.username', '');
- element._hasUsernameChange = false;
- assert.isTrue(element.usernameMutable);
-
- element.set('_username', 'new username');
-
- assert.isTrue(usernameChangedSpy.called);
- assert.isFalse(statusChangedSpy.called);
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isTrue(usernameStub.called);
- assert.isFalse(nameStub.called);
- assert.isFalse(statusStub.called);
- usernameStub.lastCall.returnValue.then(() => {
- assert.equal(usernameStub.lastCall.args[0], 'new username');
- done();
- });
- });
- });
-
- test('status', done => {
- assert.isFalse(element.hasUnsavedChanges);
-
- element.set('_account.status', 'new status');
-
- assert.isFalse(nameChangedSpy.called);
- assert.isTrue(statusChangedSpy.called);
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isFalse(usernameStub.called);
- assert.isTrue(statusStub.called);
- assert.isFalse(nameStub.called);
- statusStub.lastCall.returnValue.then(() => {
- assert.equal(statusStub.lastCall.args[0], 'new status');
- done();
- });
- });
- });
- });
-
- suite('edit name and status', () => {
- let nameChangedSpy;
- let statusChangedSpy;
- let nameStub;
- let statusStub;
-
- setup(() => {
- nameChangedSpy = sandbox.spy(element, '_nameChanged');
- statusChangedSpy = sandbox.spy(element, '_statusChanged');
- element.set('_serverConfig',
- {auth: {editable_account_fields: ['FULL_NAME']}});
-
- nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
- name => Promise.resolve());
- statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
- status => Promise.resolve());
- sandbox.stub(element.$.restAPI, 'setAccountUsername',
- username => Promise.resolve());
- });
-
- test('set name and status', done => {
- assert.isTrue(element.nameMutable);
- assert.isFalse(element.hasUnsavedChanges);
-
- element.set('_account.name', 'new name');
-
- assert.isTrue(nameChangedSpy.called);
-
- element.set('_account.status', 'new status');
-
- assert.isTrue(statusChangedSpy.called);
-
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isTrue(statusStub.called);
- assert.isTrue(nameStub.called);
-
+ element.save().then(() => {
+ assert.isFalse(usernameStub.called);
+ assert.isTrue(nameStub.called);
+ assert.isFalse(statusStub.called);
+ nameStub.lastCall.returnValue.then(() => {
assert.equal(nameStub.lastCall.args[0], 'new name');
-
- assert.equal(statusStub.lastCall.args[0], 'new status');
-
done();
});
});
});
- suite('set status but read name', () => {
- let statusChangedSpy;
- let statusStub;
+ test('username', done => {
+ element.set('_account.username', '');
+ element._hasUsernameChange = false;
+ assert.isTrue(element.usernameMutable);
- setup(() => {
- statusChangedSpy = sandbox.spy(element, '_statusChanged');
- element.set('_serverConfig',
- {auth: {editable_account_fields: []}});
+ element.set('_username', 'new username');
- statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
- status => Promise.resolve());
- });
+ assert.isTrue(usernameChangedSpy.called);
+ assert.isFalse(statusChangedSpy.called);
+ assert.isTrue(element.hasUnsavedChanges);
- test('read full name but set status', done => {
- const section = element.$.nameSection;
- const displaySpan = section.querySelectorAll('.value')[0];
- const inputSpan = section.querySelectorAll('.value')[1];
-
- assert.isFalse(element.nameMutable);
-
- assert.isFalse(element.hasUnsavedChanges);
-
- assert.isFalse(displaySpan.hasAttribute('hidden'));
- assert.equal(displaySpan.textContent, account.name);
- assert.isTrue(inputSpan.hasAttribute('hidden'));
-
- element.set('_account.status', 'new status');
-
- assert.isTrue(statusChangedSpy.called);
-
- assert.isTrue(element.hasUnsavedChanges);
-
- element.save().then(() => {
- assert.isTrue(statusStub.called);
- statusStub.lastCall.returnValue.then(() => {
- assert.equal(statusStub.lastCall.args[0], 'new status');
- done();
- });
+ element.save().then(() => {
+ assert.isTrue(usernameStub.called);
+ assert.isFalse(nameStub.called);
+ assert.isFalse(statusStub.called);
+ usernameStub.lastCall.returnValue.then(() => {
+ assert.equal(usernameStub.lastCall.args[0], 'new username');
+ done();
});
});
});
- test('_usernameChanged compares usernames with loose equality', () => {
- element._account = {};
- element._username = '';
- element._hasUsernameChange = false;
- element._loading = false;
- // _usernameChanged is an observer, but call it here after setting
- // _hasUsernameChange in the test to force recomputation.
- element._usernameChanged();
- flushAsynchronousOperations();
+ test('status', done => {
+ assert.isFalse(element.hasUnsavedChanges);
- assert.isFalse(element._hasUsernameChange);
+ element.set('_account.status', 'new status');
- element.set('_username', 'test');
- flushAsynchronousOperations();
+ assert.isFalse(nameChangedSpy.called);
+ assert.isTrue(statusChangedSpy.called);
+ assert.isTrue(element.hasUnsavedChanges);
- assert.isTrue(element._hasUsernameChange);
- });
-
- test('_hideAvatarChangeUrl', () => {
- assert.equal(element._hideAvatarChangeUrl(''), 'hide');
-
- assert.equal(element._hideAvatarChangeUrl('https://example.com'), '');
+ element.save().then(() => {
+ assert.isFalse(usernameStub.called);
+ assert.isTrue(statusStub.called);
+ assert.isFalse(nameStub.called);
+ statusStub.lastCall.returnValue.then(() => {
+ assert.equal(statusStub.lastCall.args[0], 'new status');
+ done();
+ });
+ });
});
});
+
+ suite('edit name and status', () => {
+ let nameChangedSpy;
+ let statusChangedSpy;
+ let nameStub;
+ let statusStub;
+
+ setup(() => {
+ nameChangedSpy = sandbox.spy(element, '_nameChanged');
+ statusChangedSpy = sandbox.spy(element, '_statusChanged');
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: ['FULL_NAME']}});
+
+ nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+ name => Promise.resolve());
+ statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+ status => Promise.resolve());
+ sandbox.stub(element.$.restAPI, 'setAccountUsername',
+ username => Promise.resolve());
+ });
+
+ test('set name and status', done => {
+ assert.isTrue(element.nameMutable);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ element.set('_account.name', 'new name');
+
+ assert.isTrue(nameChangedSpy.called);
+
+ element.set('_account.status', 'new status');
+
+ assert.isTrue(statusChangedSpy.called);
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ element.save().then(() => {
+ assert.isTrue(statusStub.called);
+ assert.isTrue(nameStub.called);
+
+ assert.equal(nameStub.lastCall.args[0], 'new name');
+
+ assert.equal(statusStub.lastCall.args[0], 'new status');
+
+ done();
+ });
+ });
+ });
+
+ suite('set status but read name', () => {
+ let statusChangedSpy;
+ let statusStub;
+
+ setup(() => {
+ statusChangedSpy = sandbox.spy(element, '_statusChanged');
+ element.set('_serverConfig',
+ {auth: {editable_account_fields: []}});
+
+ statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+ status => Promise.resolve());
+ });
+
+ test('read full name but set status', done => {
+ const section = element.$.nameSection;
+ const displaySpan = section.querySelectorAll('.value')[0];
+ const inputSpan = section.querySelectorAll('.value')[1];
+
+ assert.isFalse(element.nameMutable);
+
+ assert.isFalse(element.hasUnsavedChanges);
+
+ assert.isFalse(displaySpan.hasAttribute('hidden'));
+ assert.equal(displaySpan.textContent, account.name);
+ assert.isTrue(inputSpan.hasAttribute('hidden'));
+
+ element.set('_account.status', 'new status');
+
+ assert.isTrue(statusChangedSpy.called);
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ element.save().then(() => {
+ assert.isTrue(statusStub.called);
+ statusStub.lastCall.returnValue.then(() => {
+ assert.equal(statusStub.lastCall.args[0], 'new status');
+ done();
+ });
+ });
+ });
+ });
+
+ test('_usernameChanged compares usernames with loose equality', () => {
+ element._account = {};
+ element._username = '';
+ element._hasUsernameChange = false;
+ element._loading = false;
+ // _usernameChanged is an observer, but call it here after setting
+ // _hasUsernameChange in the test to force recomputation.
+ element._usernameChanged();
+ flushAsynchronousOperations();
+
+ assert.isFalse(element._hasUsernameChange);
+
+ element.set('_username', 'test');
+ flushAsynchronousOperations();
+
+ assert.isTrue(element._hasUsernameChange);
+ });
+
+ test('_hideAvatarChangeUrl', () => {
+ assert.equal(element._hideAvatarChangeUrl(''), 'hide');
+
+ assert.equal(element._hideAvatarChangeUrl('https://example.com'), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 67dc0c4..18a0419 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -14,46 +14,56 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrAgreementsList extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-agreements-list'; }
+import '../../../scripts/bundled-polymer.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-agreements-list_html.js';
- static get properties() {
- return {
- _agreements: Array,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrAgreementsList extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.loadData();
- }
+ static get is() { return 'gr-agreements-list'; }
- loadData() {
- return this.$.restAPI.getAccountAgreements().then(agreements => {
- this._agreements = agreements;
- });
- }
-
- getUrl() {
- return this.getBaseUrl() + '/settings/new-agreement';
- }
-
- getUrlBase(item) {
- return this.getBaseUrl() + '/' + item;
- }
+ static get properties() {
+ return {
+ _agreements: Array,
+ };
}
- customElements.define(GrAgreementsList.is, GrAgreementsList);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.loadData();
+ }
+
+ loadData() {
+ return this.$.restAPI.getAccountAgreements().then(agreements => {
+ this._agreements = agreements;
+ });
+ }
+
+ getUrl() {
+ return this.getBaseUrl() + '/settings/new-agreement';
+ }
+
+ getUrlBase(item) {
+ return this.getBaseUrl() + '/' + item;
+ }
+}
+
+customElements.define(GrAgreementsList.is, GrAgreementsList);
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
index 74d92d3..4bd9365 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
@@ -1,28 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-agreements-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
#agreements .nameColumn {
min-width: 15em;
@@ -47,7 +41,7 @@
<template is="dom-repeat" items="[[_agreements]]">
<tr>
<td class="nameColumn">
- <a href$="[[getUrlBase(item.url)]]" rel="external">
+ <a href\$="[[getUrlBase(item.url)]]" rel="external">
[[item.name]]
</a>
</td>
@@ -56,9 +50,7 @@
</template>
</tbody>
</table>
- <a href$="[[getUrl()]]">New Contributor Agreement</a>
+ <a href\$="[[getUrl()]]">New Contributor Agreement</a>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-agreements-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
index 39b8663..4aa917b 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_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-settings-view</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-agreements-list.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-agreements-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-agreements-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,38 +40,41 @@
</template>
</test-fixture>
-<script>
- suite('gr-agreements-list tests', async () => {
- await readyToTest();
- let element;
- let agreements;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-agreements-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-agreements-list tests', () => {
+ let element;
+ let agreements;
- setup(done => {
- agreements = [{
- url: 'some url',
- description: 'Agreements 1 description',
- name: 'Agreements 1',
- }];
+ setup(done => {
+ agreements = [{
+ url: 'some url',
+ description: 'Agreements 1 description',
+ name: 'Agreements 1',
+ }];
- stub('gr-rest-api-interface', {
- getAccountAgreements() { return Promise.resolve(agreements); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountAgreements() { return Promise.resolve(agreements); },
});
- test('renders', () => {
- const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 1);
-
- const nameCells = Array.from(rows).map(row =>
- row.querySelectorAll('td')[0].textContent.trim()
- );
-
- assert.equal(nameCells[0], 'Agreements 1');
- });
+ element.loadData().then(() => { flush(done); });
});
+
+ test('renders', () => {
+ const rows = dom(element.root).querySelectorAll('tbody tr');
+
+ assert.equal(rows.length, 1);
+
+ const nameCells = Array.from(rows).map(row =>
+ row.querySelectorAll('td')[0].textContent.trim()
+ );
+
+ assert.equal(nameCells[0], 'Agreements 1');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 8521126..85c34a4 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -14,72 +14,85 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
- /**
- * @appliesMixin Gerrit.ChangeTableMixin
- * @extends Polymer.Element
- */
- class GrChangeTableEditor extends Polymer.mixinBehaviors( [
- Gerrit.ChangeTableBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-change-table-editor'; }
+import '../../../scripts/bundled-polymer.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-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-change-table-editor_html.js';
- static get properties() {
- return {
- displayedColumns: {
- type: Array,
- notify: true,
- },
- showNumber: {
- type: Boolean,
- notify: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @extends Polymer.Element
+ */
+class GrChangeTableEditor extends mixinBehaviors( [
+ Gerrit.ChangeTableBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * Get the list of enabled column names from whichever checkboxes are
- * checked (excluding the number checkbox).
- *
- * @return {!Array<string>}
- */
- _getDisplayedColumns() {
- return Array.from(Polymer.dom(this.root)
- .querySelectorAll('.checkboxContainer input:not([name=number])'))
- .filter(checkbox => checkbox.checked)
- .map(checkbox => checkbox.name);
- }
+ static get is() { return 'gr-change-table-editor'; }
- /**
- * Handle a click on a checkbox container and relay the click to the checkbox it
- * contains.
- */
- _handleCheckboxContainerClick(e) {
- const checkbox = Polymer.dom(e.target).querySelector('input');
- if (!checkbox) { return; }
- checkbox.click();
- }
-
- /**
- * Handle a click on the number checkbox and update the showNumber property
- * accordingly.
- */
- _handleNumberCheckboxClick(e) {
- this.showNumber = Polymer.dom(e).rootTarget.checked;
- }
-
- /**
- * Handle a click on a displayed column checkboxes (excluding number) and
- * update the displayedColumns property accordingly.
- */
- _handleTargetClick(e) {
- this.set('displayedColumns', this._getDisplayedColumns());
- }
+ static get properties() {
+ return {
+ displayedColumns: {
+ type: Array,
+ notify: true,
+ },
+ showNumber: {
+ type: Boolean,
+ notify: true,
+ },
+ };
}
- customElements.define(GrChangeTableEditor.is, GrChangeTableEditor);
-})();
+ /**
+ * Get the list of enabled column names from whichever checkboxes are
+ * checked (excluding the number checkbox).
+ *
+ * @return {!Array<string>}
+ */
+ _getDisplayedColumns() {
+ return Array.from(dom(this.root)
+ .querySelectorAll('.checkboxContainer input:not([name=number])'))
+ .filter(checkbox => checkbox.checked)
+ .map(checkbox => checkbox.name);
+ }
+
+ /**
+ * Handle a click on a checkbox container and relay the click to the checkbox it
+ * contains.
+ */
+ _handleCheckboxContainerClick(e) {
+ const checkbox = dom(e.target).querySelector('input');
+ if (!checkbox) { return; }
+ checkbox.click();
+ }
+
+ /**
+ * Handle a click on the number checkbox and update the showNumber property
+ * accordingly.
+ */
+ _handleNumberCheckboxClick(e) {
+ this.showNumber = dom(e).rootTarget.checked;
+ }
+
+ /**
+ * Handle a click on a displayed column checkboxes (excluding number) and
+ * update the displayedColumns property accordingly.
+ */
+ _handleTargetClick(e) {
+ this.set('displayedColumns', this._getDisplayedColumns());
+ }
+}
+
+customElements.define(GrChangeTableEditor.is, GrChangeTableEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js
index 09a9dbc..7aa785c 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_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="../../../behaviors/gr-change-table-behavior/gr-change-table-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-date-formatter/gr-date-formatter.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="../../../styles/gr-form-styles.html">
-
-<dom-module id="gr-change-table-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -56,31 +49,19 @@
<tbody>
<tr>
<td>Number</td>
- <td class="checkboxContainer"
- on-click="_handleCheckboxContainerClick">
- <input
- type="checkbox"
- name="number"
- on-click="_handleNumberCheckboxClick"
- checked$="[[showNumber]]">
+ <td class="checkboxContainer" on-click="_handleCheckboxContainerClick">
+ <input type="checkbox" name="number" on-click="_handleNumberCheckboxClick" checked\$="[[showNumber]]">
</td>
</tr>
<template is="dom-repeat" items="[[columnNames]]">
<tr>
<td>[[item]]</td>
- <td class="checkboxContainer"
- on-click="_handleCheckboxContainerClick">
- <input
- type="checkbox"
- name="[[item]]"
- on-click="_handleTargetClick"
- checked$="[[!isColumnHidden(item, displayedColumns)]]">
+ <td class="checkboxContainer" on-click="_handleCheckboxContainerClick">
+ <input type="checkbox" name="[[item]]" on-click="_handleTargetClick" checked\$="[[!isColumnHidden(item, displayedColumns)]]">
</td>
</tr>
</template>
</tbody>
</table>
</div>
- </template>
- <script src="gr-change-table-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 460d6bc..1d60384 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_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-settings-view</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-change-table-editor.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-change-table-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-table-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,138 +40,140 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-table-editor tests', async () => {
- await readyToTest();
- let element;
- let columns;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-table-editor.js';
+suite('gr-change-table-editor tests', () => {
+ let element;
+ let columns;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
- columns = [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- ];
+ columns = [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ ];
- element.set('displayedColumns', columns);
- element.showNumber = false;
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('tbody').querySelectorAll('tr');
- let tds;
-
- // The `+ 1` is for the number column, which isn't included in the change
- // table behavior's list.
- assert.equal(rows.length, element.columnNames.length + 1);
- for (let i = 0; i < columns.length; i++) {
- tds = rows[i + 1].querySelectorAll('td');
- assert.equal(tds[0].textContent, columns[i]);
- }
- });
-
- test('hide item', () => {
- const checkbox = element.shadowRoot
- .querySelector('table tr:nth-child(2) input');
- const isChecked = checkbox.checked;
- const displayedLength = element.displayedColumns.length;
- assert.isTrue(isChecked);
-
- MockInteractions.tap(checkbox);
- flushAsynchronousOperations();
-
- assert.equal(element.displayedColumns.length, displayedLength - 1);
- });
-
- test('show item', () => {
- element.set('displayedColumns', [
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- ]);
- flushAsynchronousOperations();
- const checkbox = element.shadowRoot
- .querySelector('table tr:nth-child(2) input');
- const isChecked = checkbox.checked;
- const displayedLength = element.displayedColumns.length;
- assert.isFalse(isChecked);
- assert.equal(element.shadowRoot
- .querySelector('table').style.display, '');
-
- MockInteractions.tap(checkbox);
- flushAsynchronousOperations();
-
- assert.equal(element.displayedColumns.length,
- displayedLength + 1);
- });
-
- test('_getDisplayedColumns', () => {
- assert.deepEqual(element._getDisplayedColumns(), columns);
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('.checkboxContainer input[name=Assignee]'));
- assert.deepEqual(element._getDisplayedColumns(),
- columns.filter(c => c !== 'Assignee'));
- });
-
- test('_handleCheckboxContainerClick relayes taps to checkboxes', () => {
- sandbox.stub(element, '_handleNumberCheckboxClick');
- sandbox.stub(element, '_handleTargetClick');
-
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('table tr:first-of-type .checkboxContainer'));
- assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
- assert.isFalse(element._handleTargetClick.called);
-
- MockInteractions.tap(
- element.shadowRoot
- .querySelector('table tr:last-of-type .checkboxContainer'));
- assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
- assert.isTrue(element._handleTargetClick.calledOnce);
- });
-
- test('_handleNumberCheckboxClick', () => {
- sandbox.spy(element, '_handleNumberCheckboxClick');
-
- MockInteractions
- .tap(element.shadowRoot
- .querySelector('.checkboxContainer input[name=number]'));
- assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
- assert.isTrue(element.showNumber);
-
- MockInteractions
- .tap(element.shadowRoot
- .querySelector('.checkboxContainer input[name=number]'));
- assert.isTrue(element._handleNumberCheckboxClick.calledTwice);
- assert.isFalse(element.showNumber);
- });
-
- test('_handleTargetClick', () => {
- sandbox.spy(element, '_handleTargetClick');
- assert.include(element.displayedColumns, 'Assignee');
- MockInteractions
- .tap(element.shadowRoot
- .querySelector('.checkboxContainer input[name=Assignee]'));
- assert.isTrue(element._handleTargetClick.calledOnce);
- assert.notInclude(element.displayedColumns, 'Assignee');
- });
+ element.set('displayedColumns', columns);
+ element.showNumber = false;
+ flushAsynchronousOperations();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('tbody').querySelectorAll('tr');
+ let tds;
+
+ // The `+ 1` is for the number column, which isn't included in the change
+ // table behavior's list.
+ assert.equal(rows.length, element.columnNames.length + 1);
+ for (let i = 0; i < columns.length; i++) {
+ tds = rows[i + 1].querySelectorAll('td');
+ assert.equal(tds[0].textContent, columns[i]);
+ }
+ });
+
+ test('hide item', () => {
+ const checkbox = element.shadowRoot
+ .querySelector('table tr:nth-child(2) input');
+ const isChecked = checkbox.checked;
+ const displayedLength = element.displayedColumns.length;
+ assert.isTrue(isChecked);
+
+ MockInteractions.tap(checkbox);
+ flushAsynchronousOperations();
+
+ assert.equal(element.displayedColumns.length, displayedLength - 1);
+ });
+
+ test('show item', () => {
+ element.set('displayedColumns', [
+ 'Status',
+ 'Owner',
+ 'Assignee',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ ]);
+ flushAsynchronousOperations();
+ const checkbox = element.shadowRoot
+ .querySelector('table tr:nth-child(2) input');
+ const isChecked = checkbox.checked;
+ const displayedLength = element.displayedColumns.length;
+ assert.isFalse(isChecked);
+ assert.equal(element.shadowRoot
+ .querySelector('table').style.display, '');
+
+ MockInteractions.tap(checkbox);
+ flushAsynchronousOperations();
+
+ assert.equal(element.displayedColumns.length,
+ displayedLength + 1);
+ });
+
+ test('_getDisplayedColumns', () => {
+ assert.deepEqual(element._getDisplayedColumns(), columns);
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('.checkboxContainer input[name=Assignee]'));
+ assert.deepEqual(element._getDisplayedColumns(),
+ columns.filter(c => c !== 'Assignee'));
+ });
+
+ test('_handleCheckboxContainerClick relayes taps to checkboxes', () => {
+ sandbox.stub(element, '_handleNumberCheckboxClick');
+ sandbox.stub(element, '_handleTargetClick');
+
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('table tr:first-of-type .checkboxContainer'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+ assert.isFalse(element._handleTargetClick.called);
+
+ MockInteractions.tap(
+ element.shadowRoot
+ .querySelector('table tr:last-of-type .checkboxContainer'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+ assert.isTrue(element._handleTargetClick.calledOnce);
+ });
+
+ test('_handleNumberCheckboxClick', () => {
+ sandbox.spy(element, '_handleNumberCheckboxClick');
+
+ MockInteractions
+ .tap(element.shadowRoot
+ .querySelector('.checkboxContainer input[name=number]'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledOnce);
+ assert.isTrue(element.showNumber);
+
+ MockInteractions
+ .tap(element.shadowRoot
+ .querySelector('.checkboxContainer input[name=number]'));
+ assert.isTrue(element._handleNumberCheckboxClick.calledTwice);
+ assert.isFalse(element.showNumber);
+ });
+
+ test('_handleTargetClick', () => {
+ sandbox.spy(element, '_handleTargetClick');
+ assert.include(element.displayedColumns, 'Assignee');
+ MockInteractions
+ .tap(element.shadowRoot
+ .querySelector('.checkboxContainer input[name=Assignee]'));
+ assert.isTrue(element._handleTargetClick.calledOnce);
+ assert.notInclude(element.displayedColumns, 'Assignee');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index cff1d54..373ac63 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -14,151 +14,164 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrClaView extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-cla-view'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-cla-view_html.js';
- static get properties() {
- return {
- _groups: Object,
- /** @type {?} */
- _serverConfig: Object,
- _agreementsText: String,
- _agreementName: String,
- _signedAgreements: Array,
- _showAgreements: {
- type: Boolean,
- value: false,
- },
- _agreementsUrl: String,
- };
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrClaView extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-cla-view'; }
+
+ static get properties() {
+ return {
+ _groups: Object,
+ /** @type {?} */
+ _serverConfig: Object,
+ _agreementsText: String,
+ _agreementName: String,
+ _signedAgreements: Array,
+ _showAgreements: {
+ type: Boolean,
+ value: false,
+ },
+ _agreementsUrl: String,
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.loadData();
+
+ this.fire('title-change', {title: 'New Contributor Agreement'});
+ }
+
+ loadData() {
+ const promises = [];
+ promises.push(this.$.restAPI.getConfig(true).then(config => {
+ this._serverConfig = config;
+ }));
+
+ promises.push(this.$.restAPI.getAccountGroups().then(groups => {
+ this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
+ }));
+
+ promises.push(this.$.restAPI.getAccountAgreements().then(agreements => {
+ this._signedAgreements = agreements || [];
+ }));
+
+ return Promise.all(promises);
+ }
+
+ _getAgreementsUrl(configUrl) {
+ let url;
+ if (!configUrl) {
+ return '';
+ }
+ if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
+ url = configUrl;
+ } else {
+ url = this.getBaseUrl() + '/' + configUrl;
}
- /** @override */
- attached() {
- super.attached();
+ return url;
+ }
+
+ _handleShowAgreement(e) {
+ this._agreementName = e.target.getAttribute('data-name');
+ this._agreementsUrl =
+ this._getAgreementsUrl(e.target.getAttribute('data-url'));
+ this._showAgreements = true;
+ }
+
+ _handleSaveAgreements(e) {
+ this._createToast('Agreement saving...');
+
+ const name = this._agreementName;
+ return this.$.restAPI.saveAccountAgreement({name}).then(res => {
+ let message = 'Agreement failed to be submitted, please try again';
+ if (res.status === 200) {
+ message = 'Agreement has been successfully submited.';
+ }
+ this._createToast(message);
this.loadData();
+ this._agreementsText = '';
+ this._showAgreements = false;
+ });
+ }
- this.fire('title-change', {title: 'New Contributor Agreement'});
- }
+ _createToast(message) {
+ this.dispatchEvent(new CustomEvent(
+ 'show-alert', {detail: {message}, bubbles: true, composed: true}));
+ }
- loadData() {
- const promises = [];
- promises.push(this.$.restAPI.getConfig(true).then(config => {
- this._serverConfig = config;
- }));
+ _computeShowAgreementsClass(agreements) {
+ return agreements ? 'show' : '';
+ }
- promises.push(this.$.restAPI.getAccountGroups().then(groups => {
- this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
- }));
-
- promises.push(this.$.restAPI.getAccountAgreements().then(agreements => {
- this._signedAgreements = agreements || [];
- }));
-
- return Promise.all(promises);
- }
-
- _getAgreementsUrl(configUrl) {
- let url;
- if (!configUrl) {
- return '';
+ _disableAgreements(item, groups, signedAgreements) {
+ if (!groups) return false;
+ for (const group of groups) {
+ if ((item && item.auto_verify_group &&
+ item.auto_verify_group.id === group.id) ||
+ signedAgreements.find(i => i.name === item.name)) {
+ return true;
}
- if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
- url = configUrl;
- } else {
- url = this.getBaseUrl() + '/' + configUrl;
+ }
+ return false;
+ }
+
+ _hideAgreements(item, groups, signedAgreements) {
+ return this._disableAgreements(item, groups, signedAgreements) ?
+ '' : 'hide';
+ }
+
+ _disableAgreementsText(text) {
+ return text.toLowerCase() === 'i agree' ? false : true;
+ }
+
+ // This checks for auto_verify_group,
+ // if specified it returns 'hideAgreementsTextBox' which
+ // then hides the text box and submit button.
+ _computeHideAgreementClass(name, config) {
+ if (!config) return '';
+ for (const key in config) {
+ if (!config.hasOwnProperty(key)) {
+ continue;
}
-
- return url;
- }
-
- _handleShowAgreement(e) {
- this._agreementName = e.target.getAttribute('data-name');
- this._agreementsUrl =
- this._getAgreementsUrl(e.target.getAttribute('data-url'));
- this._showAgreements = true;
- }
-
- _handleSaveAgreements(e) {
- this._createToast('Agreement saving...');
-
- const name = this._agreementName;
- return this.$.restAPI.saveAccountAgreement({name}).then(res => {
- let message = 'Agreement failed to be submitted, please try again';
- if (res.status === 200) {
- message = 'Agreement has been successfully submited.';
- }
- this._createToast(message);
- this.loadData();
- this._agreementsText = '';
- this._showAgreements = false;
- });
- }
-
- _createToast(message) {
- this.dispatchEvent(new CustomEvent(
- 'show-alert', {detail: {message}, bubbles: true, composed: true}));
- }
-
- _computeShowAgreementsClass(agreements) {
- return agreements ? 'show' : '';
- }
-
- _disableAgreements(item, groups, signedAgreements) {
- if (!groups) return false;
- for (const group of groups) {
- if ((item && item.auto_verify_group &&
- item.auto_verify_group.id === group.id) ||
- signedAgreements.find(i => i.name === item.name)) {
- return true;
- }
- }
- return false;
- }
-
- _hideAgreements(item, groups, signedAgreements) {
- return this._disableAgreements(item, groups, signedAgreements) ?
- '' : 'hide';
- }
-
- _disableAgreementsText(text) {
- return text.toLowerCase() === 'i agree' ? false : true;
- }
-
- // This checks for auto_verify_group,
- // if specified it returns 'hideAgreementsTextBox' which
- // then hides the text box and submit button.
- _computeHideAgreementClass(name, config) {
- if (!config) return '';
- for (const key in config) {
- if (!config.hasOwnProperty(key)) {
+ for (const prop in config[key]) {
+ if (!config[key].hasOwnProperty(prop)) {
continue;
}
- for (const prop in config[key]) {
- if (!config[key].hasOwnProperty(prop)) {
- continue;
- }
- if (name === config[key].name &&
- !config[key].auto_verify_group) {
- return 'hideAgreementsTextBox';
- }
+ if (name === config[key].name &&
+ !config[key].auto_verify_group) {
+ return 'hideAgreementsTextBox';
}
}
}
}
+}
- customElements.define(GrClaView.is, GrClaView);
-})();
+customElements.define(GrClaView.is, GrClaView);
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
index fb5d64f..2c2fca0 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
@@ -1,31 +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="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-cla-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
h1 {
margin-bottom: var(--spacing-m);
@@ -74,36 +65,26 @@
<h3>Select an agreement type:</h3>
<template is="dom-repeat" items="[[_serverConfig.auth.contributor_agreements]]">
<span class="contributorAgreementButton">
- <input id$="claNewAgreementsInput[[item.name]]"
- name="claNewAgreementsRadio"
- type="radio"
- data-name$="[[item.name]]"
- data-url$="[[item.url]]"
- on-click="_handleShowAgreement"
- disabled$="[[_disableAgreements(item, _groups, _signedAgreements)]]">
+ <input id\$="claNewAgreementsInput[[item.name]]" name="claNewAgreementsRadio" type="radio" data-name\$="[[item.name]]" data-url\$="[[item.url]]" on-click="_handleShowAgreement" disabled\$="[[_disableAgreements(item, _groups, _signedAgreements)]]">
<label id="claNewAgreementsLabel">[[item.name]]</label>
</span>
- <div class$="alreadySubmittedText [[_hideAgreements(item, _groups, _signedAgreements)]]">
+ <div class\$="alreadySubmittedText [[_hideAgreements(item, _groups, _signedAgreements)]]">
Agreement already submitted.
</div>
<div class="agreementsUrl">
[[item.description]]
</div>
</template>
- <div id="claNewAgreement" class$="[[_computeShowAgreementsClass(_showAgreements)]]">
+ <div id="claNewAgreement" class\$="[[_computeShowAgreementsClass(_showAgreements)]]">
<h3 class="smallHeading">Review the agreement:</h3>
<div id="agreementsUrl" class="agreementsUrl">
- <a href$="[[_agreementsUrl]]" target="blank" rel="noopener">
+ <a href\$="[[_agreementsUrl]]" target="blank" rel="noopener">
Please review the agreement.</a>
</div>
- <div class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]">
+ <div class\$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]">
<h3 class="smallHeading">Complete the agreement:</h3>
- <iron-input bind-value="{{_agreementsText}}"
- placeholder="Enter 'I agree' here">
- <input id="input-agreements"
- is="iron-input"
- bind-value="{{_agreementsText}}"
- placeholder="Enter 'I agree' here">
+ <iron-input bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here">
+ <input id="input-agreements" is="iron-input" bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here">
</iron-input>
<gr-button on-click="_handleSaveAgreements" disabled="[[_disableAgreementsText(_agreementsText)]]">
Submit
@@ -112,6 +93,4 @@
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-cla-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
index d40d36d..50f82e2 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_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-cla-view</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-cla-view.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-cla-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-cla-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,162 +40,165 @@
</template>
</test-fixture>
-<script>
- suite('gr-cla-view tests', async () => {
- await readyToTest();
- let element;
- const signedAgreements = [{
- name: 'CLA',
- description: 'Contributor License Agreement',
- url: 'static/cla.html',
- }];
- const auth = {
- name: 'Individual',
- description: 'test-description',
- url: 'static/cla_individual.html',
- auto_verify_group: {
- url: '#/admin/groups/uuid-e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- options: {
- visible_to_all: true,
- },
- group_id: 20,
- owner: 'CLA Accepted - Individual',
- owner_id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- created_on: '2017-07-31 15:11:04.000000000',
- id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- name: 'CLA Accepted - Individual',
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-cla-view.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-cla-view tests', () => {
+ let element;
+ const signedAgreements = [{
+ name: 'CLA',
+ description: 'Contributor License Agreement',
+ url: 'static/cla.html',
+ }];
+ const auth = {
+ name: 'Individual',
+ description: 'test-description',
+ url: 'static/cla_individual.html',
+ auto_verify_group: {
+ url: '#/admin/groups/uuid-e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+ options: {
+ visible_to_all: true,
},
- };
-
- const auth2 = {
- name: 'Individual2',
- description: 'test-description2',
- url: 'static/cla_individual2.html',
- auto_verify_group: {
- url: '#/admin/groups/uuid-bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
- options: {},
- group_id: 21,
- owner: 'CLA Accepted - Individual2',
- owner_id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
- created_on: '2017-07-31 15:25:42.000000000',
- id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
- name: 'CLA Accepted - Individual2',
- },
- };
-
- const auth3 = {
- name: 'CLA',
- description: 'Contributor License Agreement',
- url: 'static/cla_individual.html',
- };
-
- const config = {
- auth: {
- use_contributor_agreements: true,
- contributor_agreements: [
- {
- name: 'Individual',
- description: 'test-description',
- url: 'static/cla_individual.html',
- },
- {
- name: 'CLA',
- description: 'Contributor License Agreement',
- url: 'static/cla.html',
- }],
- },
- };
- const config2 = {
- auth: {
- use_contributor_agreements: true,
- contributor_agreements: [
- {
- name: 'Individual2',
- description: 'test-description2',
- url: 'static/cla_individual2.html',
- },
- ],
- },
- };
- const groups = [{
- options: {visible_to_all: true},
+ group_id: 20,
+ owner: 'CLA Accepted - Individual',
+ owner_id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+ created_on: '2017-07-31 15:11:04.000000000',
id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
- group_id: 3,
name: 'CLA Accepted - Individual',
},
- ];
+ };
- setup(done => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve(config); },
- getAccountGroups() { return Promise.resolve(groups); },
- getAccountAgreements() { return Promise.resolve(signedAgreements); },
- });
- element = fixture('basic');
- element.loadData().then(() => { flush(done); });
- });
+ const auth2 = {
+ name: 'Individual2',
+ description: 'test-description2',
+ url: 'static/cla_individual2.html',
+ auto_verify_group: {
+ url: '#/admin/groups/uuid-bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+ options: {},
+ group_id: 21,
+ owner: 'CLA Accepted - Individual2',
+ owner_id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+ created_on: '2017-07-31 15:25:42.000000000',
+ id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb',
+ name: 'CLA Accepted - Individual2',
+ },
+ };
- test('renders as expected with signed agreement', () => {
- const agreementSections = Polymer.dom(element.root)
- .querySelectorAll('.contributorAgreementButton');
- const agreementSubmittedTexts = Polymer.dom(element.root)
- .querySelectorAll('.alreadySubmittedText');
- assert.equal(agreementSections.length, 2);
- assert.isFalse(agreementSections[0].querySelector('input').disabled);
- assert.equal(getComputedStyle(agreementSubmittedTexts[0]).display,
- 'none');
- assert.isTrue(agreementSections[1].querySelector('input').disabled);
- assert.notEqual(getComputedStyle(agreementSubmittedTexts[1]).display,
- 'none');
- });
+ const auth3 = {
+ name: 'CLA',
+ description: 'Contributor License Agreement',
+ url: 'static/cla_individual.html',
+ };
- test('_disableAgreements', () => {
- // In the auto verify group and have not yet signed agreement
- assert.isTrue(
- element._disableAgreements(auth, groups, signedAgreements));
- // Not in the auto verify group and have not yet signed agreement
- assert.isFalse(
- element._disableAgreements(auth2, groups, signedAgreements));
- // Not in the auto verify group, have signed agreement
- assert.isTrue(
- element._disableAgreements(auth3, groups, signedAgreements));
- // Make sure the undefined check works
- assert.isFalse(
- element._disableAgreements(auth, undefined, signedAgreements));
- });
+ const config = {
+ auth: {
+ use_contributor_agreements: true,
+ contributor_agreements: [
+ {
+ name: 'Individual',
+ description: 'test-description',
+ url: 'static/cla_individual.html',
+ },
+ {
+ name: 'CLA',
+ description: 'Contributor License Agreement',
+ url: 'static/cla.html',
+ }],
+ },
+ };
+ const config2 = {
+ auth: {
+ use_contributor_agreements: true,
+ contributor_agreements: [
+ {
+ name: 'Individual2',
+ description: 'test-description2',
+ url: 'static/cla_individual2.html',
+ },
+ ],
+ },
+ };
+ const groups = [{
+ options: {visible_to_all: true},
+ id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0',
+ group_id: 3,
+ name: 'CLA Accepted - Individual',
+ },
+ ];
- test('_hideAgreements', () => {
- // Not in the auto verify group and have not yet signed agreement
- assert.equal(
- element._hideAgreements(auth, groups, signedAgreements), '');
- // In the auto verify group
- assert.equal(
- element._hideAgreements(auth2, groups, signedAgreements), 'hide');
- // Not in the auto verify group, have signed agreement
- assert.equal(
- element._hideAgreements(auth3, groups, signedAgreements), '');
+ setup(done => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve(config); },
+ getAccountGroups() { return Promise.resolve(groups); },
+ getAccountAgreements() { return Promise.resolve(signedAgreements); },
});
-
- test('_disableAgreementsText', () => {
- assert.isFalse(element._disableAgreementsText('I AGREE'));
- assert.isTrue(element._disableAgreementsText('I DO NOT AGREE'));
- });
-
- test('_computeHideAgreementClass', () => {
- assert.equal(
- element._computeHideAgreementClass(
- auth.name, config.auth.contributor_agreements),
- 'hideAgreementsTextBox');
- assert.isUndefined(
- element._computeHideAgreementClass(
- auth.name, config2.auth.contributor_agreements));
- });
-
- test('_getAgreementsUrl', () => {
- assert.equal(element._getAgreementsUrl(
- 'http://test.org/test.html'), 'http://test.org/test.html');
- assert.equal(element._getAgreementsUrl(
- 'test_cla.html'), '/test_cla.html');
- });
+ element = fixture('basic');
+ element.loadData().then(() => { flush(done); });
});
+
+ test('renders as expected with signed agreement', () => {
+ const agreementSections = dom(element.root)
+ .querySelectorAll('.contributorAgreementButton');
+ const agreementSubmittedTexts = dom(element.root)
+ .querySelectorAll('.alreadySubmittedText');
+ assert.equal(agreementSections.length, 2);
+ assert.isFalse(agreementSections[0].querySelector('input').disabled);
+ assert.equal(getComputedStyle(agreementSubmittedTexts[0]).display,
+ 'none');
+ assert.isTrue(agreementSections[1].querySelector('input').disabled);
+ assert.notEqual(getComputedStyle(agreementSubmittedTexts[1]).display,
+ 'none');
+ });
+
+ test('_disableAgreements', () => {
+ // In the auto verify group and have not yet signed agreement
+ assert.isTrue(
+ element._disableAgreements(auth, groups, signedAgreements));
+ // Not in the auto verify group and have not yet signed agreement
+ assert.isFalse(
+ element._disableAgreements(auth2, groups, signedAgreements));
+ // Not in the auto verify group, have signed agreement
+ assert.isTrue(
+ element._disableAgreements(auth3, groups, signedAgreements));
+ // Make sure the undefined check works
+ assert.isFalse(
+ element._disableAgreements(auth, undefined, signedAgreements));
+ });
+
+ test('_hideAgreements', () => {
+ // Not in the auto verify group and have not yet signed agreement
+ assert.equal(
+ element._hideAgreements(auth, groups, signedAgreements), '');
+ // In the auto verify group
+ assert.equal(
+ element._hideAgreements(auth2, groups, signedAgreements), 'hide');
+ // Not in the auto verify group, have signed agreement
+ assert.equal(
+ element._hideAgreements(auth3, groups, signedAgreements), '');
+ });
+
+ test('_disableAgreementsText', () => {
+ assert.isFalse(element._disableAgreementsText('I AGREE'));
+ assert.isTrue(element._disableAgreementsText('I DO NOT AGREE'));
+ });
+
+ test('_computeHideAgreementClass', () => {
+ assert.equal(
+ element._computeHideAgreementClass(
+ auth.name, config.auth.contributor_agreements),
+ 'hideAgreementsTextBox');
+ assert.isUndefined(
+ element._computeHideAgreementClass(
+ auth.name, config2.auth.contributor_agreements));
+ });
+
+ test('_getAgreementsUrl', () => {
+ assert.equal(element._getAgreementsUrl(
+ 'http://test.org/test.html'), 'http://test.org/test.html');
+ assert.equal(element._getAgreementsUrl(
+ 'test_cla.html'), '/test_cla.html');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 9523136..2a7ac06 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -14,76 +14,86 @@
* 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 GrEditPreferences extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-edit-preferences'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.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-edit-preferences_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrEditPreferences extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @type {?} */
- editPrefs: Object,
- };
- }
+ static get is() { return 'gr-edit-preferences'; }
- loadData() {
- return this.$.restAPI.getEditPreferences().then(prefs => {
- this.editPrefs = prefs;
- });
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
- _handleEditPrefsChanged() {
- this.hasUnsavedChanges = true;
- }
-
- _handleEditSyntaxHighlightingChanged() {
- this.set('editPrefs.syntax_highlighting',
- this.$.editSyntaxHighlighting.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleEditShowTabsChanged() {
- this.set('editPrefs.show_tabs', this.$.editShowTabs.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleMatchBracketsChanged() {
- this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleEditLineWrappingChanged() {
- this.set('editPrefs.line_wrapping', this.$.editShowLineWrapping.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleIndentWithTabsChanged() {
- this.set('editPrefs.indent_with_tabs', this.$.showIndentWithTabs.checked);
- this._handleEditPrefsChanged();
- }
-
- _handleAutoCloseBracketsChanged() {
- this.set('editPrefs.auto_close_brackets',
- this.$.showAutoCloseBrackets.checked);
- this._handleEditPrefsChanged();
- }
-
- save() {
- return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => {
- this.hasUnsavedChanges = false;
- });
- }
+ /** @type {?} */
+ editPrefs: Object,
+ };
}
- customElements.define(GrEditPreferences.is, GrEditPreferences);
-})();
+ loadData() {
+ return this.$.restAPI.getEditPreferences().then(prefs => {
+ this.editPrefs = prefs;
+ });
+ }
+
+ _handleEditPrefsChanged() {
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleEditSyntaxHighlightingChanged() {
+ this.set('editPrefs.syntax_highlighting',
+ this.$.editSyntaxHighlighting.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleEditShowTabsChanged() {
+ this.set('editPrefs.show_tabs', this.$.editShowTabs.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleMatchBracketsChanged() {
+ this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleEditLineWrappingChanged() {
+ this.set('editPrefs.line_wrapping', this.$.editShowLineWrapping.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleIndentWithTabsChanged() {
+ this.set('editPrefs.indent_with_tabs', this.$.showIndentWithTabs.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ _handleAutoCloseBracketsChanged() {
+ this.set('editPrefs.auto_close_brackets',
+ this.$.showAutoCloseBrackets.checked);
+ this._handleEditPrefsChanged();
+ }
+
+ save() {
+ return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => {
+ this.hasUnsavedChanges = false;
+ });
+ }
+}
+
+customElements.define(GrEditPreferences.is, GrEditPreferences);
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
index 80440c7..de22dbb 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
@@ -1,29 +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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-<dom-module id="gr-edit-preferences">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -34,128 +27,63 @@
<section>
<span class="title">Tab width</span>
<span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.tab_size}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- <input
- is="iron-input"
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.tab_size}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.tab_size}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.tab_size}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
</iron-input>
</span>
</section>
<section>
<span class="title">Columns</span>
<span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.line_length}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- <input
- is="iron-input"
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.line_length}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.line_length}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.line_length}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
</iron-input>
</span>
</section>
<section>
<span class="title">Indent unit</span>
<span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.indent_unit}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
- <input
- is="iron-input"
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{editPrefs.indent_unit}}"
- on-keypress="_handleEditPrefsChanged"
- on-change="_handleEditPrefsChanged">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.indent_unit}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
+ <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.indent_unit}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged">
</iron-input>
</span>
</section>
<section>
<span class="title">Syntax highlighting</span>
<span class="value">
- <input
- id="editSyntaxHighlighting"
- type="checkbox"
- checked$="[[editPrefs.syntax_highlighting]]"
- on-change="_handleEditSyntaxHighlightingChanged">
+ <input id="editSyntaxHighlighting" type="checkbox" checked\$="[[editPrefs.syntax_highlighting]]" on-change="_handleEditSyntaxHighlightingChanged">
</span>
</section>
<section>
<span class="title">Show tabs</span>
<span class="value">
- <input
- id="editShowTabs"
- type="checkbox"
- checked$="[[editPrefs.show_tabs]]"
- on-change="_handleEditShowTabsChanged">
+ <input id="editShowTabs" type="checkbox" checked\$="[[editPrefs.show_tabs]]" on-change="_handleEditShowTabsChanged">
</span>
</section>
<section>
<span class="title">Match brackets</span>
<span class="value">
- <input
- id="showMatchBrackets"
- type="checkbox"
- checked$="[[editPrefs.match_brackets]]"
- on-change="_handleMatchBracketsChanged">
+ <input id="showMatchBrackets" type="checkbox" checked\$="[[editPrefs.match_brackets]]" on-change="_handleMatchBracketsChanged">
</span>
</section>
<section>
<span class="title">Line wrapping</span>
<span class="value">
- <input
- id="editShowLineWrapping"
- type="checkbox"
- checked$="[[editPrefs.line_wrapping]]"
- on-change="_handleEditLineWrappingChanged">
+ <input id="editShowLineWrapping" type="checkbox" checked\$="[[editPrefs.line_wrapping]]" on-change="_handleEditLineWrappingChanged">
</span>
</section>
<section>
<span class="title">Indent with tabs</span>
<span class="value">
- <input
- id="showIndentWithTabs"
- type="checkbox"
- checked$="[[editPrefs.indent_with_tabs]]"
- on-change="_handleIndentWithTabsChanged">
+ <input id="showIndentWithTabs" type="checkbox" checked\$="[[editPrefs.indent_with_tabs]]" on-change="_handleIndentWithTabsChanged">
</span>
</section>
<section>
<span class="title">Auto close brackets</span>
<span class="value">
- <input
- id="showAutoCloseBrackets"
- type="checkbox"
- checked$="[[editPrefs.auto_close_brackets]]"
- on-change="_handleAutoCloseBracketsChanged">
+ <input id="showAutoCloseBrackets" type="checkbox" checked\$="[[editPrefs.auto_close_brackets]]" on-change="_handleAutoCloseBracketsChanged">
</span>
</section>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-edit-preferences.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
index 3c99977..b73b14f 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_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-edit-preferences</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-edit-preferences.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-edit-preferences.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-edit-preferences.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,95 +40,97 @@
</template>
</test-fixture>
-<script>
- suite('gr-edit-preferences tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let editPreferences;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-edit-preferences.js';
+suite('gr-edit-preferences tests', () => {
+ let element;
+ let sandbox;
+ let editPreferences;
- function valueOf(title, fieldsetid) {
- const sections = element.$[fieldsetid].querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent.trim() === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title, fieldsetid) {
+ const sections = element.$[fieldsetid].querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent.trim() === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- setup(() => {
- editPreferences = {
- auto_close_brackets: false,
- cursor_blink_rate: 0,
- hide_line_numbers: false,
- hide_top_menu: false,
- indent_unit: 2,
- indent_with_tabs: false,
- key_map_type: 'DEFAULT',
- line_length: 100,
- line_wrapping: false,
- match_brackets: true,
- show_base: false,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- };
+ setup(() => {
+ editPreferences = {
+ auto_close_brackets: false,
+ cursor_blink_rate: 0,
+ hide_line_numbers: false,
+ hide_top_menu: false,
+ indent_unit: 2,
+ indent_with_tabs: false,
+ key_map_type: 'DEFAULT',
+ line_length: 100,
+ line_wrapping: false,
+ match_brackets: true,
+ show_base: false,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ };
- stub('gr-rest-api-interface', {
- getEditPreferences() {
- return Promise.resolve(editPreferences);
- },
- });
-
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- return element.loadData();
+ stub('gr-rest-api-interface', {
+ getEditPreferences() {
+ return Promise.resolve(editPreferences);
+ },
});
- teardown(() => { sandbox.restore(); });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ return element.loadData();
+ });
- test('renders', () => {
- // Rendered with the expected preferences selected.
- assert.equal(valueOf('Tab width', 'editPreferences')
- .firstElementChild.bindValue, editPreferences.tab_size);
- assert.equal(valueOf('Columns', 'editPreferences')
- .firstElementChild.bindValue, editPreferences.line_length);
- assert.equal(valueOf('Indent unit', 'editPreferences')
- .firstElementChild.bindValue, editPreferences.indent_unit);
- assert.equal(valueOf('Syntax highlighting', 'editPreferences')
- .firstElementChild.checked, editPreferences.syntax_highlighting);
- assert.equal(valueOf('Show tabs', 'editPreferences')
- .firstElementChild.checked, editPreferences.show_tabs);
- assert.equal(valueOf('Match brackets', 'editPreferences')
- .firstElementChild.checked, editPreferences.match_brackets);
- assert.equal(valueOf('Line wrapping', 'editPreferences')
- .firstElementChild.checked, editPreferences.line_wrapping);
- assert.equal(valueOf('Indent with tabs', 'editPreferences')
- .firstElementChild.checked, editPreferences.indent_with_tabs);
- assert.equal(valueOf('Auto close brackets', 'editPreferences')
- .firstElementChild.checked, editPreferences.auto_close_brackets);
+ teardown(() => { sandbox.restore(); });
+ test('renders', () => {
+ // Rendered with the expected preferences selected.
+ assert.equal(valueOf('Tab width', 'editPreferences')
+ .firstElementChild.bindValue, editPreferences.tab_size);
+ assert.equal(valueOf('Columns', 'editPreferences')
+ .firstElementChild.bindValue, editPreferences.line_length);
+ assert.equal(valueOf('Indent unit', 'editPreferences')
+ .firstElementChild.bindValue, editPreferences.indent_unit);
+ assert.equal(valueOf('Syntax highlighting', 'editPreferences')
+ .firstElementChild.checked, editPreferences.syntax_highlighting);
+ assert.equal(valueOf('Show tabs', 'editPreferences')
+ .firstElementChild.checked, editPreferences.show_tabs);
+ assert.equal(valueOf('Match brackets', 'editPreferences')
+ .firstElementChild.checked, editPreferences.match_brackets);
+ assert.equal(valueOf('Line wrapping', 'editPreferences')
+ .firstElementChild.checked, editPreferences.line_wrapping);
+ assert.equal(valueOf('Indent with tabs', 'editPreferences')
+ .firstElementChild.checked, editPreferences.indent_with_tabs);
+ assert.equal(valueOf('Auto close brackets', 'editPreferences')
+ .firstElementChild.checked, editPreferences.auto_close_brackets);
+
+ assert.isFalse(element.hasUnsavedChanges);
+ });
+
+ test('save changes', () => {
+ sandbox.stub(element.$.restAPI, 'saveEditPreferences')
+ .returns(Promise.resolve());
+ const showTabsCheckbox = valueOf('Show tabs', 'editPreferences')
+ .firstElementChild;
+ showTabsCheckbox.checked = false;
+ element._handleEditShowTabsChanged();
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ // Save the change.
+ return element.save().then(() => {
assert.isFalse(element.hasUnsavedChanges);
});
-
- test('save changes', () => {
- sandbox.stub(element.$.restAPI, 'saveEditPreferences')
- .returns(Promise.resolve());
- const showTabsCheckbox = valueOf('Show tabs', 'editPreferences')
- .firstElementChild;
- showTabsCheckbox.checked = false;
- element._handleEditShowTabsChanged();
-
- assert.isTrue(element.hasUnsavedChanges);
-
- // Save the change.
- return element.save().then(() => {
- assert.isFalse(element.hasUnsavedChanges);
- });
- });
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index c60568c..fc97079 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -14,89 +14,99 @@
* 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 GrEmailEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-email-editor'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../shared/gr-button/gr-button.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 {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-email-editor_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrEmailEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _emails: Array,
- _emailsToRemove: {
- type: Array,
- value() { return []; },
- },
- /** @type {?string} */
- _newPreferred: {
- type: String,
- value: null,
- },
- };
+ static get is() { return 'gr-email-editor'; }
+
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
+
+ _emails: Array,
+ _emailsToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ /** @type {?string} */
+ _newPreferred: {
+ type: String,
+ value: null,
+ },
+ };
+ }
+
+ loadData() {
+ return this.$.restAPI.getAccountEmails().then(emails => {
+ this._emails = emails;
+ });
+ }
+
+ save() {
+ const promises = [];
+
+ for (const emailObj of this._emailsToRemove) {
+ promises.push(this.$.restAPI.deleteAccountEmail(emailObj.email));
}
- loadData() {
- return this.$.restAPI.getAccountEmails().then(emails => {
- this._emails = emails;
- });
+ if (this._newPreferred) {
+ promises.push(this.$.restAPI.setPreferredAccountEmail(
+ this._newPreferred));
}
- save() {
- const promises = [];
+ return Promise.all(promises).then(() => {
+ this._emailsToRemove = [];
+ this._newPreferred = null;
+ this.hasUnsavedChanges = false;
+ });
+ }
- for (const emailObj of this._emailsToRemove) {
- promises.push(this.$.restAPI.deleteAccountEmail(emailObj.email));
- }
+ _handleDeleteButton(e) {
+ const index = parseInt(dom(e).localTarget
+ .getAttribute('data-index'), 10);
+ const email = this._emails[index];
+ this.push('_emailsToRemove', email);
+ this.splice('_emails', index, 1);
+ this.hasUnsavedChanges = true;
+ }
- if (this._newPreferred) {
- promises.push(this.$.restAPI.setPreferredAccountEmail(
- this._newPreferred));
- }
-
- return Promise.all(promises).then(() => {
- this._emailsToRemove = [];
- this._newPreferred = null;
- this.hasUnsavedChanges = false;
- });
- }
-
- _handleDeleteButton(e) {
- const index = parseInt(Polymer.dom(e).localTarget
- .getAttribute('data-index'), 10);
- const email = this._emails[index];
- this.push('_emailsToRemove', email);
- this.splice('_emails', index, 1);
- this.hasUnsavedChanges = true;
- }
-
- _handlePreferredControlClick(e) {
- if (e.target.classList.contains('preferredControl')) {
- e.target.firstElementChild.click();
- }
- }
-
- _handlePreferredChange(e) {
- const preferred = e.target.value;
- for (let i = 0; i < this._emails.length; i++) {
- if (preferred === this._emails[i].email) {
- this.set(['_emails', i, 'preferred'], true);
- this._newPreferred = preferred;
- this.hasUnsavedChanges = true;
- } else if (this._emails[i].preferred) {
- this.set(['_emails', i, 'preferred'], false);
- }
- }
+ _handlePreferredControlClick(e) {
+ if (e.target.classList.contains('preferredControl')) {
+ e.target.firstElementChild.click();
}
}
- customElements.define(GrEmailEditor.is, GrEmailEditor);
-})();
+ _handlePreferredChange(e) {
+ const preferred = e.target.value;
+ for (let i = 0; i < this._emails.length; i++) {
+ if (preferred === this._emails[i].email) {
+ this.set(['_emails', i, 'preferred'], true);
+ this._newPreferred = preferred;
+ this.hasUnsavedChanges = true;
+ } else if (this._emails[i].preferred) {
+ this.set(['_emails', i, 'preferred'], false);
+ }
+ }
+ }
+}
+
+customElements.define(GrEmailEditor.is, GrEmailEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
index 041b2a7..b02df3c 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
@@ -1,28 +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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-email-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -65,29 +59,12 @@
<tr>
<td class="emailColumn">[[item.email]]</td>
<td class="preferredControl" on-click="_handlePreferredControlClick">
- <iron-input
- class="preferredRadio"
- type="radio"
- on-change="_handlePreferredChange"
- name="preferred"
- bind-value="[[item.email]]"
- checked$="[[item.preferred]]">
- <input
- is="iron-input"
- class="preferredRadio"
- type="radio"
- on-change="_handlePreferredChange"
- name="preferred"
- value="[[item.email]]"
- checked$="[[item.preferred]]">
+ <iron-input class="preferredRadio" type="radio" on-change="_handlePreferredChange" name="preferred" bind-value="[[item.email]]" checked\$="[[item.preferred]]">
+ <input is="iron-input" class="preferredRadio" type="radio" on-change="_handlePreferredChange" name="preferred" value="[[item.email]]" checked\$="[[item.preferred]]">
</iron-input>
</td>
<td>
- <gr-button
- data-index$="[[index]]"
- on-click="_handleDeleteButton"
- disabled="[[item.preferred]]"
- class="remove-button">Delete</gr-button>
+ <gr-button data-index\$="[[index]]" on-click="_handleDeleteButton" disabled="[[item.preferred]]" class="remove-button">Delete</gr-button>
</td>
</tr>
</template>
@@ -95,6 +72,4 @@
</table>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-email-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
index ecb108d..196f8a9 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_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-email-editor</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-email-editor.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-email-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-email-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,121 +40,123 @@
</template>
</test-fixture>
-<script>
- suite('gr-email-editor tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-email-editor.js';
+suite('gr-email-editor tests', () => {
+ let element;
- setup(done => {
- const emails = [
- {email: 'email@one.com'},
- {email: 'email@two.com', preferred: true},
- {email: 'email@three.com'},
- ];
+ setup(done => {
+ const emails = [
+ {email: 'email@one.com'},
+ {email: 'email@two.com', preferred: true},
+ {email: 'email@three.com'},
+ ];
- stub('gr-rest-api-interface', {
- getAccountEmails() { return Promise.resolve(emails); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(flush(done));
+ stub('gr-rest-api-interface', {
+ getAccountEmails() { return Promise.resolve(emails); },
});
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('table').querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 3);
+ element.loadData().then(flush(done));
+ });
- assert.isFalse(rows[0].querySelector('input[type=radio]').checked);
- assert.isNotOk(rows[0].querySelector('gr-button').disabled);
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('table').querySelectorAll('tbody tr');
- assert.isTrue(rows[1].querySelector('input[type=radio]').checked);
- assert.isOk(rows[1].querySelector('gr-button').disabled);
+ assert.equal(rows.length, 3);
- assert.isFalse(rows[2].querySelector('input[type=radio]').checked);
- assert.isNotOk(rows[2].querySelector('gr-button').disabled);
+ assert.isFalse(rows[0].querySelector('input[type=radio]').checked);
+ assert.isNotOk(rows[0].querySelector('gr-button').disabled);
- assert.isFalse(element.hasUnsavedChanges);
- });
+ assert.isTrue(rows[1].querySelector('input[type=radio]').checked);
+ assert.isOk(rows[1].querySelector('gr-button').disabled);
- test('edit preferred', () => {
- const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange');
- const radios = element.shadowRoot
- .querySelector('table').querySelectorAll('input[type=radio]');
+ assert.isFalse(rows[2].querySelector('input[type=radio]').checked);
+ assert.isNotOk(rows[2].querySelector('gr-button').disabled);
- assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
- assert.isNotOk(radios[0].checked);
- assert.isOk(radios[1].checked);
- assert.isFalse(preferredChangedSpy.called);
+ assert.isFalse(element.hasUnsavedChanges);
+ });
- radios[0].click();
+ test('edit preferred', () => {
+ const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange');
+ const radios = element.shadowRoot
+ .querySelector('table').querySelectorAll('input[type=radio]');
- assert.isTrue(element.hasUnsavedChanges);
- assert.isOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
- assert.isOk(radios[0].checked);
- assert.isNotOk(radios[1].checked);
- assert.isTrue(preferredChangedSpy.called);
- });
+ assert.isFalse(element.hasUnsavedChanges);
+ assert.isNotOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 0);
+ assert.equal(element._emails.length, 3);
+ assert.isNotOk(radios[0].checked);
+ assert.isOk(radios[1].checked);
+ assert.isFalse(preferredChangedSpy.called);
- test('delete email', () => {
- const buttons = element.shadowRoot
- .querySelector('table').querySelectorAll('gr-button');
+ radios[0].click();
- assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.isOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 0);
+ assert.equal(element._emails.length, 3);
+ assert.isOk(radios[0].checked);
+ assert.isNotOk(radios[1].checked);
+ assert.isTrue(preferredChangedSpy.called);
+ });
- buttons[2].click();
+ test('delete email', () => {
+ const buttons = element.shadowRoot
+ .querySelector('table').querySelectorAll('gr-button');
- assert.isTrue(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 1);
- assert.equal(element._emails.length, 2);
+ assert.isFalse(element.hasUnsavedChanges);
+ assert.isNotOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 0);
+ assert.equal(element._emails.length, 3);
- assert.equal(element._emailsToRemove[0].email, 'email@three.com');
- });
+ buttons[2].click();
- test('save changes', done => {
- const deleteEmailStub =
- sinon.stub(element.$.restAPI, 'deleteAccountEmail');
- const setPreferredStub = sinon.stub(element.$.restAPI,
- 'setPreferredAccountEmail');
- const rows = element.shadowRoot
- .querySelector('table').querySelectorAll('tbody tr');
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.isNotOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 1);
+ assert.equal(element._emails.length, 2);
- assert.isFalse(element.hasUnsavedChanges);
- assert.isNotOk(element._newPreferred);
- assert.equal(element._emailsToRemove.length, 0);
- assert.equal(element._emails.length, 3);
+ assert.equal(element._emailsToRemove[0].email, 'email@three.com');
+ });
- // Delete the first email and set the last as preferred.
- rows[0].querySelector('gr-button').click();
- rows[2].querySelector('input[type=radio]').click();
+ test('save changes', done => {
+ const deleteEmailStub =
+ sinon.stub(element.$.restAPI, 'deleteAccountEmail');
+ const setPreferredStub = sinon.stub(element.$.restAPI,
+ 'setPreferredAccountEmail');
+ const rows = element.shadowRoot
+ .querySelector('table').querySelectorAll('tbody tr');
- assert.isTrue(element.hasUnsavedChanges);
- assert.equal(element._newPreferred, 'email@three.com');
- assert.equal(element._emailsToRemove.length, 1);
- assert.equal(element._emailsToRemove[0].email, 'email@one.com');
- assert.equal(element._emails.length, 2);
+ assert.isFalse(element.hasUnsavedChanges);
+ assert.isNotOk(element._newPreferred);
+ assert.equal(element._emailsToRemove.length, 0);
+ assert.equal(element._emails.length, 3);
- // Save the changes.
- element.save().then(() => {
- assert.equal(deleteEmailStub.callCount, 1);
- assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com');
+ // Delete the first email and set the last as preferred.
+ rows[0].querySelector('gr-button').click();
+ rows[2].querySelector('input[type=radio]').click();
- assert.isTrue(setPreferredStub.called);
- assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com');
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.equal(element._newPreferred, 'email@three.com');
+ assert.equal(element._emailsToRemove.length, 1);
+ assert.equal(element._emailsToRemove[0].email, 'email@one.com');
+ assert.equal(element._emails.length, 2);
- done();
- });
+ // Save the changes.
+ element.save().then(() => {
+ assert.equal(deleteEmailStub.callCount, 1);
+ assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com');
+
+ assert.isTrue(setPreferredStub.called);
+ assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com');
+
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
index 9f04915..90631c7 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -14,100 +14,113 @@
* 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 GrGpgEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-gpg-editor'; }
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-overlay/gr-overlay.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 {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-gpg-editor_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- value: false,
- notify: true,
- },
- _keys: Array,
- /** @type {?} */
- _keyToView: Object,
- _newKey: {
- type: String,
- value: '',
- },
- _keysToRemove: {
- type: Array,
- value() { return []; },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrGpgEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- loadData() {
- this._keys = [];
- return this.$.restAPI.getAccountGPGKeys().then(keys => {
- if (!keys) {
- return;
- }
- this._keys = Object.keys(keys)
- .map(key => {
- const gpgKey = keys[key];
- gpgKey.id = key;
- return gpgKey;
- });
- });
- }
+ static get is() { return 'gr-gpg-editor'; }
- save() {
- const promises = this._keysToRemove.map(key => {
- this.$.restAPI.deleteAccountGPGKey(key.id);
- });
-
- return Promise.all(promises).then(() => {
- this._keysToRemove = [];
- this.hasUnsavedChanges = false;
- });
- }
-
- _showKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this._keyToView = this._keys[index];
- this.$.viewKeyOverlay.open();
- }
-
- _closeOverlay() {
- this.$.viewKeyOverlay.close();
- }
-
- _handleDeleteKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this.push('_keysToRemove', this._keys[index]);
- this.splice('_keys', index, 1);
- this.hasUnsavedChanges = true;
- }
-
- _handleAddKey() {
- this.$.addButton.disabled = true;
- this.$.newKey.disabled = true;
- return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]})
- .then(key => {
- this.$.newKey.disabled = false;
- this._newKey = '';
- this.loadData();
- })
- .catch(() => {
- this.$.addButton.disabled = false;
- this.$.newKey.disabled = false;
- });
- }
-
- _computeAddButtonDisabled(newKey) {
- return !newKey.length;
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrGpgEditor.is, GrGpgEditor);
-})();
+ loadData() {
+ this._keys = [];
+ return this.$.restAPI.getAccountGPGKeys().then(keys => {
+ if (!keys) {
+ return;
+ }
+ this._keys = Object.keys(keys)
+ .map(key => {
+ const gpgKey = keys[key];
+ gpgKey.id = key;
+ return gpgKey;
+ });
+ });
+ }
+
+ save() {
+ const promises = this._keysToRemove.map(key => {
+ this.$.restAPI.deleteAccountGPGKey(key.id);
+ });
+
+ return Promise.all(promises).then(() => {
+ this._keysToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ }
+
+ _showKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this._keyToView = this._keys[index];
+ this.$.viewKeyOverlay.open();
+ }
+
+ _closeOverlay() {
+ this.$.viewKeyOverlay.close();
+ }
+
+ _handleDeleteKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+ this.splice('_keys', index, 1);
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]})
+ .then(key => {
+ this.$.newKey.disabled = false;
+ this._newKey = '';
+ this.loadData();
+ })
+ .catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ }
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ }
+}
+
+customElements.define(GrGpgEditor.is, GrGpgEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
index 7b8a191..3ec4642 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
@@ -1,31 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-gpg-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -81,29 +72,20 @@
</template>
</td>
<td class="keyHeader">
- <gr-button
- on-click="_showKey"
- data-index$="[[index]]"
- link>Click to View</gr-button>
+ <gr-button on-click="_showKey" data-index\$="[[index]]" link="">Click to View</gr-button>
</td>
<td>
- <gr-copy-clipboard
- has-tooltip
- button-title="Copy GPG public key to clipboard"
- hide-input
- text="[[key.key]]">
+ <gr-copy-clipboard has-tooltip="" button-title="Copy GPG public key to clipboard" hide-input="" text="[[key.key]]">
</gr-copy-clipboard>
</td>
<td>
- <gr-button
- data-index$="[[index]]"
- on-click="_handleDeleteKey">Delete</gr-button>
+ <gr-button data-index\$="[[index]]" on-click="_handleDeleteKey">Delete</gr-button>
</td>
</tr>
</template>
</tbody>
</table>
- <gr-overlay id="viewKeyOverlay" with-backdrop>
+ <gr-overlay id="viewKeyOverlay" with-backdrop="">
<fieldset>
<section>
<span class="title">Status</span>
@@ -114,32 +96,19 @@
<span class="value">[[_keyToView.key]]</span>
</section>
</fieldset>
- <gr-button
- class="closeButton"
- on-click="_closeOverlay">Close</gr-button>
+ <gr-button class="closeButton" on-click="_closeOverlay">Close</gr-button>
</gr-overlay>
- <gr-button
- on-click="save"
- disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button>
+ <gr-button on-click="save" disabled\$="[[!hasUnsavedChanges]]">Save changes</gr-button>
</fieldset>
<fieldset>
<section>
<span class="title">New GPG key</span>
<span class="value">
- <iron-autogrow-textarea
- id="newKey"
- autocomplete="on"
- bind-value="{{_newKey}}"
- placeholder="New GPG Key"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="newKey" autocomplete="on" bind-value="{{_newKey}}" placeholder="New GPG Key"></iron-autogrow-textarea>
</span>
</section>
- <gr-button
- id="addButton"
- disabled$="[[_computeAddButtonDisabled(_newKey)]]"
- on-click="_handleAddKey">Add new GPG key</gr-button>
+ <gr-button id="addButton" disabled\$="[[_computeAddButtonDisabled(_newKey)]]" on-click="_handleAddKey">Add new GPG key</gr-button>
</fieldset>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-gpg-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
index 08c36fe..5c95222 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_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-gpg-editor</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-gpg-editor.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-gpg-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-gpg-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,163 +40,166 @@
</template>
</test-fixture>
-<script>
- suite('gr-gpg-editor tests', async () => {
- await readyToTest();
- let element;
- let keys;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-gpg-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-gpg-editor tests', () => {
+ let element;
+ let keys;
- setup(done => {
- const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
- const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
- keys = {
- AFC8A49B: {
- fingerprint: fingerprint1,
- user_ids: [
- 'John Doe john.doe@example.com',
- ],
- key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
- '\nVersion: BCPG v1.52\n\t<key 1>',
- status: 'TRUSTED',
- problems: [],
- },
- AED9B59C: {
- fingerprint: fingerprint2,
- user_ids: [
- 'Gerrit gerrit@example.com',
- ],
- key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
- '\nVersion: BCPG v1.52\n\t<key 2>',
- status: 'TRUSTED',
- problems: [],
- },
- };
+ setup(done => {
+ const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ keys = {
+ AFC8A49B: {
+ fingerprint: fingerprint1,
+ user_ids: [
+ 'John Doe john.doe@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 1>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ AED9B59C: {
+ fingerprint: fingerprint2,
+ user_ids: [
+ 'Gerrit gerrit@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 2>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
- stub('gr-rest-api-interface', {
- getAccountGPGKeys() { return Promise.resolve(keys); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountGPGKeys() { return Promise.resolve(keys); },
});
- test('renders', () => {
- const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 2);
+ element.loadData().then(() => { flush(done); });
+ });
- let cells = rows[0].querySelectorAll('td');
- assert.equal(cells[0].textContent, 'AFC8A49B');
+ test('renders', () => {
+ const rows = dom(element.root).querySelectorAll('tbody tr');
- cells = rows[1].querySelectorAll('td');
- assert.equal(cells[0].textContent, 'AED9B59C');
- });
+ assert.equal(rows.length, 2);
- test('remove key', done => {
- const lastKey = keys[Object.keys(keys)[1]];
+ let cells = rows[0].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AFC8A49B');
- const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
- () => Promise.resolve());
+ cells = rows[1].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AED9B59C');
+ });
+ test('remove key', done => {
+ const lastKey = keys[Object.keys(keys)[1]];
+
+ const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
+ () => Promise.resolve());
+
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ // Get the delete button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(6) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keys.length, 1);
+ assert.equal(element._keysToRemove.length, 1);
+ assert.equal(element._keysToRemove[0], lastKey);
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.isFalse(saveStub.called);
+
+ element.save().then(() => {
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
assert.equal(element._keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
-
- // Get the delete button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(6) gr-button');
-
- MockInteractions.tap(button);
-
- assert.equal(element._keys.length, 1);
- assert.equal(element._keysToRemove.length, 1);
- assert.equal(element._keysToRemove[0], lastKey);
- assert.isTrue(element.hasUnsavedChanges);
- assert.isFalse(saveStub.called);
-
- element.save().then(() => {
- assert.isTrue(saveStub.called);
- assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
- assert.equal(element._keysToRemove.length, 0);
- assert.isFalse(element.hasUnsavedChanges);
- done();
- });
- });
-
- test('show key', () => {
- const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
-
- // Get the show button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(4) gr-button');
-
- MockInteractions.tap(button);
-
- assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
- assert.isTrue(openSpy.called);
- });
-
- test('add key', done => {
- const newKeyString =
- '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
- '\nVersion: BCPG v1.52\n\t<key 3>';
- const newKeyObject = {
- ADE8A59B: {
- fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B',
- user_ids: [
- 'John john@example.com',
- ],
- key: newKeyString,
- status: 'TRUSTED',
- problems: [],
- },
- };
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
- () => Promise.resolve(newKeyObject));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isTrue(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
- });
-
- test('add invalid key', done => {
- const newKeyString = 'not even close to valid';
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
- () => Promise.reject(new Error('error')));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ done();
});
});
+
+ test('show key', () => {
+ const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+
+ // Get the show button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(4) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
+ assert.isTrue(openSpy.called);
+ });
+
+ test('add key', done => {
+ const newKeyString =
+ '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 3>';
+ const newKeyObject = {
+ ADE8A59B: {
+ fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B',
+ user_ids: [
+ 'John john@example.com',
+ ],
+ key: newKeyString,
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => Promise.resolve(newKeyObject));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+
+ test('add invalid key', done => {
+ const newKeyString = 'not even close to valid';
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => Promise.reject(new Error('error')));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index c7b5faa..01739cd 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -14,39 +14,48 @@
* 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 GrGroupList extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-group-list'; }
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.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-group-list_html.js';
- static get properties() {
- return {
- _groups: Array,
- };
- }
+/** @extends Polymer.Element */
+class GrGroupList extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- loadData() {
- return this.$.restAPI.getAccountGroups().then(groups => {
- this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
- });
- }
+ static get is() { return 'gr-group-list'; }
- _computeVisibleToAll(group) {
- return group.options.visible_to_all ? 'Yes' : 'No';
- }
-
- _computeGroupPath(group) {
- if (!group || !group.id) { return; }
-
- // Group ID is already encoded from the API
- // Decode it here to match with our router encoding behavior
- return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id));
- }
+ static get properties() {
+ return {
+ _groups: Array,
+ };
}
- customElements.define(GrGroupList.is, GrGroupList);
-})();
+ loadData() {
+ return this.$.restAPI.getAccountGroups().then(groups => {
+ this._groups = groups.sort((a, b) => a.name.localeCompare(b.name));
+ });
+ }
+
+ _computeVisibleToAll(group) {
+ return group.options.visible_to_all ? 'Yes' : 'No';
+ }
+
+ _computeGroupPath(group) {
+ if (!group || !group.id) { return; }
+
+ // Group ID is already encoded from the API
+ // Decode it here to match with our router encoding behavior
+ return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id));
+ }
+}
+
+customElements.define(GrGroupList.is, GrGroupList);
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
index e51294d..ddacd31 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
@@ -1,28 +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="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-form-styles.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">
-
-<dom-module id="gr-group-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -52,7 +46,7 @@
<template is="dom-repeat" items="[[_groups]]">
<tr>
<td class="nameColumn">
- <a href$="[[_computeGroupPath(item)]]">
+ <a href\$="[[_computeGroupPath(item)]]">
[[item.name]]
</a>
</td>
@@ -64,6 +58,4 @@
</table>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-group-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
index 10b67ec..205b413 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_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-settings-view</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-group-list.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-group-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-group-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,81 +40,84 @@
</template>
</test-fixture>
-<script>
- suite('gr-group-list tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
- let groups;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-group-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-group-list tests', () => {
+ let sandbox;
+ let element;
+ let groups;
- setup(done => {
- sandbox = sinon.sandbox.create();
- groups = [{
- url: 'some url',
- options: {},
- description: 'Group 1 description',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '123',
- id: 'abc',
- name: 'Group 1',
- }, {
- options: {visible_to_all: true},
- id: '456',
- name: 'Group 2',
- }, {
- options: {},
- id: '789',
- name: 'Group 3',
- }];
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ groups = [{
+ url: 'some url',
+ options: {},
+ description: 'Group 1 description',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '123',
+ id: 'abc',
+ name: 'Group 1',
+ }, {
+ options: {visible_to_all: true},
+ id: '456',
+ name: 'Group 2',
+ }, {
+ options: {},
+ id: '789',
+ name: 'Group 3',
+ }];
- stub('gr-rest-api-interface', {
- getAccountGroups() { return Promise.resolve(groups); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountGroups() { return Promise.resolve(groups); },
});
- teardown(() => { sandbox.restore(); });
+ element = fixture('basic');
- test('renders', () => {
- const rows = Array.from(
- Polymer.dom(element.root).querySelectorAll('tbody tr'));
-
- assert.equal(rows.length, 3);
-
- const nameCells = rows.map(row =>
- row.querySelectorAll('td a')[0].textContent.trim()
- );
-
- assert.equal(nameCells[0], 'Group 1');
- assert.equal(nameCells[1], 'Group 2');
- assert.equal(nameCells[2], 'Group 3');
- });
-
- test('_computeVisibleToAll', () => {
- assert.equal(element._computeVisibleToAll(groups[0]), 'No');
- assert.equal(element._computeVisibleToAll(groups[1]), 'Yes');
- });
-
- test('_computeGroupPath', () => {
- sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
- () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
- let group = {
- id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
- };
-
- assert.equal(element._computeGroupPath(group),
- '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
- group = {
- name: 'admin',
- };
-
- assert.isUndefined(element._computeGroupPath(group));
- });
+ element.loadData().then(() => { flush(done); });
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('renders', () => {
+ const rows = Array.from(
+ dom(element.root).querySelectorAll('tbody tr'));
+
+ assert.equal(rows.length, 3);
+
+ const nameCells = rows.map(row =>
+ row.querySelectorAll('td a')[0].textContent.trim()
+ );
+
+ assert.equal(nameCells[0], 'Group 1');
+ assert.equal(nameCells[1], 'Group 2');
+ assert.equal(nameCells[2], 'Group 3');
+ });
+
+ test('_computeVisibleToAll', () => {
+ assert.equal(element._computeVisibleToAll(groups[0]), 'No');
+ assert.equal(element._computeVisibleToAll(groups[1]), 'Yes');
+ });
+
+ test('_computeGroupPath', () => {
+ sandbox.stub(Gerrit.Nav, 'getUrlForGroup',
+ () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+ let group = {
+ id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
+ };
+
+ assert.equal(element._computeGroupPath(group),
+ '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
+
+ group = {
+ name: 'admin',
+ };
+
+ assert.isUndefined(element._computeGroupPath(group));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
index efd0c39..02657f8 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -14,59 +14,70 @@
* 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 GrHttpPassword extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-http-password'; }
+import '../../../styles/gr-form-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.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-http-password_html.js';
- static get properties() {
- return {
- _username: String,
- _generatedPassword: String,
- _passwordUrl: String,
- };
- }
+/** @extends Polymer.Element */
+class GrHttpPassword extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.loadData();
- }
+ static get is() { return 'gr-http-password'; }
- loadData() {
- const promises = [];
-
- promises.push(this.$.restAPI.getAccount().then(account => {
- this._username = account.username;
- }));
-
- promises.push(this.$.restAPI.getConfig().then(info => {
- this._passwordUrl = info.auth.http_password_url || null;
- }));
-
- return Promise.all(promises);
- }
-
- _handleGenerateTap() {
- this._generatedPassword = 'Generating...';
- this.$.generatedPasswordOverlay.open();
- this.$.restAPI.generateAccountHttpPassword().then(newPassword => {
- this._generatedPassword = newPassword;
- });
- }
-
- _closeOverlay() {
- this.$.generatedPasswordOverlay.close();
- }
-
- _generatedPasswordOverlayClosed() {
- this._generatedPassword = '';
- }
+ static get properties() {
+ return {
+ _username: String,
+ _generatedPassword: String,
+ _passwordUrl: String,
+ };
}
- customElements.define(GrHttpPassword.is, GrHttpPassword);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.loadData();
+ }
+
+ loadData() {
+ const promises = [];
+
+ promises.push(this.$.restAPI.getAccount().then(account => {
+ this._username = account.username;
+ }));
+
+ promises.push(this.$.restAPI.getConfig().then(info => {
+ this._passwordUrl = info.auth.http_password_url || null;
+ }));
+
+ return Promise.all(promises);
+ }
+
+ _handleGenerateTap() {
+ this._generatedPassword = 'Generating...';
+ this.$.generatedPasswordOverlay.open();
+ this.$.restAPI.generateAccountHttpPassword().then(newPassword => {
+ this._generatedPassword = newPassword;
+ });
+ }
+
+ _closeOverlay() {
+ this.$.generatedPasswordOverlay.close();
+ }
+
+ _generatedPasswordOverlayClosed() {
+ this._generatedPassword = '';
+ }
+}
+
+customElements.define(GrHttpPassword.is, GrHttpPassword);
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
index 22ba457..b75f56e 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
@@ -1,30 +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="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-http-password">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.password {
font-family: var(--monospace-font-family);
@@ -60,47 +52,33 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<div class="gr-form-styles">
- <div hidden$="[[_passwordUrl]]">
+ <div hidden\$="[[_passwordUrl]]">
<section>
<span class="title">Username</span>
<span class="value">[[_username]]</span>
</section>
- <gr-button
- id="generateButton"
- on-click="_handleGenerateTap">Generate new password</gr-button>
+ <gr-button id="generateButton" on-click="_handleGenerateTap">Generate new password</gr-button>
</div>
- <span hidden$="[[!_passwordUrl]]">
- <a href$="[[_passwordUrl]]" target="_blank" rel="noopener">
+ <span hidden\$="[[!_passwordUrl]]">
+ <a href\$="[[_passwordUrl]]" target="_blank" rel="noopener">
Obtain password</a>
(opens in a new tab)
</span>
</div>
- <gr-overlay
- id="generatedPasswordOverlay"
- on-iron-overlay-closed="_generatedPasswordOverlayClosed"
- with-backdrop>
+ <gr-overlay id="generatedPasswordOverlay" on-iron-overlay-closed="_generatedPasswordOverlayClosed" with-backdrop="">
<div class="gr-form-styles">
<section id="generatedPasswordDisplay">
<span class="title">New Password:</span>
<span class="value">[[_generatedPassword]]</span>
- <gr-copy-clipboard
- has-tooltip
- button-title="Copy password to clipboard"
- hide-input
- text="[[_generatedPassword]]">
+ <gr-copy-clipboard has-tooltip="" button-title="Copy password to clipboard" hide-input="" text="[[_generatedPassword]]">
</gr-copy-clipboard>
</section>
<section id="passwordWarning">
This password will not be displayed again.<br>
If you lose it, you will need to generate a new one.
</section>
- <gr-button
- link
- class="closeButton"
- on-click="_closeOverlay">Close</gr-button>
+ <gr-button link="" class="closeButton" on-click="_closeOverlay">Close</gr-button>
</div>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-http-password.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
index 974a0f2..57c8622 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_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-settings-view</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-http-password.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-http-password.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-http-password.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,61 +40,62 @@
</template>
</test-fixture>
-<script>
- suite('gr-http-password tests', async () => {
- await readyToTest();
- let element;
- let account;
- let config;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-http-password.js';
+suite('gr-http-password tests', () => {
+ let element;
+ let account;
+ let config;
- setup(done => {
- account = {username: 'user name'};
- config = {auth: {}};
+ setup(done => {
+ account = {username: 'user name'};
+ config = {auth: {}};
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(account); },
- getConfig() { return Promise.resolve(config); },
- });
-
- element = fixture('basic');
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(account); },
+ getConfig() { return Promise.resolve(config); },
});
- test('generate password', () => {
- const button = element.$.generateButton;
- const nextPassword = 'the new password';
- let generateResolve;
- const generateStub = sinon.stub(element.$.restAPI,
- 'generateAccountHttpPassword', () => new Promise(resolve => {
- generateResolve = resolve;
- }));
+ element = fixture('basic');
+ element.loadData().then(() => { flush(done); });
+ });
- assert.isNotOk(element._generatedPassword);
+ test('generate password', () => {
+ const button = element.$.generateButton;
+ const nextPassword = 'the new password';
+ let generateResolve;
+ const generateStub = sinon.stub(element.$.restAPI,
+ 'generateAccountHttpPassword', () => new Promise(resolve => {
+ generateResolve = resolve;
+ }));
- MockInteractions.tap(button);
+ assert.isNotOk(element._generatedPassword);
- assert.isTrue(generateStub.called);
- assert.equal(element._generatedPassword, 'Generating...');
+ MockInteractions.tap(button);
- generateResolve(nextPassword);
+ assert.isTrue(generateStub.called);
+ assert.equal(element._generatedPassword, 'Generating...');
- generateStub.lastCall.returnValue.then(() => {
- assert.equal(element._generatedPassword, nextPassword);
- });
- });
+ generateResolve(nextPassword);
- test('without http_password_url', () => {
- assert.isNull(element._passwordUrl);
- });
-
- test('with http_password_url', done => {
- config.auth.http_password_url = 'http://example.com/';
- element.loadData().then(() => {
- assert.isNotNull(element._passwordUrl);
- assert.equal(element._passwordUrl, config.auth.http_password_url);
- done();
- });
+ generateStub.lastCall.returnValue.then(() => {
+ assert.equal(element._generatedPassword, nextPassword);
});
});
+ test('without http_password_url', () => {
+ assert.isNull(element._passwordUrl);
+ });
+
+ test('with http_password_url', done => {
+ config.auth.http_password_url = 'http://example.com/';
+ element.loadData().then(() => {
+ assert.isNotNull(element._passwordUrl);
+ assert.equal(element._passwordUrl, config.auth.http_password_url);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index ac4f9e4..57f0e1d 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -14,95 +14,108 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const AUTH = [
- 'OPENID',
- 'OAUTH',
- ];
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-styles.js';
+import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-overlay/gr-overlay.js';
+import '../../shared/gr-rest-api-interface/gr-rest-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-identities_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrIdentities extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-identities'; }
+const AUTH = [
+ 'OPENID',
+ 'OAUTH',
+];
- static get properties() {
- return {
- _identities: Object,
- _idName: String,
- serverConfig: Object,
- _showLinkAnotherIdentity: {
- type: Boolean,
- computed: '_computeShowLinkAnotherIdentity(serverConfig)',
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrIdentities extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- loadData() {
- return this.$.restAPI.getExternalIds().then(id => {
- this._identities = id;
- });
- }
+ static get is() { return 'gr-identities'; }
- _computeIdentity(id) {
- return id && id.startsWith('mailto:') ? '' : id;
- }
-
- _computeHideDeleteClass(canDelete) {
- return canDelete ? 'show' : '';
- }
-
- _handleDeleteItemConfirm() {
- this.$.overlay.close();
- return this.$.restAPI.deleteAccountIdentity([this._idName])
- .then(() => { this.loadData(); });
- }
-
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
- }
-
- _handleDeleteItem(e) {
- const name = e.model.get('item.identity');
- if (!name) { return; }
- this._idName = name;
- this.$.overlay.open();
- }
-
- _computeIsTrusted(item) {
- return item ? '' : 'Untrusted';
- }
-
- filterIdentities(item) {
- return !item.identity.startsWith('username:');
- }
-
- _computeShowLinkAnotherIdentity(config) {
- if (config && config.auth &&
- config.auth.git_basic_auth_policy) {
- return AUTH.includes(
- config.auth.git_basic_auth_policy.toUpperCase());
- }
-
- return false;
- }
-
- _computeLinkAnotherIdentity() {
- const baseUrl = this.getBaseUrl() || '';
- let pathname = window.location.pathname;
- if (baseUrl) {
- pathname = '/' + pathname.substring(baseUrl.length);
- }
- return baseUrl + '/login/' + encodeURIComponent(pathname) + '?link';
- }
+ static get properties() {
+ return {
+ _identities: Object,
+ _idName: String,
+ serverConfig: Object,
+ _showLinkAnotherIdentity: {
+ type: Boolean,
+ computed: '_computeShowLinkAnotherIdentity(serverConfig)',
+ },
+ };
}
- customElements.define(GrIdentities.is, GrIdentities);
-})();
+ loadData() {
+ return this.$.restAPI.getExternalIds().then(id => {
+ this._identities = id;
+ });
+ }
+
+ _computeIdentity(id) {
+ return id && id.startsWith('mailto:') ? '' : id;
+ }
+
+ _computeHideDeleteClass(canDelete) {
+ return canDelete ? 'show' : '';
+ }
+
+ _handleDeleteItemConfirm() {
+ this.$.overlay.close();
+ return this.$.restAPI.deleteAccountIdentity([this._idName])
+ .then(() => { this.loadData(); });
+ }
+
+ _handleConfirmDialogCancel() {
+ this.$.overlay.close();
+ }
+
+ _handleDeleteItem(e) {
+ const name = e.model.get('item.identity');
+ if (!name) { return; }
+ this._idName = name;
+ this.$.overlay.open();
+ }
+
+ _computeIsTrusted(item) {
+ return item ? '' : 'Untrusted';
+ }
+
+ filterIdentities(item) {
+ return !item.identity.startsWith('username:');
+ }
+
+ _computeShowLinkAnotherIdentity(config) {
+ if (config && config.auth &&
+ config.auth.git_basic_auth_policy) {
+ return AUTH.includes(
+ config.auth.git_basic_auth_policy.toUpperCase());
+ }
+
+ return false;
+ }
+
+ _computeLinkAnotherIdentity() {
+ const baseUrl = this.getBaseUrl() || '';
+ let pathname = window.location.pathname;
+ if (baseUrl) {
+ pathname = '/' + pathname.substring(baseUrl.length);
+ }
+ return baseUrl + '/login/' + encodeURIComponent(pathname) + '?link';
+ }
+}
+
+customElements.define(GrIdentities.is, GrIdentities);
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
index 53d74f2..f1424cc 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
@@ -1,31 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
-<link rel="import" href="../../shared/gr-button/gr-button.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">
-
-<dom-module id="gr-identities">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -75,9 +66,7 @@
<td class="emailAddressColumn">[[item.email_address]]</td>
<td class="identityColumn">[[_computeIdentity(item.identity)]]</td>
<td class="deleteColumn">
- <gr-button
- class$="deleteButton [[_computeHideDeleteClass(item.can_delete)]]"
- on-click="_handleDeleteItem">
+ <gr-button class\$="deleteButton [[_computeHideDeleteClass(item.can_delete)]]" on-click="_handleDeleteItem">
Delete
</gr-button>
</td>
@@ -88,21 +77,14 @@
</fieldset>
<template is="dom-if" if="[[_showLinkAnotherIdentity]]">
<fieldset>
- <a href$="[[_computeLinkAnotherIdentity()]]">
- <gr-button id="linkAnotherIdentity" link>Link Another Identity</gr-button>
+ <a href\$="[[_computeLinkAnotherIdentity()]]">
+ <gr-button id="linkAnotherIdentity" link="">Link Another Identity</gr-button>
</a>
</fieldset>
</template>
</div>
- <gr-overlay id="overlay" with-backdrop>
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteItemConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_idName]]"
- item-type="id"></gr-confirm-delete-item-dialog>
+ <gr-overlay id="overlay" with-backdrop="">
+ <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteItemConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_idName]]" item-type="id"></gr-confirm-delete-item-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-identities.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
index be73a0c..acf4507 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_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-identities</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-identities.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-identities.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-identities.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,158 +40,161 @@
</template>
</test-fixture>
-<script>
- suite('gr-identities tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const ids = [
- {
- identity: 'username:john',
- email_address: 'john.doe@example.com',
- trusted: true,
- }, {
- identity: 'gerrit:gerrit',
- email_address: 'gerrit@example.com',
- }, {
- identity: 'mailto:gerrit2@example.com',
- email_address: 'gerrit2@example.com',
- trusted: true,
- can_delete: true,
- },
- ];
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-identities.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-identities tests', () => {
+ let element;
+ let sandbox;
+ const ids = [
+ {
+ identity: 'username:john',
+ email_address: 'john.doe@example.com',
+ trusted: true,
+ }, {
+ identity: 'gerrit:gerrit',
+ email_address: 'gerrit@example.com',
+ }, {
+ identity: 'mailto:gerrit2@example.com',
+ email_address: 'gerrit2@example.com',
+ trusted: true,
+ can_delete: true,
+ },
+ ];
- setup(done => {
- sandbox = sinon.sandbox.create();
+ setup(done => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getExternalIds() { return Promise.resolve(ids); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getExternalIds() { return Promise.resolve(ids); },
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
- test('renders', () => {
- const rows = Array.from(
- Polymer.dom(element.root).querySelectorAll('tbody tr'));
+ element.loadData().then(() => { flush(done); });
+ });
- assert.equal(rows.length, 2);
+ teardown(() => {
+ sandbox.restore();
+ });
- const nameCells = rows.map(row =>
- row.querySelectorAll('td')[2].textContent
- );
+ test('renders', () => {
+ const rows = Array.from(
+ dom(element.root).querySelectorAll('tbody tr'));
- assert.equal(nameCells[0], 'gerrit:gerrit');
- assert.equal(nameCells[1], '');
- });
+ assert.equal(rows.length, 2);
- test('renders email', () => {
- const rows = Array.from(
- Polymer.dom(element.root).querySelectorAll('tbody tr'));
+ const nameCells = rows.map(row =>
+ row.querySelectorAll('td')[2].textContent
+ );
- assert.equal(rows.length, 2);
+ assert.equal(nameCells[0], 'gerrit:gerrit');
+ assert.equal(nameCells[1], '');
+ });
- const nameCells = rows.map(row =>
- row.querySelectorAll('td')[1].textContent
- );
+ test('renders email', () => {
+ const rows = Array.from(
+ dom(element.root).querySelectorAll('tbody tr'));
- assert.equal(nameCells[0], 'gerrit@example.com');
- assert.equal(nameCells[1], 'gerrit2@example.com');
- });
+ assert.equal(rows.length, 2);
- test('_computeIdentity', () => {
- assert.equal(
- element._computeIdentity(ids[0].identity), 'username:john');
- assert.equal(element._computeIdentity(ids[2].identity), '');
- });
+ const nameCells = rows.map(row =>
+ row.querySelectorAll('td')[1].textContent
+ );
- test('filterIdentities', () => {
- assert.isFalse(element.filterIdentities(ids[0]));
+ assert.equal(nameCells[0], 'gerrit@example.com');
+ assert.equal(nameCells[1], 'gerrit2@example.com');
+ });
- assert.isTrue(element.filterIdentities(ids[1]));
- });
+ test('_computeIdentity', () => {
+ assert.equal(
+ element._computeIdentity(ids[0].identity), 'username:john');
+ assert.equal(element._computeIdentity(ids[2].identity), '');
+ });
- test('delete id', done => {
- element._idName = 'mailto:gerrit2@example.com';
- const loadDataStub = sandbox.stub(element, 'loadData');
- element._handleDeleteItemConfirm().then(() => {
- assert.isTrue(loadDataStub.called);
- done();
- });
- });
+ test('filterIdentities', () => {
+ assert.isFalse(element.filterIdentities(ids[0]));
- test('_handleDeleteItem opens modal', () => {
- const deleteBtn =
- Polymer.dom(element.root).querySelector('.deleteButton');
- const deleteItem = sandbox.stub(element, '_handleDeleteItem');
- MockInteractions.tap(deleteBtn);
- assert.isTrue(deleteItem.called);
- });
+ assert.isTrue(element.filterIdentities(ids[1]));
+ });
- test('_computeShowLinkAnotherIdentity', () => {
- let serverConfig;
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'OAUTH',
- },
- };
- assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'OpenID',
- },
- };
- assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP_LDAP',
- },
- };
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'LDAP',
- },
- };
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP',
- },
- };
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
-
- serverConfig = {};
- assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
- });
-
- test('_showLinkAnotherIdentity', () => {
- element.serverConfig = {
- auth: {
- git_basic_auth_policy: 'OAUTH',
- },
- };
-
- assert.isTrue(element._showLinkAnotherIdentity);
-
- element.serverConfig = {
- auth: {
- git_basic_auth_policy: 'LDAP',
- },
- };
-
- assert.isFalse(element._showLinkAnotherIdentity);
+ test('delete id', done => {
+ element._idName = 'mailto:gerrit2@example.com';
+ const loadDataStub = sandbox.stub(element, 'loadData');
+ element._handleDeleteItemConfirm().then(() => {
+ assert.isTrue(loadDataStub.called);
+ done();
});
});
+
+ test('_handleDeleteItem opens modal', () => {
+ const deleteBtn =
+ dom(element.root).querySelector('.deleteButton');
+ const deleteItem = sandbox.stub(element, '_handleDeleteItem');
+ MockInteractions.tap(deleteBtn);
+ assert.isTrue(deleteItem.called);
+ });
+
+ test('_computeShowLinkAnotherIdentity', () => {
+ let serverConfig;
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OAUTH',
+ },
+ };
+ assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OpenID',
+ },
+ };
+ assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP_LDAP',
+ },
+ };
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'LDAP',
+ },
+ };
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP',
+ },
+ };
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+
+ serverConfig = {};
+ assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig));
+ });
+
+ test('_showLinkAnotherIdentity', () => {
+ element.serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OAUTH',
+ },
+ };
+
+ assert.isTrue(element._showLinkAnotherIdentity);
+
+ element.serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'LDAP',
+ },
+ };
+
+ assert.isFalse(element._showLinkAnotherIdentity);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 0ee232b..42982fd 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -14,68 +14,80 @@
* 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 GrMenuEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-menu-editor'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../styles/gr-form-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-menu-editor_html.js';
- static get properties() {
- return {
- menuItems: Array,
- _newName: String,
- _newUrl: String,
- };
- }
+/** @extends Polymer.Element */
+class GrMenuEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _handleMoveUpButton(e) {
- const index = Number(Polymer.dom(e).localTarget.dataset.index);
- if (index === 0) { return; }
- const row = this.menuItems[index];
- const prev = this.menuItems[index - 1];
- this.splice('menuItems', index - 1, 2, row, prev);
- }
+ static get is() { return 'gr-menu-editor'; }
- _handleMoveDownButton(e) {
- const index = Number(Polymer.dom(e).localTarget.dataset.index);
- if (index === this.menuItems.length - 1) { return; }
- const row = this.menuItems[index];
- const next = this.menuItems[index + 1];
- this.splice('menuItems', index, 2, next, row);
- }
-
- _handleDeleteButton(e) {
- const index = Number(Polymer.dom(e).localTarget.dataset.index);
- this.splice('menuItems', index, 1);
- }
-
- _handleAddButton() {
- if (this._computeAddDisabled(this._newName, this._newUrl)) { return; }
-
- this.splice('menuItems', this.menuItems.length, 0, {
- name: this._newName,
- url: this._newUrl,
- target: '_blank',
- });
-
- this._newName = '';
- this._newUrl = '';
- }
-
- _computeAddDisabled(newName, newUrl) {
- return !newName.length || !newUrl.length;
- }
-
- _handleInputKeydown(e) {
- if (e.keyCode === 13) {
- e.stopPropagation();
- this._handleAddButton();
- }
- }
+ static get properties() {
+ return {
+ menuItems: Array,
+ _newName: String,
+ _newUrl: String,
+ };
}
- customElements.define(GrMenuEditor.is, GrMenuEditor);
-})();
+ _handleMoveUpButton(e) {
+ const index = Number(dom(e).localTarget.dataset.index);
+ if (index === 0) { return; }
+ const row = this.menuItems[index];
+ const prev = this.menuItems[index - 1];
+ this.splice('menuItems', index - 1, 2, row, prev);
+ }
+
+ _handleMoveDownButton(e) {
+ const index = Number(dom(e).localTarget.dataset.index);
+ if (index === this.menuItems.length - 1) { return; }
+ const row = this.menuItems[index];
+ const next = this.menuItems[index + 1];
+ this.splice('menuItems', index, 2, next, row);
+ }
+
+ _handleDeleteButton(e) {
+ const index = Number(dom(e).localTarget.dataset.index);
+ this.splice('menuItems', index, 1);
+ }
+
+ _handleAddButton() {
+ if (this._computeAddDisabled(this._newName, this._newUrl)) { return; }
+
+ this.splice('menuItems', this.menuItems.length, 0, {
+ name: this._newName,
+ url: this._newUrl,
+ target: '_blank',
+ });
+
+ this._newName = '';
+ this._newUrl = '';
+ }
+
+ _computeAddDisabled(newName, newUrl) {
+ return !newName.length || !newUrl.length;
+ }
+
+ _handleInputKeydown(e) {
+ if (e.keyCode === 13) {
+ e.stopPropagation();
+ this._handleAddButton();
+ }
+ }
+}
+
+customElements.define(GrMenuEditor.is, GrMenuEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
index 46fc165..58b654f 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
@@ -1,30 +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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-
-<dom-module id="gr-menu-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.buttonColumn {
width: 2em;
@@ -61,25 +53,13 @@
<td>[[item.name]]</td>
<td class="urlCell">[[item.url]]</td>
<td class="buttonColumn">
- <gr-button
- link
- data-index$="[[index]]"
- on-click="_handleMoveUpButton"
- class="moveUpButton">↑</gr-button>
+ <gr-button link="" data-index\$="[[index]]" on-click="_handleMoveUpButton" class="moveUpButton">↑</gr-button>
</td>
<td class="buttonColumn">
- <gr-button
- link
- data-index$="[[index]]"
- on-click="_handleMoveDownButton"
- class="moveDownButton">↓</gr-button>
+ <gr-button link="" data-index\$="[[index]]" on-click="_handleMoveDownButton" class="moveDownButton">↓</gr-button>
</td>
<td>
- <gr-button
- link
- data-index$="[[index]]"
- on-click="_handleDeleteButton"
- class="remove-button">Delete</gr-button>
+ <gr-button link="" data-index\$="[[index]]" on-click="_handleDeleteButton" class="remove-button">Delete</gr-button>
</td>
</tr>
</template>
@@ -87,43 +67,22 @@
<tfoot>
<tr>
<th>
- <iron-input
- placeholder="New Title"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newName}}">
- <input
- is="iron-input"
- placeholder="New Title"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newName}}">
+ <iron-input placeholder="New Title" on-keydown="_handleInputKeydown" bind-value="{{_newName}}">
+ <input is="iron-input" placeholder="New Title" on-keydown="_handleInputKeydown" bind-value="{{_newName}}">
</iron-input>
</th>
<th>
- <iron-input
- class="newUrlInput"
- placeholder="New URL"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newUrl}}">
- <input
- class="newUrlInput"
- is="iron-input"
- placeholder="New URL"
- on-keydown="_handleInputKeydown"
- bind-value="{{_newUrl}}">
+ <iron-input class="newUrlInput" placeholder="New URL" on-keydown="_handleInputKeydown" bind-value="{{_newUrl}}">
+ <input class="newUrlInput" is="iron-input" placeholder="New URL" on-keydown="_handleInputKeydown" bind-value="{{_newUrl}}">
</iron-input>
</th>
<th></th>
<th></th>
<th>
- <gr-button
- link
- disabled$="[[_computeAddDisabled(_newName, _newUrl)]]"
- on-click="_handleAddButton">Add</gr-button>
+ <gr-button link="" disabled\$="[[_computeAddDisabled(_newName, _newUrl)]]" on-click="_handleAddButton">Add</gr-button>
</th>
</tr>
</tfoot>
</table>
</div>
- </template>
- <script src="gr-menu-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index a5f2074..930255c 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_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-settings-view</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-menu-editor.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-menu-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-menu-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,146 +40,149 @@
</template>
</test-fixture>
-<script>
- suite('gr-menu-editor tests', async () => {
- await readyToTest();
- let element;
- let menu;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-menu-editor.js';
+import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-menu-editor tests', () => {
+ let element;
+ let menu;
- function assertMenuNamesEqual(element, expected) {
- const names = element.menuItems.map(i => i.name);
- assert.equal(names.length, expected.length);
- for (let i = 0; i < names.length; i++) {
- assert.equal(names[i], expected[i]);
- }
+ function assertMenuNamesEqual(element, expected) {
+ const names = element.menuItems.map(i => i.name);
+ assert.equal(names.length, expected.length);
+ for (let i = 0; i < names.length; i++) {
+ assert.equal(names[i], expected[i]);
+ }
+ }
+
+ // Click the up/down button (according to direction) for the index'th row.
+ // The index of the first row is 0, corresponding to the array.
+ function move(element, index, direction) {
+ const selector = 'tr:nth-child(' + (index + 1) + ') .move' +
+ direction + 'Button';
+ const button =
+ element.shadowRoot
+ .querySelector('tbody').querySelector(selector)
+ .shadowRoot
+ .querySelector('paper-button');
+ MockInteractions.tap(button);
+ }
+
+ setup(done => {
+ element = fixture('basic');
+ menu = [
+ {url: '/first/url', name: 'first name', target: '_blank'},
+ {url: '/second/url', name: 'second name', target: '_blank'},
+ {url: '/third/url', name: 'third name', target: '_blank'},
+ ];
+ element.set('menuItems', menu);
+ flush$0();
+ flush(done);
+ });
+
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('tbody').querySelectorAll('tr');
+ let tds;
+
+ assert.equal(rows.length, menu.length);
+ for (let i = 0; i < menu.length; i++) {
+ tds = rows[i].querySelectorAll('td');
+ assert.equal(tds[0].textContent, menu[i].name);
+ assert.equal(tds[1].textContent, menu[i].url);
}
- // Click the up/down button (according to direction) for the index'th row.
- // The index of the first row is 0, corresponding to the array.
- function move(element, index, direction) {
- const selector = 'tr:nth-child(' + (index + 1) + ') .move' +
- direction + 'Button';
- const button =
- element.shadowRoot
- .querySelector('tbody').querySelector(selector)
- .shadowRoot
- .querySelector('paper-button');
- MockInteractions.tap(button);
- }
+ assert.isTrue(element._computeAddDisabled(element._newName,
+ element._newUrl));
+ });
- setup(done => {
- element = fixture('basic');
- menu = [
- {url: '/first/url', name: 'first name', target: '_blank'},
- {url: '/second/url', name: 'second name', target: '_blank'},
- {url: '/third/url', name: 'third name', target: '_blank'},
- ];
- element.set('menuItems', menu);
- Polymer.dom.flush();
- flush(done);
- });
+ test('_computeAddDisabled', () => {
+ assert.isTrue(element._computeAddDisabled('', ''));
+ assert.isTrue(element._computeAddDisabled('name', ''));
+ assert.isTrue(element._computeAddDisabled('', 'url'));
+ assert.isFalse(element._computeAddDisabled('name', 'url'));
+ });
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('tbody').querySelectorAll('tr');
- let tds;
+ test('add a new menu item', () => {
+ const newName = 'new name';
+ const newUrl = 'new url';
- assert.equal(rows.length, menu.length);
- for (let i = 0; i < menu.length; i++) {
- tds = rows[i].querySelectorAll('td');
- assert.equal(tds[0].textContent, menu[i].name);
- assert.equal(tds[1].textContent, menu[i].url);
- }
+ element._newName = newName;
+ element._newUrl = newUrl;
+ assert.isFalse(element._computeAddDisabled(element._newName,
+ element._newUrl));
- assert.isTrue(element._computeAddDisabled(element._newName,
- element._newUrl));
- });
+ const originalMenuLength = element.menuItems.length;
- test('_computeAddDisabled', () => {
- assert.isTrue(element._computeAddDisabled('', ''));
- assert.isTrue(element._computeAddDisabled('name', ''));
- assert.isTrue(element._computeAddDisabled('', 'url'));
- assert.isFalse(element._computeAddDisabled('name', 'url'));
- });
+ element._handleAddButton();
- test('add a new menu item', () => {
- const newName = 'new name';
- const newUrl = 'new url';
+ assert.equal(element.menuItems.length, originalMenuLength + 1);
+ assert.equal(element.menuItems[element.menuItems.length - 1].name,
+ newName);
+ assert.equal(element.menuItems[element.menuItems.length - 1].url, newUrl);
+ });
- element._newName = newName;
- element._newUrl = newUrl;
- assert.isFalse(element._computeAddDisabled(element._newName,
- element._newUrl));
+ test('move items down', () => {
+ assertMenuNamesEqual(element,
+ ['first name', 'second name', 'third name']);
- const originalMenuLength = element.menuItems.length;
+ // Move the middle item down
+ move(element, 1, 'Down');
+ assertMenuNamesEqual(element,
+ ['first name', 'third name', 'second name']);
- element._handleAddButton();
+ // Moving the bottom item down is a no-op.
+ move(element, 2, 'Down');
+ assertMenuNamesEqual(element,
+ ['first name', 'third name', 'second name']);
+ });
- assert.equal(element.menuItems.length, originalMenuLength + 1);
- assert.equal(element.menuItems[element.menuItems.length - 1].name,
- newName);
- assert.equal(element.menuItems[element.menuItems.length - 1].url, newUrl);
- });
+ test('move items up', () => {
+ assertMenuNamesEqual(element,
+ ['first name', 'second name', 'third name']);
- test('move items down', () => {
- assertMenuNamesEqual(element,
- ['first name', 'second name', 'third name']);
+ // Move the last item up twice to be the first.
+ move(element, 2, 'Up');
+ move(element, 1, 'Up');
+ assertMenuNamesEqual(element,
+ ['third name', 'first name', 'second name']);
- // Move the middle item down
- move(element, 1, 'Down');
- assertMenuNamesEqual(element,
- ['first name', 'third name', 'second name']);
+ // Moving the top item up is a no-op.
+ move(element, 0, 'Up');
+ assertMenuNamesEqual(element,
+ ['third name', 'first name', 'second name']);
+ });
- // Moving the bottom item down is a no-op.
- move(element, 2, 'Down');
- assertMenuNamesEqual(element,
- ['first name', 'third name', 'second name']);
- });
+ test('remove item', () => {
+ assertMenuNamesEqual(element,
+ ['first name', 'second name', 'third name']);
- test('move items up', () => {
- assertMenuNamesEqual(element,
- ['first name', 'second name', 'third name']);
+ // Tap the delete button for the middle item.
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('tbody')
+ .querySelector('tr:nth-child(2) .remove-button')
+ .shadowRoot
+ .querySelector('paper-button'));
- // Move the last item up twice to be the first.
- move(element, 2, 'Up');
- move(element, 1, 'Up');
- assertMenuNamesEqual(element,
- ['third name', 'first name', 'second name']);
+ assertMenuNamesEqual(element, ['first name', 'third name']);
- // Moving the top item up is a no-op.
- move(element, 0, 'Up');
- assertMenuNamesEqual(element,
- ['third name', 'first name', 'second name']);
- });
-
- test('remove item', () => {
- assertMenuNamesEqual(element,
- ['first name', 'second name', 'third name']);
-
- // Tap the delete button for the middle item.
+ // Delete remaining items.
+ for (let i = 0; i < 2; i++) {
MockInteractions.tap(element.shadowRoot
.querySelector('tbody')
- .querySelector('tr:nth-child(2) .remove-button')
+ .querySelector('tr:first-child .remove-button')
.shadowRoot
.querySelector('paper-button'));
+ }
+ assertMenuNamesEqual(element, []);
- assertMenuNamesEqual(element, ['first name', 'third name']);
-
- // Delete remaining items.
- for (let i = 0; i < 2; i++) {
- MockInteractions.tap(element.shadowRoot
- .querySelector('tbody')
- .querySelector('tr:first-child .remove-button')
- .shadowRoot
- .querySelector('paper-button'));
- }
- assertMenuNamesEqual(element, []);
-
- // Add item to empty menu.
- element._newName = 'new name';
- element._newUrl = 'new url';
- element._handleAddButton();
- assertMenuNamesEqual(element, ['new name']);
- });
+ // Add item to empty menu.
+ element._newName = 'new name';
+ element._newUrl = 'new url';
+ element._handleAddButton();
+ assertMenuNamesEqual(element, ['new name']);
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 4bb98d0..c20800f 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -14,142 +14,155 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/gr-form-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.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-registration-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrRegistrationDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-registration-dialog'; }
+ /**
+ * Fired when account details are changed.
+ *
+ * @event account-detail-update
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the close button is pressed.
+ *
+ * @event close
*/
- class GrRegistrationDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-registration-dialog'; }
- /**
- * Fired when account details are changed.
- *
- * @event account-detail-update
- */
- /**
- * Fired when the close button is pressed.
- *
- * @event close
- */
-
- static get properties() {
- return {
- settingsUrl: String,
- /** @type {?} */
- _account: {
- type: Object,
- value: () => {
- // Prepopulate possibly undefined fields with values to trigger
- // computed bindings.
- return {email: null, name: null, username: null};
- },
+ static get properties() {
+ return {
+ settingsUrl: String,
+ /** @type {?} */
+ _account: {
+ type: Object,
+ value: () => {
+ // Prepopulate possibly undefined fields with values to trigger
+ // computed bindings.
+ return {email: null, name: null, username: null};
},
- _usernameMutable: {
- type: Boolean,
- computed: '_computeUsernameMutable(_serverConfig, _account.username)',
- },
- _loading: {
- type: Boolean,
- value: true,
- observer: '_loadingChanged',
- },
- _saving: {
- type: Boolean,
- value: false,
- },
- _serverConfig: Object,
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
-
- loadData() {
- this._loading = true;
-
- const loadAccount = this.$.restAPI.getAccount().then(account => {
- // Using Object.assign here allows preservation of the default values
- // supplied in the value generating function of this._account, unless
- // they are overridden by properties in the account from the response.
- this._account = Object.assign({}, this._account, account);
- });
-
- const loadConfig = this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
- });
-
- return Promise.all([loadAccount, loadConfig]).then(() => {
- this._loading = false;
- });
- }
-
- _save() {
- this._saving = true;
- const promises = [
- this.$.restAPI.setAccountName(this.$.name.value),
- this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''),
- ];
-
- if (this._usernameMutable) {
- promises.push(this.$.restAPI.setAccountUsername(this.$.username.value));
- }
-
- return Promise.all(promises).then(() => {
- this._saving = false;
- this.fire('account-detail-update');
- });
- }
-
- _handleSave(e) {
- e.preventDefault();
- this._save().then(this.close.bind(this));
- }
-
- _handleClose(e) {
- e.preventDefault();
- this.close();
- }
-
- close() {
- this._saving = true; // disable buttons indefinitely
- this.fire('close');
- }
-
- _computeSaveDisabled(name, email, saving) {
- return !name || !email || saving;
- }
-
- _computeUsernameMutable(config, username) {
- // Polymer 2: check for undefined
- if ([
- config,
- username,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- return config.auth.editable_account_fields.includes('USER_NAME') &&
- !username;
- }
-
- _computeUsernameClass(usernameMutable) {
- return usernameMutable ? '' : 'hide';
- }
-
- _loadingChanged() {
- this.classList.toggle('loading', this._loading);
- }
+ },
+ _usernameMutable: {
+ type: Boolean,
+ computed: '_computeUsernameMutable(_serverConfig, _account.username)',
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ observer: '_loadingChanged',
+ },
+ _saving: {
+ type: Boolean,
+ value: false,
+ },
+ _serverConfig: Object,
+ };
}
- customElements.define(GrRegistrationDialog.is, GrRegistrationDialog);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
+
+ loadData() {
+ this._loading = true;
+
+ const loadAccount = this.$.restAPI.getAccount().then(account => {
+ // Using Object.assign here allows preservation of the default values
+ // supplied in the value generating function of this._account, unless
+ // they are overridden by properties in the account from the response.
+ this._account = Object.assign({}, this._account, account);
+ });
+
+ const loadConfig = this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
+ });
+
+ return Promise.all([loadAccount, loadConfig]).then(() => {
+ this._loading = false;
+ });
+ }
+
+ _save() {
+ this._saving = true;
+ const promises = [
+ this.$.restAPI.setAccountName(this.$.name.value),
+ this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''),
+ ];
+
+ if (this._usernameMutable) {
+ promises.push(this.$.restAPI.setAccountUsername(this.$.username.value));
+ }
+
+ return Promise.all(promises).then(() => {
+ this._saving = false;
+ this.fire('account-detail-update');
+ });
+ }
+
+ _handleSave(e) {
+ e.preventDefault();
+ this._save().then(this.close.bind(this));
+ }
+
+ _handleClose(e) {
+ e.preventDefault();
+ this.close();
+ }
+
+ close() {
+ this._saving = true; // disable buttons indefinitely
+ this.fire('close');
+ }
+
+ _computeSaveDisabled(name, email, saving) {
+ return !name || !email || saving;
+ }
+
+ _computeUsernameMutable(config, username) {
+ // Polymer 2: check for undefined
+ if ([
+ config,
+ username,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ return config.auth.editable_account_fields.includes('USER_NAME') &&
+ !username;
+ }
+
+ _computeUsernameClass(usernameMutable) {
+ return usernameMutable ? '' : 'hide';
+ }
+
+ _loadingChanged() {
+ this.classList.toggle('loading', this._loading);
+ }
+}
+
+customElements.define(GrRegistrationDialog.is, GrRegistrationDialog);
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
index c289a49..737e6d5 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
@@ -1,31 +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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-registration-dialog">
- <template>
+export const htmlTemplate = html`
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -85,31 +76,19 @@
<hr>
<section>
<div class="title">Full Name</div>
- <iron-input
- bind-value="{{_account.name}}">
- <input
- is="iron-input"
- id="name"
- bind-value="{{_account.name}}"
- disabled="[[_saving]]">
+ <iron-input bind-value="{{_account.name}}">
+ <input is="iron-input" id="name" bind-value="{{_account.name}}" disabled="[[_saving]]">
</iron-input>
</section>
- <section class$="[[_computeUsernameClass(_usernameMutable)]]">
+ <section class\$="[[_computeUsernameClass(_usernameMutable)]]">
<div class="title">Username</div>
- <iron-input
- bind-value="{{_account.username}}">
- <input
- is="iron-input"
- id="username"
- bind-value="{{_account.username}}"
- disabled="[[_saving]]">
+ <iron-input bind-value="{{_account.username}}">
+ <input is="iron-input" id="username" bind-value="{{_account.username}}" disabled="[[_saving]]">
</iron-input>
</section>
<section>
<div class="title">Preferred Email</div>
- <select
- id="email"
- disabled="[[_saving]]">
+ <select id="email" disabled="[[_saving]]">
<option value="[[_account.email]]">[[_account.email]]</option>
<template is="dom-repeat" items="[[_account.secondary_emails]]">
<option value="[[item]]">[[item]]</option>
@@ -119,24 +98,13 @@
<hr>
<p>
More configuration options for Gerrit may be found in the
- <a on-click="close" href$="[[settingsUrl]]">settings</a>.
+ <a on-click="close" href\$="[[settingsUrl]]">settings</a>.
</p>
</main>
<footer>
- <gr-button
- id="closeButton"
- link
- disabled="[[_saving]]"
- on-click="_handleClose">Close</gr-button>
- <gr-button
- id="saveButton"
- primary
- link
- disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]"
- on-click="_handleSave">Save</gr-button>
+ <gr-button id="closeButton" link="" disabled="[[_saving]]" on-click="_handleClose">Close</gr-button>
+ <gr-button id="saveButton" primary="" link="" disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]" on-click="_handleSave">Save</gr-button>
</footer>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-registration-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index a3be75c..9aaaaa7 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-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-registration-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-registration-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-registration-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-registration-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -41,149 +46,151 @@
</template>
</test-fixture>
-<script>
- suite('gr-registration-dialog tests', async () => {
- await readyToTest();
- let element;
- let account;
- let sandbox;
- let _listeners;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-registration-dialog.js';
+suite('gr-registration-dialog tests', () => {
+ let element;
+ let account;
+ let sandbox;
+ let _listeners;
- setup(() => {
- sandbox = sinon.sandbox.create();
- _listeners = {};
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ _listeners = {};
- account = {
- name: 'name',
- username: null,
- email: 'email',
- secondary_emails: [
- 'email2',
- 'email3',
- ],
- };
+ account = {
+ name: 'name',
+ username: null,
+ email: 'email',
+ secondary_emails: [
+ 'email2',
+ 'email3',
+ ],
+ };
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve(account);
- },
- setAccountName(name) {
- account.name = name;
- return Promise.resolve();
- },
- setAccountUsername(username) {
- account.username = username;
- return Promise.resolve();
- },
- setPreferredAccountEmail(email) {
- account.email = email;
- return Promise.resolve();
- },
- getConfig() {
- return Promise.resolve(
- {auth: {editable_account_fields: ['USER_NAME']}});
- },
- });
-
- element = fixture('basic');
-
- return element.loadData();
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve(account);
+ },
+ setAccountName(name) {
+ account.name = name;
+ return Promise.resolve();
+ },
+ setAccountUsername(username) {
+ account.username = username;
+ return Promise.resolve();
+ },
+ setPreferredAccountEmail(email) {
+ account.email = email;
+ return Promise.resolve();
+ },
+ getConfig() {
+ return Promise.resolve(
+ {auth: {editable_account_fields: ['USER_NAME']}});
+ },
});
- teardown(() => {
- sandbox.restore();
- for (const eventType in _listeners) {
- if (_listeners.hasOwnProperty(eventType)) {
- element.removeEventListener(eventType, _listeners[eventType]);
- }
+ element = fixture('basic');
+
+ return element.loadData();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ for (const eventType in _listeners) {
+ if (_listeners.hasOwnProperty(eventType)) {
+ element.removeEventListener(eventType, _listeners[eventType]);
}
- });
-
- function listen(eventType) {
- return new Promise(resolve => {
- _listeners[eventType] = function() { resolve(); };
- element.addEventListener(eventType, _listeners[eventType]);
- });
}
+ });
- function save(opt_action) {
- const promise = listen('account-detail-update');
- if (opt_action) {
- opt_action();
- } else {
- MockInteractions.tap(element.$.saveButton);
- }
- return promise;
+ function listen(eventType) {
+ return new Promise(resolve => {
+ _listeners[eventType] = function() { resolve(); };
+ element.addEventListener(eventType, _listeners[eventType]);
+ });
+ }
+
+ function save(opt_action) {
+ const promise = listen('account-detail-update');
+ if (opt_action) {
+ opt_action();
+ } else {
+ MockInteractions.tap(element.$.saveButton);
}
+ return promise;
+ }
- function close(opt_action) {
- const promise = listen('close');
- if (opt_action) {
- opt_action();
- } else {
- MockInteractions.tap(element.$.closeButton);
- }
- return promise;
+ function close(opt_action) {
+ const promise = listen('close');
+ if (opt_action) {
+ opt_action();
+ } else {
+ MockInteractions.tap(element.$.closeButton);
}
+ return promise;
+ }
- test('fires the close event on close', done => {
- close().then(done);
- });
+ test('fires the close event on close', done => {
+ close().then(done);
+ });
- test('fires the close event on save', done => {
- close(() => {
- MockInteractions.tap(element.$.saveButton);
- }).then(done);
- });
+ test('fires the close event on save', done => {
+ close(() => {
+ MockInteractions.tap(element.$.saveButton);
+ }).then(done);
+ });
- test('saves account details', done => {
- flush(() => {
- element.$.name.value = 'new name';
- element.$.username.value = 'new username';
- element.$.email.value = 'email3';
+ test('saves account details', done => {
+ flush(() => {
+ element.$.name.value = 'new name';
+ element.$.username.value = 'new username';
+ element.$.email.value = 'email3';
- // Nothing should be committed yet.
- assert.equal(account.name, 'name');
- assert.isNotOk(account.username);
- assert.equal(account.email, 'email');
+ // Nothing should be committed yet.
+ assert.equal(account.name, 'name');
+ assert.isNotOk(account.username);
+ assert.equal(account.email, 'email');
- // Save and verify new values are committed.
- save()
- .then(() => {
- assert.equal(account.name, 'new name');
- assert.equal(account.username, 'new username');
- assert.equal(account.email, 'email3');
- })
- .then(done);
- });
- });
-
- test('email select properly populated', done => {
- element._account = {email: 'foo', secondary_emails: ['bar', 'baz']};
- flush(() => {
- assert.equal(element.$.email.value, 'foo');
- done();
- });
- });
-
- test('save btn disabled', () => {
- const compute = element._computeSaveDisabled;
- assert.isTrue(compute('', '', false));
- assert.isTrue(compute('', 'test', false));
- assert.isTrue(compute('test', '', false));
- assert.isTrue(compute('test', 'test', true));
- assert.isFalse(compute('test', 'test', false));
- });
-
- test('_computeUsernameMutable', () => {
- assert.isTrue(element._computeUsernameMutable(
- {auth: {editable_account_fields: ['USER_NAME']}}, null));
- assert.isFalse(element._computeUsernameMutable(
- {auth: {editable_account_fields: ['USER_NAME']}}, 'abc'));
- assert.isFalse(element._computeUsernameMutable(
- {auth: {editable_account_fields: []}}, null));
- assert.isFalse(element._computeUsernameMutable(
- {auth: {editable_account_fields: []}}, 'abc'));
+ // Save and verify new values are committed.
+ save()
+ .then(() => {
+ assert.equal(account.name, 'new name');
+ assert.equal(account.username, 'new username');
+ assert.equal(account.email, 'email3');
+ })
+ .then(done);
});
});
+
+ test('email select properly populated', done => {
+ element._account = {email: 'foo', secondary_emails: ['bar', 'baz']};
+ flush(() => {
+ assert.equal(element.$.email.value, 'foo');
+ done();
+ });
+ });
+
+ test('save btn disabled', () => {
+ const compute = element._computeSaveDisabled;
+ assert.isTrue(compute('', '', false));
+ assert.isTrue(compute('', 'test', false));
+ assert.isTrue(compute('test', '', false));
+ assert.isTrue(compute('test', 'test', true));
+ assert.isFalse(compute('test', 'test', false));
+ });
+
+ test('_computeUsernameMutable', () => {
+ assert.isTrue(element._computeUsernameMutable(
+ {auth: {editable_account_fields: ['USER_NAME']}}, null));
+ assert.isFalse(element._computeUsernameMutable(
+ {auth: {editable_account_fields: ['USER_NAME']}}, 'abc'));
+ assert.isFalse(element._computeUsernameMutable(
+ {auth: {editable_account_fields: []}}, null));
+ assert.isFalse(element._computeUsernameMutable(
+ {auth: {editable_account_fields: []}}, 'abc'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
index bae1f38..3884a15 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -14,22 +14,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.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-settings-item_html.js';
- /** @extends Polymer.Element */
- class GrSettingsItem extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-settings-item'; }
+/** @extends Polymer.Element */
+class GrSettingsItem extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- anchor: String,
- title: String,
- };
- }
+ static get is() { return 'gr-settings-item'; }
+
+ static get properties() {
+ return {
+ anchor: String,
+ title: String,
+ };
}
+}
- customElements.define(GrSettingsItem.is, GrSettingsItem);
-})();
+customElements.define(GrSettingsItem.is, GrSettingsItem);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
index 937ee79..accb8c8 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
@@ -1,24 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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">
-
-<dom-module id="gr-settings-item">
- <template>
+export const htmlTemplate = html`
<style>
:host {
display: block;
@@ -27,6 +25,4 @@
</style>
<h2 id="[[anchor]]">[[title]]</h2>
<slot></slot>
- </template>
- <script src="gr-settings-item.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
index d5a7eb7..5b11516 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -14,22 +14,28 @@
* 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 GrSettingsMenuItem extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-settings-menu-item'; }
+import '../../../styles/gr-page-nav-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-settings-menu-item_html.js';
- static get properties() {
- return {
- href: String,
- title: String,
- };
- }
+/** @extends Polymer.Element */
+class GrSettingsMenuItem extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-settings-menu-item'; }
+
+ static get properties() {
+ return {
+ href: String,
+ title: String,
+ };
}
+}
- customElements.define(GrSettingsMenuItem.is, GrSettingsMenuItem);
-})();
+customElements.define(GrSettingsMenuItem.is, GrSettingsMenuItem);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
index c356e80..5cb129f 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
@@ -1,25 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/gr-page-nav-styles.html">
-
-<dom-module id="gr-settings-menu-item">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -27,8 +24,6 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<div class="navStyles">
- <li><a href$="[[href]]">[[title]]</a></li>
+ <li><a href\$="[[href]]">[[title]]</a></li>
</div>
- </template>
- <script src="gr-settings-menu-item.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 78bad8c..733fa56 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -14,456 +14,489 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const PREFS_SECTION_FIELDS = [
- 'changes_per_page',
- 'date_format',
- 'time_format',
- 'email_strategy',
- 'diff_view',
- 'publish_comments_on_push',
- 'work_in_progress_by_default',
- 'default_base_for_merges',
- 'signed_off_by',
- 'email_format',
- 'size_bar_in_change_table',
- 'relative_date_in_change_table',
- ];
+import '@polymer/iron-input/iron-input.js';
+import '../../../behaviors/docs-url-behavior/docs-url-behavior.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/gr-menu-page-styles.js';
+import '../../../styles/gr-page-nav-styles.js';
+import '../../../styles/shared-styles.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../gr-change-table-editor/gr-change-table-editor.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
+import '../../shared/gr-diff-preferences/gr-diff-preferences.js';
+import '../../shared/gr-page-nav/gr-page-nav.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../shared/gr-select/gr-select.js';
+import '../gr-account-info/gr-account-info.js';
+import '../gr-agreements-list/gr-agreements-list.js';
+import '../gr-edit-preferences/gr-edit-preferences.js';
+import '../gr-email-editor/gr-email-editor.js';
+import '../gr-gpg-editor/gr-gpg-editor.js';
+import '../gr-group-list/gr-group-list.js';
+import '../gr-http-password/gr-http-password.js';
+import '../gr-identities/gr-identities.js';
+import '../gr-menu-editor/gr-menu-editor.js';
+import '../gr-ssh-editor/gr-ssh-editor.js';
+import '../gr-watched-projects-editor/gr-watched-projects-editor.js';
+import '../../../scripts/util.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-settings-view_html.js';
- const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' +
- 'Documentation';
- const GERRIT_DOCS_FILTER_PATH = '/user-notify.html';
- const ABSOLUTE_URL_PATTERN = /^https?:/;
- const TRAILING_SLASH_PATTERN = /\/$/;
+const PREFS_SECTION_FIELDS = [
+ 'changes_per_page',
+ 'date_format',
+ 'time_format',
+ 'email_strategy',
+ 'diff_view',
+ 'publish_comments_on_push',
+ 'work_in_progress_by_default',
+ 'default_base_for_merges',
+ 'signed_off_by',
+ 'email_format',
+ 'size_bar_in_change_table',
+ 'relative_date_in_change_table',
+];
- const RELOAD_MESSAGE = 'Reloading...';
+const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' +
+ 'Documentation';
+const GERRIT_DOCS_FILTER_PATH = '/user-notify.html';
+const ABSOLUTE_URL_PATTERN = /^https?:/;
+const TRAILING_SLASH_PATTERN = /\/$/;
- const HTTP_AUTH = [
- 'HTTP',
- 'HTTP_LDAP',
- ];
+const RELOAD_MESSAGE = 'Reloading...';
+
+const HTTP_AUTH = [
+ 'HTTP',
+ 'HTTP_LDAP',
+];
+
+/**
+ * @appliesMixin Gerrit.DocsUrlMixin
+ * @appliesMixin Gerrit.ChangeTableMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrSettingsView extends mixinBehaviors( [
+ Gerrit.DocsUrlBehavior,
+ Gerrit.ChangeTableBehavior,
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-settings-view'; }
+ /**
+ * Fired when the title of the page should change.
+ *
+ * @event title-change
+ */
/**
- * @appliesMixin Gerrit.DocsUrlMixin
- * @appliesMixin Gerrit.ChangeTableMixin
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired with email confirmation text, or when the page reloads.
+ *
+ * @event show-alert
*/
- class GrSettingsView extends Polymer.mixinBehaviors( [
- Gerrit.DocsUrlBehavior,
- Gerrit.ChangeTableBehavior,
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-settings-view'; }
- /**
- * Fired when the title of the page should change.
- *
- * @event title-change
- */
- /**
- * Fired with email confirmation text, or when the page reloads.
- *
- * @event show-alert
- */
+ static get properties() {
+ return {
+ prefs: {
+ type: Object,
+ value() { return {}; },
+ },
+ params: {
+ type: Object,
+ value() { return {}; },
+ },
+ _accountInfoChanged: Boolean,
+ _changeTableColumnsNotDisplayed: Array,
+ /** @type {?} */
+ _localPrefs: {
+ type: Object,
+ value() { return {}; },
+ },
+ _localChangeTableColumns: {
+ type: Array,
+ value() { return []; },
+ },
+ _localMenu: {
+ type: Array,
+ value() { return []; },
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _changeTableChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _prefsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?} */
+ _diffPrefsChanged: Boolean,
+ /** @type {?} */
+ _editPrefsChanged: Boolean,
+ _menuChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _watchedProjectsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _keysChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _gpgKeysChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _newEmail: String,
+ _addingEmail: {
+ type: Boolean,
+ value: false,
+ },
+ _lastSentVerificationEmail: {
+ type: String,
+ value: null,
+ },
+ /** @type {?} */
+ _serverConfig: Object,
+ /** @type {?string} */
+ _docsBaseUrl: String,
+ _emailsChanged: Boolean,
- static get properties() {
- return {
- prefs: {
- type: Object,
- value() { return {}; },
- },
- params: {
- type: Object,
- value() { return {}; },
- },
- _accountInfoChanged: Boolean,
- _changeTableColumnsNotDisplayed: Array,
- /** @type {?} */
- _localPrefs: {
- type: Object,
- value() { return {}; },
- },
- _localChangeTableColumns: {
- type: Array,
- value() { return []; },
- },
- _localMenu: {
- type: Array,
- value() { return []; },
- },
- _loading: {
- type: Boolean,
- value: true,
- },
- _changeTableChanged: {
- type: Boolean,
- value: false,
- },
- _prefsChanged: {
- type: Boolean,
- value: false,
- },
- /** @type {?} */
- _diffPrefsChanged: Boolean,
- /** @type {?} */
- _editPrefsChanged: Boolean,
- _menuChanged: {
- type: Boolean,
- value: false,
- },
- _watchedProjectsChanged: {
- type: Boolean,
- value: false,
- },
- _keysChanged: {
- type: Boolean,
- value: false,
- },
- _gpgKeysChanged: {
- type: Boolean,
- value: false,
- },
- _newEmail: String,
- _addingEmail: {
- type: Boolean,
- value: false,
- },
- _lastSentVerificationEmail: {
- type: String,
- value: null,
- },
- /** @type {?} */
- _serverConfig: Object,
- /** @type {?string} */
- _docsBaseUrl: String,
- _emailsChanged: Boolean,
+ /**
+ * For testing purposes.
+ */
+ _loadingPromise: Object,
- /**
- * For testing purposes.
- */
- _loadingPromise: Object,
+ _showNumber: Boolean,
- _showNumber: Boolean,
+ _isDark: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- _isDark: {
- type: Boolean,
- value: false,
- },
- };
- }
+ static get observers() {
+ return [
+ '_handlePrefsChanged(_localPrefs.*)',
+ '_handleMenuChanged(_localMenu.splices)',
+ '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
+ ];
+ }
- static get observers() {
- return [
- '_handlePrefsChanged(_localPrefs.*)',
- '_handleMenuChanged(_localMenu.splices)',
- '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)',
- ];
- }
+ /** @override */
+ attached() {
+ super.attached();
+ // Polymer 2: anchor tag won't work on shadow DOM
+ // we need to manually calling scrollIntoView when hash changed
+ this.listen(window, 'location-change', '_handleLocationChange');
+ this.fire('title-change', {title: 'Settings'});
- /** @override */
- attached() {
- super.attached();
- // Polymer 2: anchor tag won't work on shadow DOM
- // we need to manually calling scrollIntoView when hash changed
- this.listen(window, 'location-change', '_handleLocationChange');
- this.fire('title-change', {title: 'Settings'});
+ this._isDark = !!window.localStorage.getItem('dark-theme');
- this._isDark = !!window.localStorage.getItem('dark-theme');
+ const promises = [
+ this.$.accountInfo.loadData(),
+ this.$.watchedProjectsEditor.loadData(),
+ this.$.groupList.loadData(),
+ this.$.identities.loadData(),
+ this.$.editPrefs.loadData(),
+ this.$.diffPrefs.loadData(),
+ ];
- const promises = [
- this.$.accountInfo.loadData(),
- this.$.watchedProjectsEditor.loadData(),
- this.$.groupList.loadData(),
- this.$.identities.loadData(),
- this.$.editPrefs.loadData(),
- this.$.diffPrefs.loadData(),
- ];
-
- promises.push(this.$.restAPI.getPreferences().then(prefs => {
- this.prefs = prefs;
- this._showNumber = !!prefs.legacycid_in_change_table;
- this._copyPrefs('_localPrefs', 'prefs');
- this._cloneMenu(prefs.my);
- this._cloneChangeTableColumns();
- }));
-
- promises.push(this.$.restAPI.getConfig().then(config => {
- this._serverConfig = config;
- const configPromises = [];
-
- if (this._serverConfig && this._serverConfig.sshd) {
- configPromises.push(this.$.sshEditor.loadData());
- }
-
- if (this._serverConfig &&
- this._serverConfig.receive &&
- this._serverConfig.receive.enable_signed_push) {
- configPromises.push(this.$.gpgEditor.loadData());
- }
-
- configPromises.push(
- this.getDocsBaseUrl(config, this.$.restAPI)
- .then(baseUrl => { this._docsBaseUrl = baseUrl; }));
-
- return Promise.all(configPromises);
- }));
-
- if (this.params.emailToken) {
- promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then(
- message => {
- if (message) {
- this.fire('show-alert', {message});
- }
- this.$.emailEditor.loadData();
- }));
- } else {
- promises.push(this.$.emailEditor.loadData());
- }
-
- this._loadingPromise = Promise.all(promises).then(() => {
- this._loading = false;
-
- // Handle anchor tag for initial load
- this._handleLocationChange();
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'location-change', '_handleLocationChange');
- }
-
- _handleLocationChange() {
- // Handle anchor tag after dom attached
- const urlHash = window.location.hash;
- if (urlHash) {
- // Use shadowRoot for Polymer 2
- const elem = (this.shadowRoot || document).querySelector(urlHash);
- if (elem) {
- elem.scrollIntoView();
- }
- }
- }
-
- reloadAccountDetail() {
- Promise.all([
- this.$.accountInfo.loadData(),
- this.$.emailEditor.loadData(),
- ]);
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _copyPrefs(to, from) {
- for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) {
- this.set([to, PREFS_SECTION_FIELDS[i]],
- this[from][PREFS_SECTION_FIELDS[i]]);
- }
- }
-
- _cloneMenu(prefs) {
- const menu = [];
- for (const item of prefs) {
- menu.push({
- name: item.name,
- url: item.url,
- target: item.target,
- });
- }
- this._localMenu = menu;
- }
-
- _cloneChangeTableColumns() {
- let columns = this.getVisibleColumns(this.prefs.change_table);
-
- if (columns.length === 0) {
- columns = this.columnNames;
- this._changeTableColumnsNotDisplayed = [];
- } else {
- this._changeTableColumnsNotDisplayed = this.getComplementColumns(
- this.prefs.change_table);
- }
- this._localChangeTableColumns = columns;
- }
-
- _formatChangeTableColumns(changeTableArray) {
- return changeTableArray.map(item => {
- return {column: item};
- });
- }
-
- _handleChangeTableChanged() {
- if (this._isLoading()) { return; }
- this._changeTableChanged = true;
- }
-
- _handlePrefsChanged(prefs) {
- if (this._isLoading()) { return; }
- this._prefsChanged = true;
- }
-
- _handleRelativeDateInChangeTable() {
- this.set('_localPrefs.relative_date_in_change_table',
- this.$.relativeDateInChangeTable.checked);
- }
-
- _handleShowSizeBarsInFileListChanged() {
- this.set('_localPrefs.size_bar_in_change_table',
- this.$.showSizeBarsInFileList.checked);
- }
-
- _handlePublishCommentsOnPushChanged() {
- this.set('_localPrefs.publish_comments_on_push',
- this.$.publishCommentsOnPush.checked);
- }
-
- _handleWorkInProgressByDefault() {
- this.set('_localPrefs.work_in_progress_by_default',
- this.$.workInProgressByDefault.checked);
- }
-
- _handleInsertSignedOff() {
- this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
- }
-
- _handleMenuChanged() {
- if (this._isLoading()) { return; }
- this._menuChanged = true;
- }
-
- _handleSaveAccountInfo() {
- this.$.accountInfo.save();
- }
-
- _handleSavePreferences() {
- this._copyPrefs('prefs', '_localPrefs');
-
- return this.$.restAPI.savePreferences(this.prefs).then(() => {
- this._prefsChanged = false;
- });
- }
-
- _handleSaveChangeTable() {
- this.set('prefs.change_table', this._localChangeTableColumns);
- this.set('prefs.legacycid_in_change_table', this._showNumber);
+ promises.push(this.$.restAPI.getPreferences().then(prefs => {
+ this.prefs = prefs;
+ this._showNumber = !!prefs.legacycid_in_change_table;
+ this._copyPrefs('_localPrefs', 'prefs');
+ this._cloneMenu(prefs.my);
this._cloneChangeTableColumns();
- return this.$.restAPI.savePreferences(this.prefs).then(() => {
- this._changeTableChanged = false;
- });
- }
+ }));
- _handleSaveDiffPreferences() {
- this.$.diffPrefs.save();
- }
+ promises.push(this.$.restAPI.getConfig().then(config => {
+ this._serverConfig = config;
+ const configPromises = [];
- _handleSaveEditPreferences() {
- this.$.editPrefs.save();
- }
-
- _handleSaveMenu() {
- this.set('prefs.my', this._localMenu);
- this._cloneMenu(this.prefs.my);
- return this.$.restAPI.savePreferences(this.prefs).then(() => {
- this._menuChanged = false;
- });
- }
-
- _handleResetMenuButton() {
- return this.$.restAPI.getDefaultPreferences().then(data => {
- if (data && data.my) {
- this._cloneMenu(data.my);
- }
- });
- }
-
- _handleSaveWatchedProjects() {
- this.$.watchedProjectsEditor.save();
- }
-
- _computeHeaderClass(changed) {
- return changed ? 'edited' : '';
- }
-
- _handleSaveEmails() {
- this.$.emailEditor.save();
- }
-
- _handleNewEmailKeydown(e) {
- if (e.keyCode === 13) { // Enter
- e.stopPropagation();
- this._handleAddEmailButton();
- }
- }
-
- _isNewEmailValid(newEmail) {
- return newEmail && newEmail.includes('@');
- }
-
- _computeAddEmailButtonEnabled(newEmail, addingEmail) {
- return this._isNewEmailValid(newEmail) && !addingEmail;
- }
-
- _handleAddEmailButton() {
- if (!this._isNewEmailValid(this._newEmail)) { return; }
-
- this._addingEmail = true;
- this.$.restAPI.addAccountEmail(this._newEmail).then(response => {
- this._addingEmail = false;
-
- // If it was unsuccessful.
- if (response.status < 200 || response.status >= 300) { return; }
-
- this._lastSentVerificationEmail = this._newEmail;
- this._newEmail = '';
- });
- }
-
- _getFilterDocsLink(docsBaseUrl) {
- let base = docsBaseUrl;
- if (!base || !ABSOLUTE_URL_PATTERN.test(base)) {
- base = GERRIT_DOCS_BASE_URL;
+ if (this._serverConfig && this._serverConfig.sshd) {
+ configPromises.push(this.$.sshEditor.loadData());
}
- // Remove any trailing slash, since it is in the GERRIT_DOCS_FILTER_PATH.
- base = base.replace(TRAILING_SLASH_PATTERN, '');
-
- return base + GERRIT_DOCS_FILTER_PATH;
- }
-
- _handleToggleDark() {
- if (this._isDark) {
- window.localStorage.removeItem('dark-theme');
- } else {
- window.localStorage.setItem('dark-theme', 'true');
- }
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: RELOAD_MESSAGE},
- bubbles: true,
- composed: true,
- }));
- this.async(() => {
- window.location.reload();
- }, 1);
- }
-
- _showHttpAuth(config) {
- if (config && config.auth &&
- config.auth.git_basic_auth_policy) {
- return HTTP_AUTH.includes(
- config.auth.git_basic_auth_policy.toUpperCase());
+ if (this._serverConfig &&
+ this._serverConfig.receive &&
+ this._serverConfig.receive.enable_signed_push) {
+ configPromises.push(this.$.gpgEditor.loadData());
}
- return false;
+ configPromises.push(
+ this.getDocsBaseUrl(config, this.$.restAPI)
+ .then(baseUrl => { this._docsBaseUrl = baseUrl; }));
+
+ return Promise.all(configPromises);
+ }));
+
+ if (this.params.emailToken) {
+ promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then(
+ message => {
+ if (message) {
+ this.fire('show-alert', {message});
+ }
+ this.$.emailEditor.loadData();
+ }));
+ } else {
+ promises.push(this.$.emailEditor.loadData());
+ }
+
+ this._loadingPromise = Promise.all(promises).then(() => {
+ this._loading = false;
+
+ // Handle anchor tag for initial load
+ this._handleLocationChange();
+ });
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'location-change', '_handleLocationChange');
+ }
+
+ _handleLocationChange() {
+ // Handle anchor tag after dom attached
+ const urlHash = window.location.hash;
+ if (urlHash) {
+ // Use shadowRoot for Polymer 2
+ const elem = (this.shadowRoot || document).querySelector(urlHash);
+ if (elem) {
+ elem.scrollIntoView();
+ }
}
}
- customElements.define(GrSettingsView.is, GrSettingsView);
-})();
+ reloadAccountDetail() {
+ Promise.all([
+ this.$.accountInfo.loadData(),
+ this.$.emailEditor.loadData(),
+ ]);
+ }
+
+ _isLoading() {
+ return this._loading || this._loading === undefined;
+ }
+
+ _copyPrefs(to, from) {
+ for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) {
+ this.set([to, PREFS_SECTION_FIELDS[i]],
+ this[from][PREFS_SECTION_FIELDS[i]]);
+ }
+ }
+
+ _cloneMenu(prefs) {
+ const menu = [];
+ for (const item of prefs) {
+ menu.push({
+ name: item.name,
+ url: item.url,
+ target: item.target,
+ });
+ }
+ this._localMenu = menu;
+ }
+
+ _cloneChangeTableColumns() {
+ let columns = this.getVisibleColumns(this.prefs.change_table);
+
+ if (columns.length === 0) {
+ columns = this.columnNames;
+ this._changeTableColumnsNotDisplayed = [];
+ } else {
+ this._changeTableColumnsNotDisplayed = this.getComplementColumns(
+ this.prefs.change_table);
+ }
+ this._localChangeTableColumns = columns;
+ }
+
+ _formatChangeTableColumns(changeTableArray) {
+ return changeTableArray.map(item => {
+ return {column: item};
+ });
+ }
+
+ _handleChangeTableChanged() {
+ if (this._isLoading()) { return; }
+ this._changeTableChanged = true;
+ }
+
+ _handlePrefsChanged(prefs) {
+ if (this._isLoading()) { return; }
+ this._prefsChanged = true;
+ }
+
+ _handleRelativeDateInChangeTable() {
+ this.set('_localPrefs.relative_date_in_change_table',
+ this.$.relativeDateInChangeTable.checked);
+ }
+
+ _handleShowSizeBarsInFileListChanged() {
+ this.set('_localPrefs.size_bar_in_change_table',
+ this.$.showSizeBarsInFileList.checked);
+ }
+
+ _handlePublishCommentsOnPushChanged() {
+ this.set('_localPrefs.publish_comments_on_push',
+ this.$.publishCommentsOnPush.checked);
+ }
+
+ _handleWorkInProgressByDefault() {
+ this.set('_localPrefs.work_in_progress_by_default',
+ this.$.workInProgressByDefault.checked);
+ }
+
+ _handleInsertSignedOff() {
+ this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
+ }
+
+ _handleMenuChanged() {
+ if (this._isLoading()) { return; }
+ this._menuChanged = true;
+ }
+
+ _handleSaveAccountInfo() {
+ this.$.accountInfo.save();
+ }
+
+ _handleSavePreferences() {
+ this._copyPrefs('prefs', '_localPrefs');
+
+ return this.$.restAPI.savePreferences(this.prefs).then(() => {
+ this._prefsChanged = false;
+ });
+ }
+
+ _handleSaveChangeTable() {
+ this.set('prefs.change_table', this._localChangeTableColumns);
+ this.set('prefs.legacycid_in_change_table', this._showNumber);
+ this._cloneChangeTableColumns();
+ return this.$.restAPI.savePreferences(this.prefs).then(() => {
+ this._changeTableChanged = false;
+ });
+ }
+
+ _handleSaveDiffPreferences() {
+ this.$.diffPrefs.save();
+ }
+
+ _handleSaveEditPreferences() {
+ this.$.editPrefs.save();
+ }
+
+ _handleSaveMenu() {
+ this.set('prefs.my', this._localMenu);
+ this._cloneMenu(this.prefs.my);
+ return this.$.restAPI.savePreferences(this.prefs).then(() => {
+ this._menuChanged = false;
+ });
+ }
+
+ _handleResetMenuButton() {
+ return this.$.restAPI.getDefaultPreferences().then(data => {
+ if (data && data.my) {
+ this._cloneMenu(data.my);
+ }
+ });
+ }
+
+ _handleSaveWatchedProjects() {
+ this.$.watchedProjectsEditor.save();
+ }
+
+ _computeHeaderClass(changed) {
+ return changed ? 'edited' : '';
+ }
+
+ _handleSaveEmails() {
+ this.$.emailEditor.save();
+ }
+
+ _handleNewEmailKeydown(e) {
+ if (e.keyCode === 13) { // Enter
+ e.stopPropagation();
+ this._handleAddEmailButton();
+ }
+ }
+
+ _isNewEmailValid(newEmail) {
+ return newEmail && newEmail.includes('@');
+ }
+
+ _computeAddEmailButtonEnabled(newEmail, addingEmail) {
+ return this._isNewEmailValid(newEmail) && !addingEmail;
+ }
+
+ _handleAddEmailButton() {
+ if (!this._isNewEmailValid(this._newEmail)) { return; }
+
+ this._addingEmail = true;
+ this.$.restAPI.addAccountEmail(this._newEmail).then(response => {
+ this._addingEmail = false;
+
+ // If it was unsuccessful.
+ if (response.status < 200 || response.status >= 300) { return; }
+
+ this._lastSentVerificationEmail = this._newEmail;
+ this._newEmail = '';
+ });
+ }
+
+ _getFilterDocsLink(docsBaseUrl) {
+ let base = docsBaseUrl;
+ if (!base || !ABSOLUTE_URL_PATTERN.test(base)) {
+ base = GERRIT_DOCS_BASE_URL;
+ }
+
+ // Remove any trailing slash, since it is in the GERRIT_DOCS_FILTER_PATH.
+ base = base.replace(TRAILING_SLASH_PATTERN, '');
+
+ return base + GERRIT_DOCS_FILTER_PATH;
+ }
+
+ _handleToggleDark() {
+ if (this._isDark) {
+ window.localStorage.removeItem('dark-theme');
+ } else {
+ window.localStorage.setItem('dark-theme', 'true');
+ }
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: RELOAD_MESSAGE},
+ bubbles: true,
+ composed: true,
+ }));
+ this.async(() => {
+ window.location.reload();
+ }, 1);
+ }
+
+ _showHttpAuth(config) {
+ if (config && config.auth &&
+ config.auth.git_basic_auth_policy) {
+ return HTTP_AUTH.includes(
+ config.auth.git_basic_auth_policy.toUpperCase());
+ }
+
+ return false;
+ }
+}
+
+customElements.define(GrSettingsView.is, GrSettingsView);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
index 475a8e2..0f03ec1 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
@@ -1,53 +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="/bower_components/iron-input/iron-input.html">
-
-<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
-<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
-
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/gr-menu-page-styles.html">
-<link rel="import" href="../../../styles/gr-page-nav-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../settings/gr-change-table-editor/gr-change-table-editor.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-<link rel="import" href="../gr-account-info/gr-account-info.html">
-<link rel="import" href="../gr-agreements-list/gr-agreements-list.html">
-<link rel="import" href="../gr-edit-preferences/gr-edit-preferences.html">
-<link rel="import" href="../gr-email-editor/gr-email-editor.html">
-<link rel="import" href="../gr-gpg-editor/gr-gpg-editor.html">
-<link rel="import" href="../gr-group-list/gr-group-list.html">
-<link rel="import" href="../gr-http-password/gr-http-password.html">
-<link rel="import" href="../gr-identities/gr-identities.html">
-<link rel="import" href="../gr-menu-editor/gr-menu-editor.html">
-<link rel="import" href="../gr-ssh-editor/gr-ssh-editor.html">
-<link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html">
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-settings-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
color: var(--primary-text-color);
@@ -84,8 +53,8 @@
<style include="gr-page-nav-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
- <div class="loading" hidden$="[[!_loading]]">Loading...</div>
- <div hidden$="[[_loading]]" hidden>
+ <div class="loading" hidden\$="[[!_loading]]">Loading...</div>
+ <div hidden\$="[[_loading]]" hidden="">
<gr-page-nav class="navStyles">
<ul>
<li><a href="#Profile">Profile</a></li>
@@ -99,10 +68,10 @@
<template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
<li><a href="#HTTPCredentials">HTTP Credentials</a></li>
</template>
- <li hidden$="[[!_serverConfig.sshd]]"><a href="#SSHKeys">
+ <li hidden\$="[[!_serverConfig.sshd]]"><a href="#SSHKeys">
SSH Keys
</a></li>
- <li hidden$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys">
+ <li hidden\$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys">
GPG Keys
</a></li>
<li><a href="#Groups">Groups</a></li>
@@ -121,9 +90,7 @@
<h1>User Settings</h1>
<section class="darkToggle">
<div class="toggle">
- <paper-toggle-button
- checked="[[_isDark]]"
- on-change="_handleToggleDark"></paper-toggle-button>
+ <paper-toggle-button checked="[[_isDark]]" on-change="_handleToggleDark"></paper-toggle-button>
<div>Dark theme (alpha)</div>
</div>
<p>
@@ -132,26 +99,17 @@
feedback via the link in the app footer is strongly encouraged!
</p>
</section>
- <h2
- id="Profile"
- class$="[[_computeHeaderClass(_accountInfoChanged)]]">Profile</h2>
+ <h2 id="Profile" class\$="[[_computeHeaderClass(_accountInfoChanged)]]">Profile</h2>
<fieldset id="profile">
- <gr-account-info
- id="accountInfo"
- has-unsaved-changes="{{_accountInfoChanged}}"></gr-account-info>
- <gr-button
- on-click="_handleSaveAccountInfo"
- disabled="[[!_accountInfoChanged]]">Save changes</gr-button>
+ <gr-account-info id="accountInfo" has-unsaved-changes="{{_accountInfoChanged}}"></gr-account-info>
+ <gr-button on-click="_handleSaveAccountInfo" disabled="[[!_accountInfoChanged]]">Save changes</gr-button>
</fieldset>
- <h2
- id="Preferences"
- class$="[[_computeHeaderClass(_prefsChanged)]]">Preferences</h2>
+ <h2 id="Preferences" class\$="[[_computeHeaderClass(_prefsChanged)]]">Preferences</h2>
<fieldset id="preferences">
<section>
<span class="title">Changes per page</span>
<span class="value">
- <gr-select
- bind-value="{{_localPrefs.changes_per_page}}">
+ <gr-select bind-value="{{_localPrefs.changes_per_page}}">
<select>
<option value="10">10 rows per page</option>
<option value="25">25 rows per page</option>
@@ -164,8 +122,7 @@
<section>
<span class="title">Date/time format</span>
<span class="value">
- <gr-select
- bind-value="{{_localPrefs.date_format}}">
+ <gr-select bind-value="{{_localPrefs.date_format}}">
<select>
<option value="STD">Jun 3 ; Jun 3, 2016</option>
<option value="US">06/03 ; 06/03/16</option>
@@ -174,8 +131,7 @@
<option value="UK">03/06 ; 03/06/2016</option>
</select>
</gr-select>
- <gr-select
- bind-value="{{_localPrefs.time_format}}">
+ <gr-select bind-value="{{_localPrefs.time_format}}">
<select>
<option value="HHMM_12">4:10 PM</option>
<option value="HHMM_24">16:10</option>
@@ -186,8 +142,7 @@
<section>
<span class="title">Email notifications</span>
<span class="value">
- <gr-select
- bind-value="{{_localPrefs.email_strategy}}">
+ <gr-select bind-value="{{_localPrefs.email_strategy}}">
<select>
<option value="CC_ON_OWN_COMMENTS">Every comment</option>
<option value="ENABLED">Only comments left by others</option>
@@ -196,11 +151,10 @@
</gr-select>
</span>
</section>
- <section hidden$="[[!_localPrefs.email_format]]">
+ <section hidden\$="[[!_localPrefs.email_format]]">
<span class="title">Email format</span>
<span class="value">
- <gr-select
- bind-value="{{_localPrefs.email_format}}">
+ <gr-select bind-value="{{_localPrefs.email_format}}">
<select>
<option value="HTML_PLAINTEXT">HTML and plaintext</option>
<option value="PLAINTEXT">Plaintext only</option>
@@ -208,11 +162,10 @@
</gr-select>
</span>
</section>
- <section hidden$="[[!_localPrefs.default_base_for_merges]]">
+ <section hidden\$="[[!_localPrefs.default_base_for_merges]]">
<span class="title">Default Base For Merges</span>
<span class="value">
- <gr-select
- bind-value="{{_localPrefs.default_base_for_merges}}">
+ <gr-select bind-value="{{_localPrefs.default_base_for_merges}}">
<select>
<option value="AUTO_MERGE">Auto Merge</option>
<option value="FIRST_PARENT">First Parent</option>
@@ -223,18 +176,13 @@
<section>
<span class="title">Show Relative Dates In Changes Table</span>
<span class="value">
- <input
- id="relativeDateInChangeTable"
- type="checkbox"
- checked$="[[_localPrefs.relative_date_in_change_table]]"
- on-change="_handleRelativeDateInChangeTable">
+ <input id="relativeDateInChangeTable" type="checkbox" checked\$="[[_localPrefs.relative_date_in_change_table]]" on-change="_handleRelativeDateInChangeTable">
</span>
</section>
<section>
<span class="title">Diff view</span>
<span class="value">
- <gr-select
- bind-value="{{_localPrefs.diff_view}}">
+ <gr-select bind-value="{{_localPrefs.diff_view}}">
<select>
<option value="SIDE_BY_SIDE">Side by side</option>
<option value="UNIFIED_DIFF">Unified diff</option>
@@ -245,31 +193,19 @@
<section>
<span class="title">Show size bars in file list</span>
<span class="value">
- <input
- id="showSizeBarsInFileList"
- type="checkbox"
- checked$="[[_localPrefs.size_bar_in_change_table]]"
- on-change="_handleShowSizeBarsInFileListChanged">
+ <input id="showSizeBarsInFileList" type="checkbox" checked\$="[[_localPrefs.size_bar_in_change_table]]" on-change="_handleShowSizeBarsInFileListChanged">
</span>
</section>
<section>
<span class="title">Publish comments on push</span>
<span class="value">
- <input
- id="publishCommentsOnPush"
- type="checkbox"
- checked$="[[_localPrefs.publish_comments_on_push]]"
- on-change="_handlePublishCommentsOnPushChanged">
+ <input id="publishCommentsOnPush" type="checkbox" checked\$="[[_localPrefs.publish_comments_on_push]]" on-change="_handlePublishCommentsOnPushChanged">
</span>
</section>
<section>
<span class="title">Set new changes to "work in progress" by default</span>
<span class="value">
- <input
- id="workInProgressByDefault"
- type="checkbox"
- checked$="[[_localPrefs.work_in_progress_by_default]]"
- on-change="_handleWorkInProgressByDefault">
+ <input id="workInProgressByDefault" type="checkbox" checked\$="[[_localPrefs.work_in_progress_by_default]]" on-change="_handleWorkInProgressByDefault">
</span>
</section>
<section>
@@ -277,132 +213,69 @@
Insert Signed-off-by Footer For Inline Edit Changes
</span>
<span class="value">
- <input
- id="insertSignedOff"
- type="checkbox"
- checked$="[[_localPrefs.signed_off_by]]"
- on-change="_handleInsertSignedOff">
+ <input id="insertSignedOff" type="checkbox" checked\$="[[_localPrefs.signed_off_by]]" on-change="_handleInsertSignedOff">
</span>
</section>
- <gr-button
- id="savePrefs"
- on-click="_handleSavePreferences"
- disabled="[[!_prefsChanged]]">Save changes</gr-button>
+ <gr-button id="savePrefs" on-click="_handleSavePreferences" disabled="[[!_prefsChanged]]">Save changes</gr-button>
</fieldset>
- <h2
- id="DiffPreferences"
- class$="[[_computeHeaderClass(_diffPrefsChanged)]]">
+ <h2 id="DiffPreferences" class\$="[[_computeHeaderClass(_diffPrefsChanged)]]">
Diff Preferences
</h2>
<fieldset id="diffPreferences">
- <gr-diff-preferences
- id="diffPrefs"
- has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
- <gr-button
- id="saveDiffPrefs"
- on-click="_handleSaveDiffPreferences"
- disabled$="[[!_diffPrefsChanged]]">Save changes</gr-button>
+ <gr-diff-preferences id="diffPrefs" has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences>
+ <gr-button id="saveDiffPrefs" on-click="_handleSaveDiffPreferences" disabled\$="[[!_diffPrefsChanged]]">Save changes</gr-button>
</fieldset>
- <h2
- id="EditPreferences"
- class$="[[_computeHeaderClass(_editPrefsChanged)]]">
+ <h2 id="EditPreferences" class\$="[[_computeHeaderClass(_editPrefsChanged)]]">
Edit Preferences
</h2>
<fieldset id="editPreferences">
- <gr-edit-preferences
- id="editPrefs"
- has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences>
- <gr-button
- id="saveEditPrefs"
- on-click="_handleSaveEditPreferences"
- disabled$="[[!_editPrefsChanged]]">Save changes</gr-button>
+ <gr-edit-preferences id="editPrefs" has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences>
+ <gr-button id="saveEditPrefs" on-click="_handleSaveEditPreferences" disabled\$="[[!_editPrefsChanged]]">Save changes</gr-button>
</fieldset>
- <h2 id="Menu" class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2>
+ <h2 id="Menu" class\$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2>
<fieldset id="menu">
- <gr-menu-editor
- menu-items="{{_localMenu}}"></gr-menu-editor>
- <gr-button
- id="saveMenu"
- on-click="_handleSaveMenu"
- disabled="[[!_menuChanged]]">Save changes</gr-button>
- <gr-button
- id="resetMenu"
- link
- on-click="_handleResetMenuButton">Reset</gr-button>
+ <gr-menu-editor menu-items="{{_localMenu}}"></gr-menu-editor>
+ <gr-button id="saveMenu" on-click="_handleSaveMenu" disabled="[[!_menuChanged]]">Save changes</gr-button>
+ <gr-button id="resetMenu" link="" on-click="_handleResetMenuButton">Reset</gr-button>
</fieldset>
- <h2 id="ChangeTableColumns"
- class$="[[_computeHeaderClass(_changeTableChanged)]]">
+ <h2 id="ChangeTableColumns" class\$="[[_computeHeaderClass(_changeTableChanged)]]">
Change Table Columns
</h2>
<fieldset id="changeTableColumns">
- <gr-change-table-editor
- show-number="{{_showNumber}}"
- displayed-columns="{{_localChangeTableColumns}}">
+ <gr-change-table-editor show-number="{{_showNumber}}" displayed-columns="{{_localChangeTableColumns}}">
</gr-change-table-editor>
- <gr-button
- id="saveChangeTable"
- on-click="_handleSaveChangeTable"
- disabled="[[!_changeTableChanged]]">Save changes</gr-button>
+ <gr-button id="saveChangeTable" on-click="_handleSaveChangeTable" disabled="[[!_changeTableChanged]]">Save changes</gr-button>
</fieldset>
- <h2
- id="Notifications"
- class$="[[_computeHeaderClass(_watchedProjectsChanged)]]">
+ <h2 id="Notifications" class\$="[[_computeHeaderClass(_watchedProjectsChanged)]]">
Notifications
</h2>
<fieldset id="watchedProjects">
- <gr-watched-projects-editor
- has-unsaved-changes="{{_watchedProjectsChanged}}"
- id="watchedProjectsEditor"></gr-watched-projects-editor>
- <gr-button
- on-click="_handleSaveWatchedProjects"
- disabled$="[[!_watchedProjectsChanged]]"
- id="_handleSaveWatchedProjects">Save changes</gr-button>
+ <gr-watched-projects-editor has-unsaved-changes="{{_watchedProjectsChanged}}" id="watchedProjectsEditor"></gr-watched-projects-editor>
+ <gr-button on-click="_handleSaveWatchedProjects" disabled\$="[[!_watchedProjectsChanged]]" id="_handleSaveWatchedProjects">Save changes</gr-button>
</fieldset>
- <h2
- id="EmailAddresses"
- class$="[[_computeHeaderClass(_emailsChanged)]]">
+ <h2 id="EmailAddresses" class\$="[[_computeHeaderClass(_emailsChanged)]]">
Email Addresses
</h2>
<fieldset id="email">
- <gr-email-editor
- id="emailEditor"
- has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
- <gr-button
- on-click="_handleSaveEmails"
- disabled$="[[!_emailsChanged]]">Save changes</gr-button>
+ <gr-email-editor id="emailEditor" has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor>
+ <gr-button on-click="_handleSaveEmails" disabled\$="[[!_emailsChanged]]">Save changes</gr-button>
</fieldset>
<fieldset id="newEmail">
<section>
<span class="title">New email address</span>
<span class="value">
- <iron-input
- class="newEmailInput"
- bind-value="{{_newEmail}}"
- type="text"
- on-keydown="_handleNewEmailKeydown"
- placeholder="email@example.com">
- <input
- class="newEmailInput"
- bind-value="{{_newEmail}}"
- is="iron-input"
- type="text"
- disabled="[[_addingEmail]]"
- on-keydown="_handleNewEmailKeydown"
- placeholder="email@example.com">
+ <iron-input class="newEmailInput" bind-value="{{_newEmail}}" type="text" on-keydown="_handleNewEmailKeydown" placeholder="email@example.com">
+ <input class="newEmailInput" bind-value="{{_newEmail}}" is="iron-input" type="text" disabled="[[_addingEmail]]" on-keydown="_handleNewEmailKeydown" placeholder="email@example.com">
</iron-input>
</span>
</section>
- <section
- id="verificationSentMessage"
- hidden$="[[!_lastSentVerificationEmail]]">
+ <section id="verificationSentMessage" hidden\$="[[!_lastSentVerificationEmail]]">
<p>
A verification email was sent to
<em>[[_lastSentVerificationEmail]]</em>. Please check your inbox.
</p>
</section>
- <gr-button
- disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]"
- on-click="_handleAddEmailButton">Send verification</gr-button>
+ <gr-button disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]" on-click="_handleAddEmailButton">Send verification</gr-button>
</fieldset>
<template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]">
<div>
@@ -412,21 +285,13 @@
</fieldset>
</div>
</template>
- <div hidden$="[[!_serverConfig.sshd]]">
- <h2
- id="SSHKeys"
- class$="[[_computeHeaderClass(_keysChanged)]]">SSH keys</h2>
- <gr-ssh-editor
- id="sshEditor"
- has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor>
+ <div hidden\$="[[!_serverConfig.sshd]]">
+ <h2 id="SSHKeys" class\$="[[_computeHeaderClass(_keysChanged)]]">SSH keys</h2>
+ <gr-ssh-editor id="sshEditor" has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor>
</div>
- <div hidden$="[[!_serverConfig.receive.enable_signed_push]]">
- <h2
- id="GPGKeys"
- class$="[[_computeHeaderClass(_gpgKeysChanged)]]">GPG keys</h2>
- <gr-gpg-editor
- id="gpgEditor"
- has-unsaved-changes="{{_gpgKeysChanged}}"></gr-gpg-editor>
+ <div hidden\$="[[!_serverConfig.receive.enable_signed_push]]">
+ <h2 id="GPGKeys" class\$="[[_computeHeaderClass(_gpgKeysChanged)]]">GPG keys</h2>
+ <gr-gpg-editor id="gpgEditor" has-unsaved-changes="{{_gpgKeysChanged}}"></gr-gpg-editor>
</div>
<h2 id="Groups">Groups</h2>
<fieldset>
@@ -451,9 +316,7 @@
<p>
Here are some example Gmail queries that can be used for filters or
for searching through archived messages. View the
- <a href$="[[_getFilterDocsLink(_docsBaseUrl)]]"
- target="_blank"
- rel="nofollow">Gerrit documentation</a>
+ <a href\$="[[_getFilterDocsLink(_docsBaseUrl)]]" target="_blank" rel="nofollow">Gerrit documentation</a>
for the complete set of footers.
</p>
<table>
@@ -517,6 +380,4 @@
</main>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-settings-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index cd268c6..a014c96 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_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-settings-view</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-settings-view.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-settings-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-settings-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -41,488 +46,491 @@
</template>
</test-fixture>
-<script>
- suite('gr-settings-view tests', async () => {
- await readyToTest();
- let element;
- let account;
- let preferences;
- let config;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-settings-view.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-settings-view tests', () => {
+ let element;
+ let account;
+ let preferences;
+ let config;
+ let sandbox;
- function valueOf(title, fieldsetid) {
- const sections = element.$[fieldsetid].querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent.trim() === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title, fieldsetid) {
+ const sections = element.$[fieldsetid].querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent.trim() === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- // Because deepEqual isn't behaving in Safari.
- function assertMenusEqual(actual, expected) {
- assert.equal(actual.length, expected.length);
- for (let i = 0; i < actual.length; i++) {
- assert.equal(actual[i].name, expected[i].name);
- assert.equal(actual[i].url, expected[i].url);
- }
+ // Because deepEqual isn't behaving in Safari.
+ function assertMenusEqual(actual, expected) {
+ assert.equal(actual.length, expected.length);
+ for (let i = 0; i < actual.length; i++) {
+ assert.equal(actual[i].name, expected[i].name);
+ assert.equal(actual[i].url, expected[i].url);
}
+ }
- function stubAddAccountEmail(statusCode) {
- return sandbox.stub(element.$.restAPI, 'addAccountEmail',
- () => Promise.resolve({status: statusCode}));
- }
+ function stubAddAccountEmail(statusCode) {
+ return sandbox.stub(element.$.restAPI, 'addAccountEmail',
+ () => Promise.resolve({status: statusCode}));
+ }
- setup(done => {
- sandbox = sinon.sandbox.create();
- account = {
- _account_id: 123,
- name: 'user name',
- email: 'user@email',
- username: 'user username',
- registered: '2000-01-01 00:00:00.000000000',
- };
- preferences = {
- changes_per_page: 25,
- date_format: 'UK',
- time_format: 'HHMM_12',
- diff_view: 'UNIFIED_DIFF',
- email_strategy: 'ENABLED',
- email_format: 'HTML_PLAINTEXT',
- default_base_for_merges: 'FIRST_PARENT',
- relative_date_in_change_table: false,
- size_bar_in_change_table: true,
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ account = {
+ _account_id: 123,
+ name: 'user name',
+ email: 'user@email',
+ username: 'user username',
+ registered: '2000-01-01 00:00:00.000000000',
+ };
+ preferences = {
+ changes_per_page: 25,
+ date_format: 'UK',
+ time_format: 'HHMM_12',
+ diff_view: 'UNIFIED_DIFF',
+ email_strategy: 'ENABLED',
+ email_format: 'HTML_PLAINTEXT',
+ default_base_for_merges: 'FIRST_PARENT',
+ relative_date_in_change_table: false,
+ size_bar_in_change_table: true,
- my: [
- {url: '/first/url', name: 'first name', target: '_blank'},
- {url: '/second/url', name: 'second name', target: '_blank'},
- ],
- change_table: [],
- };
- config = {auth: {editable_account_fields: []}};
+ my: [
+ {url: '/first/url', name: 'first name', target: '_blank'},
+ {url: '/second/url', name: 'second name', target: '_blank'},
+ ],
+ change_table: [],
+ };
+ config = {auth: {editable_account_fields: []}};
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getAccount() { return Promise.resolve(account); },
- getPreferences() { return Promise.resolve(preferences); },
- getWatchedProjects() {
- return Promise.resolve([]);
- },
- getAccountEmails() { return Promise.resolve(); },
- getConfig() { return Promise.resolve(config); },
- getAccountGroups() { return Promise.resolve([]); },
- });
- element = fixture('basic');
-
- // Allow the element to render.
- element._loadingPromise.then(done);
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getAccount() { return Promise.resolve(account); },
+ getPreferences() { return Promise.resolve(preferences); },
+ getWatchedProjects() {
+ return Promise.resolve([]);
+ },
+ getAccountEmails() { return Promise.resolve(); },
+ getConfig() { return Promise.resolve(config); },
+ getAccountGroups() { return Promise.resolve([]); },
});
+ element = fixture('basic');
- teardown(() => {
- sandbox.restore();
- });
+ // Allow the element to render.
+ element._loadingPromise.then(done);
+ });
- test('calls the title-change event', () => {
- const titleChangedStub = sandbox.stub();
+ teardown(() => {
+ sandbox.restore();
+ });
- // Create a new view.
- const newElement = document.createElement('gr-settings-view');
- newElement.addEventListener('title-change', titleChangedStub);
+ test('calls the title-change event', () => {
+ const titleChangedStub = sandbox.stub();
- // Attach it to the fixture.
- const blank = fixture('blank');
- blank.appendChild(newElement);
+ // Create a new view.
+ const newElement = document.createElement('gr-settings-view');
+ newElement.addEventListener('title-change', titleChangedStub);
- Polymer.dom.flush();
+ // Attach it to the fixture.
+ const blank = fixture('blank');
+ blank.appendChild(newElement);
- assert.isTrue(titleChangedStub.called);
- assert.equal(titleChangedStub.getCall(0).args[0].detail.title,
- 'Settings');
- });
+ flush();
- test('user preferences', done => {
- // Rendered with the expected preferences selected.
- assert.equal(valueOf('Changes per page', 'preferences')
- .firstElementChild.bindValue, preferences.changes_per_page);
- assert.equal(valueOf('Date/time format', 'preferences')
- .firstElementChild.bindValue, preferences.date_format);
- assert.equal(valueOf('Date/time format', 'preferences')
- .lastElementChild.bindValue, preferences.time_format);
- assert.equal(valueOf('Email notifications', 'preferences')
- .firstElementChild.bindValue, preferences.email_strategy);
- assert.equal(valueOf('Email format', 'preferences')
- .firstElementChild.bindValue, preferences.email_format);
- assert.equal(valueOf('Default Base For Merges', 'preferences')
- .firstElementChild.bindValue, preferences.default_base_for_merges);
- assert.equal(
- valueOf('Show Relative Dates In Changes Table', 'preferences')
- .firstElementChild.checked, false);
- assert.equal(valueOf('Diff view', 'preferences')
- .firstElementChild.bindValue, preferences.diff_view);
- assert.equal(valueOf('Show size bars in file list', 'preferences')
- .firstElementChild.checked, true);
- assert.equal(valueOf('Publish comments on push', 'preferences')
- .firstElementChild.checked, false);
- assert.equal(valueOf(
- 'Set new changes to "work in progress" by default', 'preferences')
- .firstElementChild.checked, false);
- assert.equal(valueOf(
- 'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences')
- .firstElementChild.checked, false);
+ assert.isTrue(titleChangedStub.called);
+ assert.equal(titleChangedStub.getCall(0).args[0].detail.title,
+ 'Settings');
+ });
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
+ test('user preferences', done => {
+ // Rendered with the expected preferences selected.
+ assert.equal(valueOf('Changes per page', 'preferences')
+ .firstElementChild.bindValue, preferences.changes_per_page);
+ assert.equal(valueOf('Date/time format', 'preferences')
+ .firstElementChild.bindValue, preferences.date_format);
+ assert.equal(valueOf('Date/time format', 'preferences')
+ .lastElementChild.bindValue, preferences.time_format);
+ assert.equal(valueOf('Email notifications', 'preferences')
+ .firstElementChild.bindValue, preferences.email_strategy);
+ assert.equal(valueOf('Email format', 'preferences')
+ .firstElementChild.bindValue, preferences.email_format);
+ assert.equal(valueOf('Default Base For Merges', 'preferences')
+ .firstElementChild.bindValue, preferences.default_base_for_merges);
+ assert.equal(
+ valueOf('Show Relative Dates In Changes Table', 'preferences')
+ .firstElementChild.checked, false);
+ assert.equal(valueOf('Diff view', 'preferences')
+ .firstElementChild.bindValue, preferences.diff_view);
+ assert.equal(valueOf('Show size bars in file list', 'preferences')
+ .firstElementChild.checked, true);
+ assert.equal(valueOf('Publish comments on push', 'preferences')
+ .firstElementChild.checked, false);
+ assert.equal(valueOf(
+ 'Set new changes to "work in progress" by default', 'preferences')
+ .firstElementChild.checked, false);
+ assert.equal(valueOf(
+ 'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences')
+ .firstElementChild.checked, false);
- // Change the diff view element.
- const diffSelect = valueOf('Diff view', 'preferences').firstElementChild;
- diffSelect.bindValue = 'SIDE_BY_SIDE';
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
- const publishOnPush =
- valueOf('Publish comments on push', 'preferences').firstElementChild;
- diffSelect.fire('change');
+ // Change the diff view element.
+ const diffSelect = valueOf('Diff view', 'preferences').firstElementChild;
+ diffSelect.bindValue = 'SIDE_BY_SIDE';
- MockInteractions.tap(publishOnPush);
-
- assert.isTrue(element._prefsChanged);
- assert.isFalse(element._menuChanged);
-
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assert.equal(prefs.diff_view, 'SIDE_BY_SIDE');
- assertMenusEqual(prefs.my, preferences.my);
- assert.equal(prefs.publish_comments_on_push, true);
- return Promise.resolve();
- },
- });
-
- // Save the change.
- element._handleSavePreferences().then(() => {
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
- done();
- });
- });
-
- test('publish comments on push', done => {
- const publishCommentsOnPush =
+ const publishOnPush =
valueOf('Publish comments on push', 'preferences').firstElementChild;
- MockInteractions.tap(publishCommentsOnPush);
+ diffSelect.fire('change');
- assert.isFalse(element._menuChanged);
- assert.isTrue(element._prefsChanged);
+ MockInteractions.tap(publishOnPush);
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assert.equal(prefs.publish_comments_on_push, true);
- return Promise.resolve();
- },
- });
+ assert.isTrue(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
- // Save the change.
- element._handleSavePreferences().then(() => {
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
- done();
- });
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assert.equal(prefs.diff_view, 'SIDE_BY_SIDE');
+ assertMenusEqual(prefs.my, preferences.my);
+ assert.equal(prefs.publish_comments_on_push, true);
+ return Promise.resolve();
+ },
});
- test('set new changes work-in-progress', done => {
- const newChangesWorkInProgress =
- valueOf('Set new changes to "work in progress" by default',
- 'preferences').firstElementChild;
- MockInteractions.tap(newChangesWorkInProgress);
-
+ // Save the change.
+ element._handleSavePreferences().then(() => {
+ assert.isFalse(element._prefsChanged);
assert.isFalse(element._menuChanged);
- assert.isTrue(element._prefsChanged);
+ done();
+ });
+ });
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assert.equal(prefs.work_in_progress_by_default, true);
- return Promise.resolve();
- },
- });
+ test('publish comments on push', done => {
+ const publishCommentsOnPush =
+ valueOf('Publish comments on push', 'preferences').firstElementChild;
+ MockInteractions.tap(publishCommentsOnPush);
- // Save the change.
- element._handleSavePreferences().then(() => {
- assert.isFalse(element._prefsChanged);
- assert.isFalse(element._menuChanged);
- done();
- });
+ assert.isFalse(element._menuChanged);
+ assert.isTrue(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assert.equal(prefs.publish_comments_on_push, true);
+ return Promise.resolve();
+ },
});
- test('menu', done => {
+ // Save the change.
+ element._handleSavePreferences().then(() => {
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
+ done();
+ });
+ });
+
+ test('set new changes work-in-progress', done => {
+ const newChangesWorkInProgress =
+ valueOf('Set new changes to "work in progress" by default',
+ 'preferences').firstElementChild;
+ MockInteractions.tap(newChangesWorkInProgress);
+
+ assert.isFalse(element._menuChanged);
+ assert.isTrue(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assert.equal(prefs.work_in_progress_by_default, true);
+ return Promise.resolve();
+ },
+ });
+
+ // Save the change.
+ element._handleSavePreferences().then(() => {
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
+ done();
+ });
+ });
+
+ test('menu', done => {
+ assert.isFalse(element._menuChanged);
+ assert.isFalse(element._prefsChanged);
+
+ assertMenusEqual(element._localMenu, preferences.my);
+
+ const menu = element.$.menu.firstElementChild;
+ let tableRows = dom(menu.root).querySelectorAll('tbody tr');
+ assert.equal(tableRows.length, preferences.my.length);
+
+ // Add a menu item:
+ element.splice('_localMenu', 1, 0, {name: 'foo', url: 'bar', target: ''});
+ flush();
+
+ tableRows = dom(menu.root).querySelectorAll('tbody tr');
+ assert.equal(tableRows.length, preferences.my.length + 1);
+
+ assert.isTrue(element._menuChanged);
+ assert.isFalse(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assertMenusEqual(prefs.my, element._localMenu);
+ return Promise.resolve();
+ },
+ });
+
+ element._handleSaveMenu().then(() => {
assert.isFalse(element._menuChanged);
assert.isFalse(element._prefsChanged);
-
- assertMenusEqual(element._localMenu, preferences.my);
-
- const menu = element.$.menu.firstElementChild;
- let tableRows = Polymer.dom(menu.root).querySelectorAll('tbody tr');
- assert.equal(tableRows.length, preferences.my.length);
-
- // Add a menu item:
- element.splice('_localMenu', 1, 0, {name: 'foo', url: 'bar', target: ''});
- Polymer.dom.flush();
-
- tableRows = Polymer.dom(menu.root).querySelectorAll('tbody tr');
- assert.equal(tableRows.length, preferences.my.length + 1);
-
- assert.isTrue(element._menuChanged);
- assert.isFalse(element._prefsChanged);
-
- stub('gr-rest-api-interface', {
- savePreferences(prefs) {
- assertMenusEqual(prefs.my, element._localMenu);
- return Promise.resolve();
- },
- });
-
- element._handleSaveMenu().then(() => {
- assert.isFalse(element._menuChanged);
- assert.isFalse(element._prefsChanged);
- assertMenusEqual(element.prefs.my, element._localMenu);
- done();
- });
+ assertMenusEqual(element.prefs.my, element._localMenu);
+ done();
});
+ });
- test('add email validation', () => {
- assert.isFalse(element._isNewEmailValid('invalid email'));
- assert.isTrue(element._isNewEmailValid('vaguely@valid.email'));
+ test('add email validation', () => {
+ assert.isFalse(element._isNewEmailValid('invalid email'));
+ assert.isTrue(element._isNewEmailValid('vaguely@valid.email'));
- assert.isFalse(
- element._computeAddEmailButtonEnabled('invalid email'), true);
- assert.isFalse(
- element._computeAddEmailButtonEnabled('vaguely@valid.email', true));
- assert.isTrue(
- element._computeAddEmailButtonEnabled('vaguely@valid.email', false));
+ assert.isFalse(
+ element._computeAddEmailButtonEnabled('invalid email'), true);
+ assert.isFalse(
+ element._computeAddEmailButtonEnabled('vaguely@valid.email', true));
+ assert.isTrue(
+ element._computeAddEmailButtonEnabled('vaguely@valid.email', false));
+ });
+
+ test('add email does not save invalid', () => {
+ const addEmailStub = stubAddAccountEmail(201);
+
+ assert.isFalse(element._addingEmail);
+ assert.isNotOk(element._lastSentVerificationEmail);
+ element._newEmail = 'invalid email';
+
+ element._handleAddEmailButton();
+
+ assert.isFalse(element._addingEmail);
+ assert.isFalse(addEmailStub.called);
+ assert.isNotOk(element._lastSentVerificationEmail);
+
+ assert.isFalse(addEmailStub.called);
+ });
+
+ test('add email does save valid', done => {
+ const addEmailStub = stubAddAccountEmail(201);
+
+ assert.isFalse(element._addingEmail);
+ assert.isNotOk(element._lastSentVerificationEmail);
+ element._newEmail = 'valid@email.com';
+
+ element._handleAddEmailButton();
+
+ assert.isTrue(element._addingEmail);
+ assert.isTrue(addEmailStub.called);
+
+ assert.isTrue(addEmailStub.called);
+ addEmailStub.lastCall.returnValue.then(() => {
+ assert.isOk(element._lastSentVerificationEmail);
+ done();
});
+ });
- test('add email does not save invalid', () => {
- const addEmailStub = stubAddAccountEmail(201);
+ test('add email does not set last-email if error', done => {
+ const addEmailStub = stubAddAccountEmail(500);
- assert.isFalse(element._addingEmail);
+ assert.isNotOk(element._lastSentVerificationEmail);
+ element._newEmail = 'valid@email.com';
+
+ element._handleAddEmailButton();
+
+ assert.isTrue(addEmailStub.called);
+ addEmailStub.lastCall.returnValue.then(() => {
assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'invalid email';
-
- element._handleAddEmailButton();
-
- assert.isFalse(element._addingEmail);
- assert.isFalse(addEmailStub.called);
- assert.isNotOk(element._lastSentVerificationEmail);
-
- assert.isFalse(addEmailStub.called);
+ done();
});
+ });
- test('add email does save valid', done => {
- const addEmailStub = stubAddAccountEmail(201);
+ test('emails are loaded without emailToken', () => {
+ sandbox.stub(element.$.emailEditor, 'loadData');
+ element.params = {};
+ element.attached();
+ assert.isTrue(element.$.emailEditor.loadData.calledOnce);
+ });
- assert.isFalse(element._addingEmail);
- assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'valid@email.com';
+ test('_handleSaveChangeTable', () => {
+ let newColumns = ['Owner', 'Project', 'Branch'];
+ element._localChangeTableColumns = newColumns.slice(0);
+ element._showNumber = false;
+ const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns');
+ element._handleSaveChangeTable();
+ assert.isTrue(cloneStub.calledOnce);
+ assert.deepEqual(element.prefs.change_table, newColumns);
+ assert.isNotOk(element.prefs.legacycid_in_change_table);
- element._handleAddEmailButton();
+ newColumns = ['Size'];
+ element._localChangeTableColumns = newColumns;
+ element._showNumber = true;
+ element._handleSaveChangeTable();
+ assert.isTrue(cloneStub.calledTwice);
+ assert.deepEqual(element.prefs.change_table, newColumns);
+ assert.isTrue(element.prefs.legacycid_in_change_table);
+ });
- assert.isTrue(element._addingEmail);
- assert.isTrue(addEmailStub.called);
-
- assert.isTrue(addEmailStub.called);
- addEmailStub.lastCall.returnValue.then(() => {
- assert.isOk(element._lastSentVerificationEmail);
- done();
- });
- });
-
- test('add email does not set last-email if error', done => {
- const addEmailStub = stubAddAccountEmail(500);
-
- assert.isNotOk(element._lastSentVerificationEmail);
- element._newEmail = 'valid@email.com';
-
- element._handleAddEmailButton();
-
- assert.isTrue(addEmailStub.called);
- addEmailStub.lastCall.returnValue.then(() => {
- assert.isNotOk(element._lastSentVerificationEmail);
- done();
- });
- });
-
- test('emails are loaded without emailToken', () => {
- sandbox.stub(element.$.emailEditor, 'loadData');
- element.params = {};
- element.attached();
- assert.isTrue(element.$.emailEditor.loadData.calledOnce);
- });
-
- test('_handleSaveChangeTable', () => {
- let newColumns = ['Owner', 'Project', 'Branch'];
- element._localChangeTableColumns = newColumns.slice(0);
- element._showNumber = false;
- const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns');
- element._handleSaveChangeTable();
- assert.isTrue(cloneStub.calledOnce);
- assert.deepEqual(element.prefs.change_table, newColumns);
- assert.isNotOk(element.prefs.legacycid_in_change_table);
-
- newColumns = ['Size'];
- element._localChangeTableColumns = newColumns;
- element._showNumber = true;
- element._handleSaveChangeTable();
- assert.isTrue(cloneStub.calledTwice);
- assert.deepEqual(element.prefs.change_table, newColumns);
- assert.isTrue(element.prefs.legacycid_in_change_table);
- });
-
- test('reset menu item back to default', done => {
- const originalMenu = {
- my: [
- {url: '/first/url', name: 'first name', target: '_blank'},
- {url: '/second/url', name: 'second name', target: '_blank'},
- {url: '/third/url', name: 'third name', target: '_blank'},
- ],
- };
-
- stub('gr-rest-api-interface', {
- getDefaultPreferences() { return Promise.resolve(originalMenu); },
- });
-
- const updatedMenu = [
+ test('reset menu item back to default', done => {
+ const originalMenu = {
+ my: [
{url: '/first/url', name: 'first name', target: '_blank'},
{url: '/second/url', name: 'second name', target: '_blank'},
{url: '/third/url', name: 'third name', target: '_blank'},
- {url: '/fourth/url', name: 'fourth name', target: '_blank'},
- ];
+ ],
+ };
- element.set('_localMenu', updatedMenu);
-
- element._handleResetMenuButton().then(() => {
- assertMenusEqual(element._localMenu, originalMenu.my);
- done();
- });
+ stub('gr-rest-api-interface', {
+ getDefaultPreferences() { return Promise.resolve(originalMenu); },
});
- test('test that reset button is called', () => {
- const overlayOpen = sandbox.stub(element, '_handleResetMenuButton');
+ const updatedMenu = [
+ {url: '/first/url', name: 'first name', target: '_blank'},
+ {url: '/second/url', name: 'second name', target: '_blank'},
+ {url: '/third/url', name: 'third name', target: '_blank'},
+ {url: '/fourth/url', name: 'fourth name', target: '_blank'},
+ ];
- MockInteractions.tap(element.$.resetMenu);
+ element.set('_localMenu', updatedMenu);
- assert.isTrue(overlayOpen.called);
- });
-
- test('_showHttpAuth', () => {
- let serverConfig;
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP',
- },
- };
-
- assert.isTrue(element._showHttpAuth(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'HTTP_LDAP',
- },
- };
-
- assert.isTrue(element._showHttpAuth(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'LDAP',
- },
- };
-
- assert.isFalse(element._showHttpAuth(serverConfig));
-
- serverConfig = {
- auth: {
- git_basic_auth_policy: 'OAUTH',
- },
- };
-
- assert.isFalse(element._showHttpAuth(serverConfig));
-
- serverConfig = {};
-
- assert.isFalse(element._showHttpAuth(serverConfig));
- });
-
- suite('_getFilterDocsLink', () => {
- test('with http: docs base URL', () => {
- const base = 'http://example.com/';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'http://example.com/user-notify.html');
- });
-
- test('with http: docs base URL without slash', () => {
- const base = 'http://example.com';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'http://example.com/user-notify.html');
- });
-
- test('with https: docs base URL', () => {
- const base = 'https://example.com/';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'https://example.com/user-notify.html');
- });
-
- test('without docs base URL', () => {
- const result = element._getFilterDocsLink(null);
- assert.equal(result, 'https://gerrit-review.googlesource.com/' +
- 'Documentation/user-notify.html');
- });
-
- test('ignores non HTTP links', () => {
- const base = 'javascript://alert("evil");';
- const result = element._getFilterDocsLink(base);
- assert.equal(result, 'https://gerrit-review.googlesource.com/' +
- 'Documentation/user-notify.html');
- });
- });
-
- suite('when email verification token is provided', () => {
- let resolveConfirm;
-
- setup(() => {
- sandbox.stub(element.$.emailEditor, 'loadData');
- sandbox.stub(
- element.$.restAPI,
- 'confirmEmail',
- () => new Promise(resolve => { resolveConfirm = resolve; }));
- element.params = {emailToken: 'foo'};
- element.attached();
- });
-
- test('it is used to confirm email via rest API', () => {
- assert.isTrue(element.$.restAPI.confirmEmail.calledOnce);
- assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo'));
- });
-
- test('emails are not loaded initially', () => {
- assert.isFalse(element.$.emailEditor.loadData.called);
- });
-
- test('user emails are loaded after email confirmed', done => {
- element._loadingPromise.then(() => {
- assert.isTrue(element.$.emailEditor.loadData.calledOnce);
- done();
- });
- resolveConfirm();
- });
-
- test('show-alert is fired when email is confirmed', done => {
- sandbox.spy(element, 'fire');
- element._loadingPromise.then(() => {
- assert.isTrue(
- element.fire.calledWith('show-alert', {message: 'bar'}));
- done();
- });
- resolveConfirm('bar');
- });
+ element._handleResetMenuButton().then(() => {
+ assertMenusEqual(element._localMenu, originalMenu.my);
+ done();
});
});
+
+ test('test that reset button is called', () => {
+ const overlayOpen = sandbox.stub(element, '_handleResetMenuButton');
+
+ MockInteractions.tap(element.$.resetMenu);
+
+ assert.isTrue(overlayOpen.called);
+ });
+
+ test('_showHttpAuth', () => {
+ let serverConfig;
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP',
+ },
+ };
+
+ assert.isTrue(element._showHttpAuth(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'HTTP_LDAP',
+ },
+ };
+
+ assert.isTrue(element._showHttpAuth(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'LDAP',
+ },
+ };
+
+ assert.isFalse(element._showHttpAuth(serverConfig));
+
+ serverConfig = {
+ auth: {
+ git_basic_auth_policy: 'OAUTH',
+ },
+ };
+
+ assert.isFalse(element._showHttpAuth(serverConfig));
+
+ serverConfig = {};
+
+ assert.isFalse(element._showHttpAuth(serverConfig));
+ });
+
+ suite('_getFilterDocsLink', () => {
+ test('with http: docs base URL', () => {
+ const base = 'http://example.com/';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'http://example.com/user-notify.html');
+ });
+
+ test('with http: docs base URL without slash', () => {
+ const base = 'http://example.com';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'http://example.com/user-notify.html');
+ });
+
+ test('with https: docs base URL', () => {
+ const base = 'https://example.com/';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'https://example.com/user-notify.html');
+ });
+
+ test('without docs base URL', () => {
+ const result = element._getFilterDocsLink(null);
+ assert.equal(result, 'https://gerrit-review.googlesource.com/' +
+ 'Documentation/user-notify.html');
+ });
+
+ test('ignores non HTTP links', () => {
+ const base = 'javascript://alert("evil");';
+ const result = element._getFilterDocsLink(base);
+ assert.equal(result, 'https://gerrit-review.googlesource.com/' +
+ 'Documentation/user-notify.html');
+ });
+ });
+
+ suite('when email verification token is provided', () => {
+ let resolveConfirm;
+
+ setup(() => {
+ sandbox.stub(element.$.emailEditor, 'loadData');
+ sandbox.stub(
+ element.$.restAPI,
+ 'confirmEmail',
+ () => new Promise(resolve => { resolveConfirm = resolve; }));
+ element.params = {emailToken: 'foo'};
+ element.attached();
+ });
+
+ test('it is used to confirm email via rest API', () => {
+ assert.isTrue(element.$.restAPI.confirmEmail.calledOnce);
+ assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo'));
+ });
+
+ test('emails are not loaded initially', () => {
+ assert.isFalse(element.$.emailEditor.loadData.called);
+ });
+
+ test('user emails are loaded after email confirmed', done => {
+ element._loadingPromise.then(() => {
+ assert.isTrue(element.$.emailEditor.loadData.calledOnce);
+ done();
+ });
+ resolveConfirm();
+ });
+
+ test('show-alert is fired when email is confirmed', done => {
+ sandbox.spy(element, 'fire');
+ element._loadingPromise.then(() => {
+ assert.isTrue(
+ element.fire.calledWith('show-alert', {message: 'bar'}));
+ done();
+ });
+ resolveConfirm('bar');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
index 44fb48c..814eb7a 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -14,95 +14,108 @@
* 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 GrSshEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-ssh-editor'; }
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/gr-form-styles.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
+import '../../shared/gr-overlay/gr-overlay.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 {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-ssh-editor_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- value: false,
- notify: true,
- },
- _keys: Array,
- /** @type {?} */
- _keyToView: Object,
- _newKey: {
- type: String,
- value: '',
- },
- _keysToRemove: {
- type: Array,
- value() { return []; },
- },
- };
- }
+/** @extends Polymer.Element */
+class GrSshEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- loadData() {
- return this.$.restAPI.getAccountSSHKeys().then(keys => {
- this._keys = keys;
- });
- }
+ static get is() { return 'gr-ssh-editor'; }
- save() {
- const promises = this._keysToRemove.map(key => {
- this.$.restAPI.deleteAccountSSHKey(key.seq);
- });
-
- return Promise.all(promises).then(() => {
- this._keysToRemove = [];
- this.hasUnsavedChanges = false;
- });
- }
-
- _getStatusLabel(isValid) {
- return isValid ? 'Valid' : 'Invalid';
- }
-
- _showKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this._keyToView = this._keys[index];
- this.$.viewKeyOverlay.open();
- }
-
- _closeOverlay() {
- this.$.viewKeyOverlay.close();
- }
-
- _handleDeleteKey(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- this.push('_keysToRemove', this._keys[index]);
- this.splice('_keys', index, 1);
- this.hasUnsavedChanges = true;
- }
-
- _handleAddKey() {
- this.$.addButton.disabled = true;
- this.$.newKey.disabled = true;
- return this.$.restAPI.addAccountSSHKey(this._newKey.trim())
- .then(key => {
- this.$.newKey.disabled = false;
- this._newKey = '';
- this.push('_keys', key);
- })
- .catch(() => {
- this.$.addButton.disabled = false;
- this.$.newKey.disabled = false;
- });
- }
-
- _computeAddButtonDisabled(newKey) {
- return !newKey.length;
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ };
}
- customElements.define(GrSshEditor.is, GrSshEditor);
-})();
+ loadData() {
+ return this.$.restAPI.getAccountSSHKeys().then(keys => {
+ this._keys = keys;
+ });
+ }
+
+ save() {
+ const promises = this._keysToRemove.map(key => {
+ this.$.restAPI.deleteAccountSSHKey(key.seq);
+ });
+
+ return Promise.all(promises).then(() => {
+ this._keysToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ }
+
+ _getStatusLabel(isValid) {
+ return isValid ? 'Valid' : 'Invalid';
+ }
+
+ _showKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this._keyToView = this._keys[index];
+ this.$.viewKeyOverlay.open();
+ }
+
+ _closeOverlay() {
+ this.$.viewKeyOverlay.close();
+ }
+
+ _handleDeleteKey(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+ this.splice('_keys', index, 1);
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this.$.restAPI.addAccountSSHKey(this._newKey.trim())
+ .then(key => {
+ this.$.newKey.disabled = false;
+ this._newKey = '';
+ this.push('_keys', key);
+ })
+ .catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ }
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ }
+}
+
+customElements.define(GrSshEditor.is, GrSshEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
index dd02ccd..3cefb90e 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
@@ -1,31 +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="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-ssh-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -79,31 +70,20 @@
<td class="commentColumn">[[key.comment]]</td>
<td>[[_getStatusLabel(key.valid)]]</td>
<td>
- <gr-button
- link
- on-click="_showKey"
- data-index$="[[index]]"
- link>Click to View</gr-button>
+ <gr-button link="" on-click="_showKey" data-index\$="[[index]]">Click to View</gr-button>
</td>
<td>
- <gr-copy-clipboard
- has-tooltip
- button-title="Copy SSH public key to clipboard"
- hide-input
- text="[[key.ssh_public_key]]">
+ <gr-copy-clipboard has-tooltip="" button-title="Copy SSH public key to clipboard" hide-input="" text="[[key.ssh_public_key]]">
</gr-copy-clipboard>
</td>
<td>
- <gr-button
- link
- data-index$="[[index]]"
- on-click="_handleDeleteKey">Delete</gr-button>
+ <gr-button link="" data-index\$="[[index]]" on-click="_handleDeleteKey">Delete</gr-button>
</td>
</tr>
</template>
</tbody>
</table>
- <gr-overlay id="viewKeyOverlay" with-backdrop>
+ <gr-overlay id="viewKeyOverlay" with-backdrop="">
<fieldset>
<section>
<span class="title">Algorithm</span>
@@ -118,33 +98,19 @@
<span class="value">[[_keyToView.comment]]</span>
</section>
</fieldset>
- <gr-button
- class="closeButton"
- on-click="_closeOverlay">Close</gr-button>
+ <gr-button class="closeButton" on-click="_closeOverlay">Close</gr-button>
</gr-overlay>
- <gr-button
- on-click="save"
- disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button>
+ <gr-button on-click="save" disabled\$="[[!hasUnsavedChanges]]">Save changes</gr-button>
</fieldset>
<fieldset>
<section>
<span class="title">New SSH key</span>
<span class="value">
- <iron-autogrow-textarea
- id="newKey"
- autocomplete="on"
- bind-value="{{_newKey}}"
- placeholder="New SSH Key"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="newKey" autocomplete="on" bind-value="{{_newKey}}" placeholder="New SSH Key"></iron-autogrow-textarea>
</span>
</section>
- <gr-button
- id="addButton"
- link
- disabled$="[[_computeAddButtonDisabled(_newKey)]]"
- on-click="_handleAddKey">Add new SSH key</gr-button>
+ <gr-button id="addButton" link="" disabled\$="[[_computeAddButtonDisabled(_newKey)]]" on-click="_handleAddKey">Add new SSH key</gr-button>
</fieldset>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-ssh-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 82d427d..4312d9a 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_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-ssh-editor</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-ssh-editor.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-ssh-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-ssh-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,149 +40,152 @@
</template>
</test-fixture>
-<script>
- suite('gr-ssh-editor tests', async () => {
- await readyToTest();
- let element;
- let keys;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-ssh-editor.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-ssh-editor tests', () => {
+ let element;
+ let keys;
- setup(done => {
- keys = [{
- seq: 1,
- ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one',
- encoded_key: '<key 1>',
- algorithm: 'ssh-rsa',
- comment: 'comment-one@machine-one',
- valid: true,
- }, {
- seq: 2,
- ssh_public_key: 'ssh-rsa <key 2> comment-two@machine-two',
- encoded_key: '<key 2>',
- algorithm: 'ssh-rsa',
- comment: 'comment-two@machine-two',
- valid: true,
- }];
+ setup(done => {
+ keys = [{
+ seq: 1,
+ ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one',
+ encoded_key: '<key 1>',
+ algorithm: 'ssh-rsa',
+ comment: 'comment-one@machine-one',
+ valid: true,
+ }, {
+ seq: 2,
+ ssh_public_key: 'ssh-rsa <key 2> comment-two@machine-two',
+ encoded_key: '<key 2>',
+ algorithm: 'ssh-rsa',
+ comment: 'comment-two@machine-two',
+ valid: true,
+ }];
- stub('gr-rest-api-interface', {
- getAccountSSHKeys() { return Promise.resolve(keys); },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getAccountSSHKeys() { return Promise.resolve(keys); },
});
- test('renders', () => {
- const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+ element = fixture('basic');
- assert.equal(rows.length, 2);
+ element.loadData().then(() => { flush(done); });
+ });
- let cells = rows[0].querySelectorAll('td');
- assert.equal(cells[0].textContent, keys[0].comment);
+ test('renders', () => {
+ const rows = dom(element.root).querySelectorAll('tbody tr');
- cells = rows[1].querySelectorAll('td');
- assert.equal(cells[0].textContent, keys[1].comment);
- });
+ assert.equal(rows.length, 2);
- test('remove key', done => {
- const lastKey = keys[1];
+ let cells = rows[0].querySelectorAll('td');
+ assert.equal(cells[0].textContent, keys[0].comment);
- const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey',
- () => Promise.resolve());
+ cells = rows[1].querySelectorAll('td');
+ assert.equal(cells[0].textContent, keys[1].comment);
+ });
+ test('remove key', done => {
+ const lastKey = keys[1];
+
+ const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey',
+ () => Promise.resolve());
+
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ // Get the delete button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(5) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keys.length, 1);
+ assert.equal(element._keysToRemove.length, 1);
+ assert.equal(element._keysToRemove[0], lastKey);
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.isFalse(saveStub.called);
+
+ element.save().then(() => {
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.lastCall.args[0], lastKey.seq);
assert.equal(element._keysToRemove.length, 0);
assert.isFalse(element.hasUnsavedChanges);
-
- // Get the delete button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(5) gr-button');
-
- MockInteractions.tap(button);
-
- assert.equal(element._keys.length, 1);
- assert.equal(element._keysToRemove.length, 1);
- assert.equal(element._keysToRemove[0], lastKey);
- assert.isTrue(element.hasUnsavedChanges);
- assert.isFalse(saveStub.called);
-
- element.save().then(() => {
- assert.isTrue(saveStub.called);
- assert.equal(saveStub.lastCall.args[0], lastKey.seq);
- assert.equal(element._keysToRemove.length, 0);
- assert.isFalse(element.hasUnsavedChanges);
- done();
- });
- });
-
- test('show key', () => {
- const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
-
- // Get the show button for the last row.
- const button = Polymer.dom(element.root).querySelector(
- 'tbody tr:last-of-type td:nth-child(3) gr-button');
-
- MockInteractions.tap(button);
-
- assert.equal(element._keyToView, keys[1]);
- assert.isTrue(openSpy.called);
- });
-
- test('add key', done => {
- const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three';
- const newKeyObject = {
- seq: 3,
- ssh_public_key: newKeyString,
- encoded_key: '<key 3>',
- algorithm: 'ssh-rsa',
- comment: 'comment-three@machine-three',
- valid: true,
- };
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
- () => Promise.resolve(newKeyObject));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isTrue(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 3);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.equal(addStub.lastCall.args[0], newKeyString);
- });
-
- test('add invalid key', done => {
- const newKeyString = 'not even close to valid';
-
- const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
- () => Promise.reject(new Error('error')));
-
- element._newKey = newKeyString;
-
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
-
- element._handleAddKey().then(() => {
- assert.isFalse(element.$.addButton.disabled);
- assert.isFalse(element.$.newKey.disabled);
- assert.equal(element._keys.length, 2);
- done();
- });
-
- assert.isTrue(element.$.addButton.disabled);
- assert.isTrue(element.$.newKey.disabled);
-
- assert.isTrue(addStub.called);
- assert.equal(addStub.lastCall.args[0], newKeyString);
+ done();
});
});
+
+ test('show key', () => {
+ const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+
+ // Get the show button for the last row.
+ const button = dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(3) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keyToView, keys[1]);
+ assert.isTrue(openSpy.called);
+ });
+
+ test('add key', done => {
+ const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three';
+ const newKeyObject = {
+ seq: 3,
+ ssh_public_key: newKeyString,
+ encoded_key: '<key 3>',
+ algorithm: 'ssh-rsa',
+ comment: 'comment-three@machine-three',
+ valid: true,
+ };
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
+ () => Promise.resolve(newKeyObject));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 3);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.equal(addStub.lastCall.args[0], newKeyString);
+ });
+
+ test('add invalid key', done => {
+ const newKeyString = 'not even close to valid';
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
+ () => Promise.reject(new Error('error')));
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.equal(addStub.lastCall.args[0], newKeyString);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index df115ca..b8960e8 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -14,169 +14,181 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const NOTIFICATION_TYPES = [
- {name: 'Changes', key: 'notify_new_changes'},
- {name: 'Patches', key: 'notify_new_patch_sets'},
- {name: 'Comments', key: 'notify_all_comments'},
- {name: 'Submits', key: 'notify_submitted_changes'},
- {name: 'Abandons', key: 'notify_abandoned_changes'},
- ];
+import '@polymer/iron-input/iron-input.js';
+import '../../shared/gr-autocomplete/gr-autocomplete.js';
+import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/gr-form-styles.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-watched-projects-editor_html.js';
- /** @extends Polymer.Element */
- class GrWatchedProjectsEditor extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-watched-projects-editor'; }
+const NOTIFICATION_TYPES = [
+ {name: 'Changes', key: 'notify_new_changes'},
+ {name: 'Patches', key: 'notify_new_patch_sets'},
+ {name: 'Comments', key: 'notify_all_comments'},
+ {name: 'Submits', key: 'notify_submitted_changes'},
+ {name: 'Abandons', key: 'notify_abandoned_changes'},
+];
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- value: false,
- notify: true,
+/** @extends Polymer.Element */
+class GrWatchedProjectsEditor extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-watched-projects-editor'; }
+
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+
+ _projects: Array,
+ _projectsToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ _query: {
+ type: Function,
+ value() {
+ return this._getProjectSuggestions.bind(this);
},
-
- _projects: Array,
- _projectsToRemove: {
- type: Array,
- value() { return []; },
- },
- _query: {
- type: Function,
- value() {
- return this._getProjectSuggestions.bind(this);
- },
- },
- };
- }
-
- loadData() {
- return this.$.restAPI.getWatchedProjects().then(projs => {
- this._projects = projs;
- });
- }
-
- save() {
- let deletePromise;
- if (this._projectsToRemove.length) {
- deletePromise = this.$.restAPI.deleteWatchedProjects(
- this._projectsToRemove);
- } else {
- deletePromise = Promise.resolve();
- }
-
- return deletePromise
- .then(() => this.$.restAPI.saveWatchedProjects(this._projects))
- .then(projects => {
- this._projects = projects;
- this._projectsToRemove = [];
- this.hasUnsavedChanges = false;
- });
- }
-
- _getTypes() {
- return NOTIFICATION_TYPES;
- }
-
- _getTypeCount() {
- return this._getTypes().length;
- }
-
- _computeCheckboxChecked(project, key) {
- return project.hasOwnProperty(key);
- }
-
- _getProjectSuggestions(input) {
- return this.$.restAPI.getSuggestedProjects(input)
- .then(response => {
- const projects = [];
- for (const key in response) {
- if (!response.hasOwnProperty(key)) { continue; }
- projects.push({
- name: key,
- value: response[key],
- });
- }
- return projects;
- });
- }
-
- _handleRemoveProject(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- const project = this._projects[index];
- this.splice('_projects', index, 1);
- this.push('_projectsToRemove', project);
- this.hasUnsavedChanges = true;
- }
-
- _canAddProject(project, text, filter) {
- if ((!project || !project.id) && !text) { return false; }
-
- // This will only be used if not using the auto complete
- if (!project && text) { return true; }
-
- // Check if the project with filter is already in the list. Compare
- // filters using == to coalesce null and undefined.
- for (let i = 0; i < this._projects.length; i++) {
- if (this._projects[i].project === project.id &&
- this._projects[i].filter == filter) {
- return false;
- }
- }
-
- return true;
- }
-
- _getNewProjectIndex(name, filter) {
- let i;
- for (i = 0; i < this._projects.length; i++) {
- if (this._projects[i].project > name ||
- (this._projects[i].project === name &&
- this._projects[i].filter > filter)) {
- break;
- }
- }
- return i;
- }
-
- _handleAddProject() {
- const newProject = this.$.newProject.value;
- const newProjectName = this.$.newProject.text;
- const filter = this.$.newFilter.value || null;
-
- if (!this._canAddProject(newProject, newProjectName, filter)) { return; }
-
- const insertIndex = this._getNewProjectIndex(newProjectName, filter);
-
- this.splice('_projects', insertIndex, 0, {
- project: newProjectName,
- filter,
- _is_local: true,
- });
-
- this.$.newProject.clear();
- this.$.newFilter.bindValue = '';
- this.hasUnsavedChanges = true;
- }
-
- _handleCheckboxChange(e) {
- const el = Polymer.dom(e).localTarget;
- const index = parseInt(el.getAttribute('data-index'), 10);
- const key = el.getAttribute('data-key');
- const checked = el.checked;
- this.set(['_projects', index, key], !!checked);
- this.hasUnsavedChanges = true;
- }
-
- _handleNotifCellClick(e) {
- const checkbox = Polymer.dom(e.target).querySelector('input');
- if (checkbox) { checkbox.click(); }
- }
+ },
+ };
}
- customElements.define(GrWatchedProjectsEditor.is, GrWatchedProjectsEditor);
-})();
+ loadData() {
+ return this.$.restAPI.getWatchedProjects().then(projs => {
+ this._projects = projs;
+ });
+ }
+
+ save() {
+ let deletePromise;
+ if (this._projectsToRemove.length) {
+ deletePromise = this.$.restAPI.deleteWatchedProjects(
+ this._projectsToRemove);
+ } else {
+ deletePromise = Promise.resolve();
+ }
+
+ return deletePromise
+ .then(() => this.$.restAPI.saveWatchedProjects(this._projects))
+ .then(projects => {
+ this._projects = projects;
+ this._projectsToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ }
+
+ _getTypes() {
+ return NOTIFICATION_TYPES;
+ }
+
+ _getTypeCount() {
+ return this._getTypes().length;
+ }
+
+ _computeCheckboxChecked(project, key) {
+ return project.hasOwnProperty(key);
+ }
+
+ _getProjectSuggestions(input) {
+ return this.$.restAPI.getSuggestedProjects(input)
+ .then(response => {
+ const projects = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ projects.push({
+ name: key,
+ value: response[key],
+ });
+ }
+ return projects;
+ });
+ }
+
+ _handleRemoveProject(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ const project = this._projects[index];
+ this.splice('_projects', index, 1);
+ this.push('_projectsToRemove', project);
+ this.hasUnsavedChanges = true;
+ }
+
+ _canAddProject(project, text, filter) {
+ if ((!project || !project.id) && !text) { return false; }
+
+ // This will only be used if not using the auto complete
+ if (!project && text) { return true; }
+
+ // Check if the project with filter is already in the list. Compare
+ // filters using == to coalesce null and undefined.
+ for (let i = 0; i < this._projects.length; i++) {
+ if (this._projects[i].project === project.id &&
+ this._projects[i].filter == filter) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ _getNewProjectIndex(name, filter) {
+ let i;
+ for (i = 0; i < this._projects.length; i++) {
+ if (this._projects[i].project > name ||
+ (this._projects[i].project === name &&
+ this._projects[i].filter > filter)) {
+ break;
+ }
+ }
+ return i;
+ }
+
+ _handleAddProject() {
+ const newProject = this.$.newProject.value;
+ const newProjectName = this.$.newProject.text;
+ const filter = this.$.newFilter.value || null;
+
+ if (!this._canAddProject(newProject, newProjectName, filter)) { return; }
+
+ const insertIndex = this._getNewProjectIndex(newProjectName, filter);
+
+ this.splice('_projects', insertIndex, 0, {
+ project: newProjectName,
+ filter,
+ _is_local: true,
+ });
+
+ this.$.newProject.clear();
+ this.$.newFilter.bindValue = '';
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleCheckboxChange(e) {
+ const el = dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ const key = el.getAttribute('data-key');
+ const checked = el.checked;
+ this.set(['_projects', index, key], !!checked);
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleNotifCellClick(e) {
+ const checkbox = dom(e.target).querySelector('input');
+ if (checkbox) { checkbox.click(); }
+ }
+}
+
+customElements.define(GrWatchedProjectsEditor.is, GrWatchedProjectsEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js
index b1ecb2e..bc381e0 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/gr-form-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-watched-projects-editor">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -60,11 +53,7 @@
</tr>
</thead>
<tbody>
- <template
- is="dom-repeat"
- items="[[_projects]]"
- as="project"
- index-as="projectIndex">
+ <template is="dom-repeat" items="[[_projects]]" as="project" index-as="projectIndex">
<tr>
<td>
[[project.project]]
@@ -72,24 +61,13 @@
<div class="projectFilter">[[project.filter]]</div>
</template>
</td>
- <template
- is="dom-repeat"
- items="[[_getTypes()]]"
- as="type">
+ <template is="dom-repeat" items="[[_getTypes()]]" as="type">
<td class="notifControl" on-click="_handleNotifCellClick">
- <input
- type="checkbox"
- data-index$="[[projectIndex]]"
- data-key$="[[type.key]]"
- on-change="_handleCheckboxChange"
- checked$="[[_computeCheckboxChecked(project, type.key)]]">
+ <input type="checkbox" data-index\$="[[projectIndex]]" data-key\$="[[type.key]]" on-change="_handleCheckboxChange" checked\$="[[_computeCheckboxChecked(project, type.key)]]">
</td>
</template>
<td>
- <gr-button
- link
- data-index$="[[projectIndex]]"
- on-click="_handleRemoveProject">Delete</gr-button>
+ <gr-button link="" data-index\$="[[projectIndex]]" on-click="_handleRemoveProject">Delete</gr-button>
</td>
</tr>
</template>
@@ -97,33 +75,19 @@
<tfoot>
<tr>
<th>
- <gr-autocomplete
- id="newProject"
- query="[[_query]]"
- threshold="1"
- allow-non-suggested-values
- tab-complete
- placeholder="Repo"></gr-autocomplete>
+ <gr-autocomplete id="newProject" query="[[_query]]" threshold="1" allow-non-suggested-values="" tab-complete="" placeholder="Repo"></gr-autocomplete>
</th>
- <th colspan$="[[_getTypeCount()]]">
- <iron-input
- class="newFilterInput"
- placeholder="branch:name, or other search expression">
- <input
- id="newFilter"
- class="newFilterInput"
- is="iron-input"
- placeholder="branch:name, or other search expression">
+ <th colspan\$="[[_getTypeCount()]]">
+ <iron-input class="newFilterInput" placeholder="branch:name, or other search expression">
+ <input id="newFilter" class="newFilterInput" is="iron-input" placeholder="branch:name, or other search expression">
</iron-input>
</th>
<th>
- <gr-button link on-click="_handleAddProject">Add</gr-button>
+ <gr-button link="" on-click="_handleAddProject">Add</gr-button>
</th>
</tr>
</tfoot>
</table>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-watched-projects-editor.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index c96d6a0..a02afcb 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_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-settings-view</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-watched-projects-editor.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-watched-projects-editor.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-watched-projects-editor.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,184 +40,186 @@
</template>
</test-fixture>
-<script>
- suite('gr-watched-projects-editor tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-watched-projects-editor.js';
+suite('gr-watched-projects-editor tests', () => {
+ let element;
- setup(done => {
- const projects = [
- {
- project: 'project a',
- notify_submitted_changes: true,
- notify_abandoned_changes: true,
- }, {
- project: 'project b',
- filter: 'filter 1',
- notify_new_changes: true,
- }, {
- project: 'project b',
- filter: 'filter 2',
- }, {
- project: 'project c',
- notify_new_changes: true,
- notify_new_patch_sets: true,
- notify_all_comments: true,
- },
- ];
+ setup(done => {
+ const projects = [
+ {
+ project: 'project a',
+ notify_submitted_changes: true,
+ notify_abandoned_changes: true,
+ }, {
+ project: 'project b',
+ filter: 'filter 1',
+ notify_new_changes: true,
+ }, {
+ project: 'project b',
+ filter: 'filter 2',
+ }, {
+ project: 'project c',
+ notify_new_changes: true,
+ notify_new_patch_sets: true,
+ notify_all_comments: true,
+ },
+ ];
- stub('gr-rest-api-interface', {
- getSuggestedProjects(input) {
- if (input.startsWith('th')) {
- return Promise.resolve({'the project': {
- id: 'the project',
- state: 'ACTIVE',
- web_links: [],
- }});
- } else {
- return Promise.resolve({});
- }
- },
- getWatchedProjects() {
- return Promise.resolve(projects);
- },
- });
-
- element = fixture('basic');
-
- element.loadData().then(() => { flush(done); });
+ stub('gr-rest-api-interface', {
+ getSuggestedProjects(input) {
+ if (input.startsWith('th')) {
+ return Promise.resolve({'the project': {
+ id: 'the project',
+ state: 'ACTIVE',
+ web_links: [],
+ }});
+ } else {
+ return Promise.resolve({});
+ }
+ },
+ getWatchedProjects() {
+ return Promise.resolve(projects);
+ },
});
- test('renders', () => {
- const rows = element.shadowRoot
- .querySelector('table').querySelectorAll('tbody tr');
- assert.equal(rows.length, 4);
+ element = fixture('basic');
- function getKeysOfRow(row) {
- const boxes = rows[row].querySelectorAll('input[checked]');
- return Array.prototype.map.call(boxes,
- e => e.getAttribute('data-key'));
- }
+ element.loadData().then(() => { flush(done); });
+ });
- let checkedKeys = getKeysOfRow(0);
- assert.equal(checkedKeys.length, 2);
- assert.equal(checkedKeys[0], 'notify_submitted_changes');
- assert.equal(checkedKeys[1], 'notify_abandoned_changes');
+ test('renders', () => {
+ const rows = element.shadowRoot
+ .querySelector('table').querySelectorAll('tbody tr');
+ assert.equal(rows.length, 4);
- checkedKeys = getKeysOfRow(1);
- assert.equal(checkedKeys.length, 1);
- assert.equal(checkedKeys[0], 'notify_new_changes');
+ function getKeysOfRow(row) {
+ const boxes = rows[row].querySelectorAll('input[checked]');
+ return Array.prototype.map.call(boxes,
+ e => e.getAttribute('data-key'));
+ }
- checkedKeys = getKeysOfRow(2);
- assert.equal(checkedKeys.length, 0);
+ let checkedKeys = getKeysOfRow(0);
+ assert.equal(checkedKeys.length, 2);
+ assert.equal(checkedKeys[0], 'notify_submitted_changes');
+ assert.equal(checkedKeys[1], 'notify_abandoned_changes');
- checkedKeys = getKeysOfRow(3);
- assert.equal(checkedKeys.length, 3);
- assert.equal(checkedKeys[0], 'notify_new_changes');
- assert.equal(checkedKeys[1], 'notify_new_patch_sets');
- assert.equal(checkedKeys[2], 'notify_all_comments');
- });
+ checkedKeys = getKeysOfRow(1);
+ assert.equal(checkedKeys.length, 1);
+ assert.equal(checkedKeys[0], 'notify_new_changes');
- test('_getProjectSuggestions empty', done => {
- element._getProjectSuggestions('nonexistent').then(projects => {
- assert.equal(projects.length, 0);
- done();
- });
- });
+ checkedKeys = getKeysOfRow(2);
+ assert.equal(checkedKeys.length, 0);
- test('_getProjectSuggestions non-empty', done => {
- element._getProjectSuggestions('the project').then(projects => {
- assert.equal(projects.length, 1);
- assert.equal(projects[0].name, 'the project');
- done();
- });
- });
+ checkedKeys = getKeysOfRow(3);
+ assert.equal(checkedKeys.length, 3);
+ assert.equal(checkedKeys[0], 'notify_new_changes');
+ assert.equal(checkedKeys[1], 'notify_new_patch_sets');
+ assert.equal(checkedKeys[2], 'notify_all_comments');
+ });
- test('_getProjectSuggestions non-empty with two letter project', done => {
- element._getProjectSuggestions('th').then(projects => {
- assert.equal(projects.length, 1);
- assert.equal(projects[0].name, 'the project');
- done();
- });
- });
-
- test('_canAddProject', () => {
- assert.isFalse(element._canAddProject(null, null, null));
- assert.isFalse(element._canAddProject({}, null, null));
-
- // Can add a project that is not in the list.
- assert.isTrue(element._canAddProject({id: 'project d'}, null, null));
- assert.isTrue(element._canAddProject({id: 'project d'}, null, 'filter 3'));
-
- // Cannot add a project that is in the list with no filter.
- assert.isFalse(element._canAddProject({id: 'project a'}, null, null));
-
- // Can add a project that is in the list if the filter differs.
- assert.isTrue(element._canAddProject({id: 'project a'}, null, 'filter 4'));
-
- // Cannot add a project that is in the list with the same filter.
- assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 1'));
- assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 2'));
-
- // Can add a project that is in the list using a new filter.
- assert.isTrue(element._canAddProject({id: 'project b'}, null, 'filter 3'));
-
- // Can add a project that is not added by the auto complete
- assert.isTrue(element._canAddProject(null, 'test', null));
- });
-
- test('_getNewProjectIndex', () => {
- // Projects are sorted in ASCII order.
- assert.equal(element._getNewProjectIndex('project A', 'filter'), 0);
- assert.equal(element._getNewProjectIndex('project a', 'filter'), 1);
-
- // Projects are sorted by filter when the names are equal
- assert.equal(element._getNewProjectIndex('project b', 'filter 0'), 1);
- assert.equal(element._getNewProjectIndex('project b', 'filter 1.5'), 2);
- assert.equal(element._getNewProjectIndex('project b', 'filter 3'), 3);
-
- // Projects with filters follow those without
- assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
- });
-
- test('_handleAddProject', () => {
- element.$.newProject.value = {id: 'project d'};
- element.$.newProject.setText('project d');
- element.$.newFilter.bindValue = '';
-
- element._handleAddProject();
-
- assert.equal(element._projects.length, 5);
- assert.equal(element._projects[4].project, 'project d');
- assert.isNotOk(element._projects[4].filter);
- assert.isTrue(element._projects[4]._is_local);
- });
-
- test('_handleAddProject with invalid inputs', () => {
- element.$.newProject.value = {id: 'project b'};
- element.$.newProject.setText('project b');
- element.$.newFilter.bindValue = 'filter 1';
- element.$.newFilter.value = 'filter 1';
-
- element._handleAddProject();
-
- assert.equal(element._projects.length, 4);
- });
-
- test('_handleRemoveProject', () => {
- assert.equal(element._projectsToRemove, 0);
- const button = element.shadowRoot
- .querySelector('table tbody tr:nth-child(2) gr-button');
- MockInteractions.tap(button);
-
- flushAsynchronousOperations();
-
- const rows = element.shadowRoot
- .querySelector('table tbody').querySelectorAll('tr');
- assert.equal(rows.length, 3);
-
- assert.equal(element._projectsToRemove.length, 1);
- assert.equal(element._projectsToRemove[0].project, 'project b');
+ test('_getProjectSuggestions empty', done => {
+ element._getProjectSuggestions('nonexistent').then(projects => {
+ assert.equal(projects.length, 0);
+ done();
});
});
+
+ test('_getProjectSuggestions non-empty', done => {
+ element._getProjectSuggestions('the project').then(projects => {
+ assert.equal(projects.length, 1);
+ assert.equal(projects[0].name, 'the project');
+ done();
+ });
+ });
+
+ test('_getProjectSuggestions non-empty with two letter project', done => {
+ element._getProjectSuggestions('th').then(projects => {
+ assert.equal(projects.length, 1);
+ assert.equal(projects[0].name, 'the project');
+ done();
+ });
+ });
+
+ test('_canAddProject', () => {
+ assert.isFalse(element._canAddProject(null, null, null));
+ assert.isFalse(element._canAddProject({}, null, null));
+
+ // Can add a project that is not in the list.
+ assert.isTrue(element._canAddProject({id: 'project d'}, null, null));
+ assert.isTrue(element._canAddProject({id: 'project d'}, null, 'filter 3'));
+
+ // Cannot add a project that is in the list with no filter.
+ assert.isFalse(element._canAddProject({id: 'project a'}, null, null));
+
+ // Can add a project that is in the list if the filter differs.
+ assert.isTrue(element._canAddProject({id: 'project a'}, null, 'filter 4'));
+
+ // Cannot add a project that is in the list with the same filter.
+ assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 1'));
+ assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 2'));
+
+ // Can add a project that is in the list using a new filter.
+ assert.isTrue(element._canAddProject({id: 'project b'}, null, 'filter 3'));
+
+ // Can add a project that is not added by the auto complete
+ assert.isTrue(element._canAddProject(null, 'test', null));
+ });
+
+ test('_getNewProjectIndex', () => {
+ // Projects are sorted in ASCII order.
+ assert.equal(element._getNewProjectIndex('project A', 'filter'), 0);
+ assert.equal(element._getNewProjectIndex('project a', 'filter'), 1);
+
+ // Projects are sorted by filter when the names are equal
+ assert.equal(element._getNewProjectIndex('project b', 'filter 0'), 1);
+ assert.equal(element._getNewProjectIndex('project b', 'filter 1.5'), 2);
+ assert.equal(element._getNewProjectIndex('project b', 'filter 3'), 3);
+
+ // Projects with filters follow those without
+ assert.equal(element._getNewProjectIndex('project c', 'filter'), 4);
+ });
+
+ test('_handleAddProject', () => {
+ element.$.newProject.value = {id: 'project d'};
+ element.$.newProject.setText('project d');
+ element.$.newFilter.bindValue = '';
+
+ element._handleAddProject();
+
+ assert.equal(element._projects.length, 5);
+ assert.equal(element._projects[4].project, 'project d');
+ assert.isNotOk(element._projects[4].filter);
+ assert.isTrue(element._projects[4]._is_local);
+ });
+
+ test('_handleAddProject with invalid inputs', () => {
+ element.$.newProject.value = {id: 'project b'};
+ element.$.newProject.setText('project b');
+ element.$.newFilter.bindValue = 'filter 1';
+ element.$.newFilter.value = 'filter 1';
+
+ element._handleAddProject();
+
+ assert.equal(element._projects.length, 4);
+ });
+
+ test('_handleRemoveProject', () => {
+ assert.equal(element._projectsToRemove, 0);
+ const button = element.shadowRoot
+ .querySelector('table tbody tr:nth-child(2) gr-button');
+ MockInteractions.tap(button);
+
+ flushAsynchronousOperations();
+
+ const rows = element.shadowRoot
+ .querySelector('table tbody').querySelectorAll('tr');
+ assert.equal(rows.length, 3);
+
+ assert.equal(element._projectsToRemove.length, 1);
+ assert.equal(element._projectsToRemove[0].project, 'project b');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 8cd2021..4ac540d 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -14,80 +14,92 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-account-link/gr-account-link.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.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-account-chip_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccountChip extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-chip'; }
+ /**
+ * Fired to indicate a key was pressed while this chip was focused.
+ *
+ * @event account-chip-keydown
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired to indicate this chip should be removed, i.e. when the x button is
+ * clicked or when the remove function is called.
+ *
+ * @event remove
*/
- class GrAccountChip extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-chip'; }
- /**
- * Fired to indicate a key was pressed while this chip was focused.
- *
- * @event account-chip-keydown
- */
- /**
- * Fired to indicate this chip should be removed, i.e. when the x button is
- * clicked or when the remove function is called.
- *
- * @event remove
- */
-
- static get properties() {
- return {
- account: Object,
- additionalText: String,
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- removable: {
- type: Boolean,
- value: false,
- },
- showAvatar: {
- type: Boolean,
- reflectToAttribute: true,
- },
- transparentBackground: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._getHasAvatars().then(hasAvatars => {
- this.showAvatar = hasAvatars;
- });
- }
-
- _getBackgroundClass(transparent) {
- return transparent ? 'transparentBackground' : '';
- }
-
- _handleRemoveTap(e) {
- e.preventDefault();
- this.fire('remove', {account: this.account});
- }
-
- _getHasAvatars() {
- return this.$.restAPI.getConfig()
- .then(cfg => Promise.resolve(!!(
- cfg && cfg.plugin && cfg.plugin.has_avatars
- )));
- }
+ static get properties() {
+ return {
+ account: Object,
+ additionalText: String,
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ removable: {
+ type: Boolean,
+ value: false,
+ },
+ showAvatar: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ transparentBackground: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrAccountChip.is, GrAccountChip);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._getHasAvatars().then(hasAvatars => {
+ this.showAvatar = hasAvatars;
+ });
+ }
+
+ _getBackgroundClass(transparent) {
+ return transparent ? 'transparentBackground' : '';
+ }
+
+ _handleRemoveTap(e) {
+ e.preventDefault();
+ this.fire('remove', {account: this.account});
+ }
+
+ _getHasAvatars() {
+ return this.$.restAPI.getConfig()
+ .then(cfg => Promise.resolve(!!(
+ cfg && cfg.plugin && cfg.plugin.has_avatars
+ )));
+ }
+}
+
+customElements.define(GrAccountChip.is, GrAccountChip);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
index 7e2d872..7f219e5 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
@@ -1,30 +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="../gr-account-link/gr-account-link.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-icons/gr-icons.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-account-chip">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -88,23 +80,12 @@
width: 1.2rem;
}
</style>
- <div class$="container [[_getBackgroundClass(transparentBackground)]]">
- <gr-account-link account="[[account]]"
- additional-text="[[additionalText]]">
+ <div class\$="container [[_getBackgroundClass(transparentBackground)]]">
+ <gr-account-link account="[[account]]" additional-text="[[additionalText]]">
</gr-account-link>
- <gr-button
- id="remove"
- link
- hidden$="[[!removable]]"
- hidden
- tabindex="-1"
- aria-label="Remove"
- class$="remove [[_getBackgroundClass(transparentBackground)]]"
- on-click="_handleRemoveTap">
+ <gr-button id="remove" link="" hidden\$="[[!removable]]" hidden="" tabindex="-1" aria-label="Remove" class\$="remove [[_getBackgroundClass(transparentBackground)]]" on-click="_handleRemoveTap">
<iron-icon icon="gr-icons:close"></iron-icon>
</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-account-chip.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
index d2a111a..49e984c 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -14,96 +14,105 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-autocomplete/gr-autocomplete.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.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-entry_html.js';
+
+/**
+ * gr-account-entry is an element for entering account
+ * and/or group with autocomplete support.
+ *
+ * @extends Polymer.Element
+ */
+class GrAccountEntry extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-entry'; }
+ /**
+ * Fired when an account is entered.
+ *
+ * @event add
+ */
/**
- * gr-account-entry is an element for entering account
- * and/or group with autocomplete support.
+ * When allowAnyInput is true, account-text-changed is fired when input text
+ * changed. This is needed so that the reply dialog's save button can be
+ * enabled for arbitrary cc's, which don't need a 'commit'.
*
- * @extends Polymer.Element
+ * @event account-text-changed
*/
- class GrAccountEntry extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-account-entry'; }
- /**
- * Fired when an account is entered.
- *
- * @event add
- */
- /**
- * When allowAnyInput is true, account-text-changed is fired when input text
- * changed. This is needed so that the reply dialog's save button can be
- * enabled for arbitrary cc's, which don't need a 'commit'.
- *
- * @event account-text-changed
- */
+ static get properties() {
+ return {
+ allowAnyInput: Boolean,
+ borderless: Boolean,
+ placeholder: String,
- static get properties() {
- return {
- allowAnyInput: Boolean,
- borderless: Boolean,
- placeholder: String,
+ // suggestFrom = 0 to enable default suggestions.
+ suggestFrom: {
+ type: Number,
+ value: 0,
+ },
- // suggestFrom = 0 to enable default suggestions.
- suggestFrom: {
- type: Number,
- value: 0,
+ /** @type {!function(string): !Promise<Array<{name, value}>>} */
+ querySuggestions: {
+ type: Function,
+ notify: true,
+ value() {
+ return input => Promise.resolve([]);
},
+ },
- /** @type {!function(string): !Promise<Array<{name, value}>>} */
- querySuggestions: {
- type: Function,
- notify: true,
- value() {
- return input => Promise.resolve([]);
- },
- },
+ _config: Object,
+ /** The value of the autocomplete entry. */
+ _inputText: {
+ type: String,
+ observer: '_inputTextChanged',
+ },
- _config: Object,
- /** The value of the autocomplete entry. */
- _inputText: {
- type: String,
- observer: '_inputTextChanged',
- },
-
- };
- }
-
- get focusStart() {
- return this.$.input.focusStart;
- }
-
- focus() {
- this.$.input.focus();
- }
-
- clear() {
- this.$.input.clear();
- }
-
- setText(text) {
- this.$.input.setText(text);
- }
-
- getText() {
- return this.$.input.text;
- }
-
- _handleInputCommit(e) {
- this.fire('add', {value: e.detail.value});
- this.$.input.focus();
- }
-
- _inputTextChanged(text) {
- if (text.length && this.allowAnyInput) {
- this.dispatchEvent(new CustomEvent(
- 'account-text-changed', {bubbles: true, composed: true}));
- }
- }
+ };
}
- customElements.define(GrAccountEntry.is, GrAccountEntry);
-})();
+ get focusStart() {
+ return this.$.input.focusStart;
+ }
+
+ focus() {
+ this.$.input.focus();
+ }
+
+ clear() {
+ this.$.input.clear();
+ }
+
+ setText(text) {
+ this.$.input.setText(text);
+ }
+
+ getText() {
+ return this.$.input.text;
+ }
+
+ _handleInputCommit(e) {
+ this.fire('add', {value: e.detail.value});
+ this.$.input.focus();
+ }
+
+ _inputTextChanged(text) {
+ if (text.length && this.allowAnyInput) {
+ this.dispatchEvent(new CustomEvent(
+ 'account-text-changed', {bubbles: true, composed: true}));
+ }
+ }
+}
+
+customElements.define(GrAccountEntry.is, GrAccountEntry);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
index 992ea8407..281526d 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
@@ -1,28 +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="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-account-entry">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
gr-autocomplete {
display: inline-block;
@@ -30,19 +24,6 @@
overflow: hidden;
}
</style>
- <gr-autocomplete
- id="input"
- borderless="[[borderless]]"
- placeholder="[[placeholder]]"
- threshold="[[suggestFrom]]"
- query="[[querySuggestions]]"
- allow-non-suggested-values="[[allowAnyInput]]"
- on-commit="_handleInputCommit"
- clear-on-commit
- warn-uncommitted
- text="{{_inputText}}"
- vertical-offset="24">
+ <gr-autocomplete id="input" borderless="[[borderless]]" placeholder="[[placeholder]]" threshold="[[suggestFrom]]" query="[[querySuggestions]]" allow-non-suggested-values="[[allowAnyInput]]" on-commit="_handleInputCommit" clear-on-commit="" warn-uncommitted="" text="{{_inputText}}" vertical-offset="24">
</gr-autocomplete>
- </template>
- <script src="gr-account-entry.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
index 51310eb..7ef07d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-entry</title>
-<script src="/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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-account-entry.html">
+<script type="module" src="./gr-account-entry.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-account-entry.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,78 +43,81 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-entry tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-account-entry.js';
+suite('gr-account-entry tests', () => {
+ let sandbox;
+ let element;
- const suggestion1 = {
- email: 'email1@example.com',
- _account_id: 1,
- some_property: 'value',
- };
- const suggestion2 = {
- email: 'email2@example.com',
- _account_id: 2,
- };
- const suggestion3 = {
- email: 'email25@example.com',
- _account_id: 25,
- some_other_property: 'other value',
- };
+ const suggestion1 = {
+ email: 'email1@example.com',
+ _account_id: 1,
+ some_property: 'value',
+ };
+ const suggestion2 = {
+ email: 'email2@example.com',
+ _account_id: 2,
+ };
+ const suggestion3 = {
+ email: 'email25@example.com',
+ _account_id: 25,
+ some_other_property: 'other value',
+ };
- setup(done => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- return flush(done);
- });
+ setup(done => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ return flush(done);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- suite('stubbed values for querySuggestions', () => {
- setup(() => {
- element.querySuggestions = input => Promise.resolve([
- suggestion1,
- suggestion2,
- suggestion3,
- ]);
- });
- });
-
- test('account-text-changed fired when input text changed and allowAnyInput',
- () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const changeStub = sandbox.stub();
- element.allowAnyInput = true;
- element.querySuggestions = input => Promise.resolve([]);
- element.addEventListener('account-text-changed', changeStub);
- element.$.input.text = 'a';
- assert.isTrue(changeStub.calledOnce);
- element.$.input.text = 'ab';
- assert.isTrue(changeStub.calledTwice);
- });
-
- test('account-text-changed not fired when input text changed without ' +
- 'allowAnyInput', () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const changeStub = sandbox.stub();
- element.querySuggestions = input => Promise.resolve([]);
- element.addEventListener('account-text-changed', changeStub);
- element.$.input.text = 'a';
- assert.isFalse(changeStub.called);
- });
-
- test('setText', () => {
- // Spy on query, as that is called when _updateSuggestions proceeds.
- const suggestSpy = sandbox.spy(element.$.input, 'query');
- element.setText('test text');
- flushAsynchronousOperations();
-
- assert.equal(element.$.input.$.input.value, 'test text');
- assert.isFalse(suggestSpy.called);
+ suite('stubbed values for querySuggestions', () => {
+ setup(() => {
+ element.querySuggestions = input => Promise.resolve([
+ suggestion1,
+ suggestion2,
+ suggestion3,
+ ]);
});
});
+
+ test('account-text-changed fired when input text changed and allowAnyInput',
+ () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const changeStub = sandbox.stub();
+ element.allowAnyInput = true;
+ element.querySuggestions = input => Promise.resolve([]);
+ element.addEventListener('account-text-changed', changeStub);
+ element.$.input.text = 'a';
+ assert.isTrue(changeStub.calledOnce);
+ element.$.input.text = 'ab';
+ assert.isTrue(changeStub.calledTwice);
+ });
+
+ test('account-text-changed not fired when input text changed without ' +
+ 'allowAnyInput', () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const changeStub = sandbox.stub();
+ element.querySuggestions = input => Promise.resolve([]);
+ element.addEventListener('account-text-changed', changeStub);
+ element.$.input.text = 'a';
+ assert.isFalse(changeStub.called);
+ });
+
+ test('setText', () => {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ const suggestSpy = sandbox.spy(element.$.input, 'query');
+ element.setText('test text');
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.input.$.input.value, 'test text');
+ assert.isFalse(suggestSpy.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 34c4cb6..ba65e03 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -14,121 +14,134 @@
* 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';
- /**
- * @appliesMixin Gerrit.DisplayNameMixin
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrAccountLabel extends Polymer.mixinBehaviors( [
- Gerrit.DisplayNameBehavior,
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-label'; }
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '../../../styles/shared-styles.js';
+import '../gr-avatar/gr-avatar.js';
+import '../gr-limited-text/gr-limited-text.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../scripts/util.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-label_html.js';
- static get properties() {
- return {
- /**
- * @type {{ name: string, status: string }}
- */
- account: Object,
- avatarImageSize: {
- type: Number,
- value: 32,
- },
- title: {
- type: String,
- reflectToAttribute: true,
- computed: '_computeAccountTitle(account, additionalText)',
- },
- additionalText: String,
- hasTooltip: {
- type: Boolean,
- reflectToAttribute: true,
- computed: '_computeHasTooltip(account)',
- },
- hideAvatar: {
- type: Boolean,
- value: false,
- },
- _serverConfig: {
- type: Object,
- value: null,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.DisplayNameMixin
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrAccountLabel extends mixinBehaviors( [
+ Gerrit.DisplayNameBehavior,
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- ready() {
- super.ready();
- if (!this.additionalText) { this.additionalText = ''; }
- this.$.restAPI.getConfig()
- .then(config => { this._serverConfig = config; });
- }
+ static get is() { return 'gr-account-label'; }
- _computeName(account, config) {
- return this.getUserName(config, account, false);
- }
-
- _computeStatusTextLength(account, config) {
- // 35 as the max length of the name + status
- return Math.max(10, 35 - this._computeName(account, config).length);
- }
-
- _computeAccountTitle(account, tooltip) {
- // Polymer 2: check for undefined
- if ([
- account,
- tooltip,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!account) { return; }
- let result = '';
- if (this._computeName(account, this._serverConfig)) {
- result += this._computeName(account, this._serverConfig);
- }
- if (account.email) {
- result += ` <${account.email}>`;
- }
- if (this.additionalText) {
- result += ` ${this.additionalText}`;
- }
-
- // Show status in the label tooltip instead of
- // in a separate tooltip on status
- if (account.status) {
- result += ` (${account.status})`;
- }
-
- return result;
- }
-
- _computeShowEmailClass(account) {
- if (!account || account.name || !account.email) { return ''; }
- return 'showEmail';
- }
-
- _computeEmailStr(account) {
- if (!account || !account.email) {
- return '';
- }
- if (account.name) {
- return '(' + account.email + ')';
- }
- return account.email;
- }
-
- _computeHasTooltip(account) {
- // If an account has loaded to fire this method, then set to true.
- return !!account;
- }
+ static get properties() {
+ return {
+ /**
+ * @type {{ name: string, status: string }}
+ */
+ account: Object,
+ avatarImageSize: {
+ type: Number,
+ value: 32,
+ },
+ title: {
+ type: String,
+ reflectToAttribute: true,
+ computed: '_computeAccountTitle(account, additionalText)',
+ },
+ additionalText: String,
+ hasTooltip: {
+ type: Boolean,
+ reflectToAttribute: true,
+ computed: '_computeHasTooltip(account)',
+ },
+ hideAvatar: {
+ type: Boolean,
+ value: false,
+ },
+ _serverConfig: {
+ type: Object,
+ value: null,
+ },
+ };
}
- customElements.define(GrAccountLabel.is, GrAccountLabel);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ if (!this.additionalText) { this.additionalText = ''; }
+ this.$.restAPI.getConfig()
+ .then(config => { this._serverConfig = config; });
+ }
+
+ _computeName(account, config) {
+ return this.getUserName(config, account, false);
+ }
+
+ _computeStatusTextLength(account, config) {
+ // 35 as the max length of the name + status
+ return Math.max(10, 35 - this._computeName(account, config).length);
+ }
+
+ _computeAccountTitle(account, tooltip) {
+ // Polymer 2: check for undefined
+ if ([
+ account,
+ tooltip,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (!account) { return; }
+ let result = '';
+ if (this._computeName(account, this._serverConfig)) {
+ result += this._computeName(account, this._serverConfig);
+ }
+ if (account.email) {
+ result += ` <${account.email}>`;
+ }
+ if (this.additionalText) {
+ result += ` ${this.additionalText}`;
+ }
+
+ // Show status in the label tooltip instead of
+ // in a separate tooltip on status
+ if (account.status) {
+ result += ` (${account.status})`;
+ }
+
+ return result;
+ }
+
+ _computeShowEmailClass(account) {
+ if (!account || account.name || !account.email) { return ''; }
+ return 'showEmail';
+ }
+
+ _computeEmailStr(account) {
+ if (!account || !account.email) {
+ return '';
+ }
+ if (account.name) {
+ return '(' + account.email + ')';
+ }
+ return account.email;
+ }
+
+ _computeHasTooltip(account) {
+ // If an account has loaded to fire this method, then set to true.
+ return !!account;
+ }
+}
+
+customElements.define(GrAccountLabel.is, GrAccountLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
index fcd9ccd..e9d0e5d 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
@@ -1,31 +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="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-avatar/gr-avatar.html">
-<link rel="import" href="../gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-account-label">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: inline;
@@ -54,25 +45,19 @@
</style>
<span>
<template is="dom-if" if="[[!hideAvatar]]">
- <gr-avatar account="[[account]]"
- image-size="[[avatarImageSize]]"></gr-avatar>
+ <gr-avatar account="[[account]]" image-size="[[avatarImageSize]]"></gr-avatar>
</template>
- <span class$="text [[_computeShowEmailClass(account)]]">
+ <span class\$="text [[_computeShowEmailClass(account)]]">
<span class="name">
[[_computeName(account, _serverConfig)]]</span>
<span class="email">
[[_computeEmailStr(account)]]
</span>
<template is="dom-if" if="[[account.status]]">
- (<gr-limited-text
- disable-tooltip="true"
- limit="[[_computeStatusTextLength(account, _serverConfig)]]"
- text="[[account.status]]">
+ (<gr-limited-text disable-tooltip="true" limit="[[_computeStatusTextLength(account, _serverConfig)]]" text="[[account.status]]">
</gr-limited-text>)
</template>
</span>
</span>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-account-label.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index f5a9b8d..db742e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-label</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-account-label.html">
+<script type="module" src="./gr-account-label.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-account-label.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,17 +43,124 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-label tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-account-label.js';
+suite('gr-account-label tests', () => {
+ let element;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ });
+ element = fixture('basic');
+ element._config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
+ });
+
+ test('null guard', () => {
+ assert.doesNotThrow(() => {
+ element.account = null;
+ });
+ });
+
+ test('missing email', () => {
+ assert.equal('', element._computeEmailStr({name: 'foo'}));
+ });
+
+ test('computed fields', () => {
+ assert.equal(
+ element._computeAccountTitle({
+ name: 'Andrew Bonventre',
+ email: 'andybons+gerrit@gmail.com',
+ }, /* additionalText= */ ''),
+ 'Andrew Bonventre <andybons+gerrit@gmail.com>');
+
+ assert.equal(
+ element._computeAccountTitle({
+ name: 'Andrew Bonventre',
+ }, /* additionalText= */ ''),
+ 'Andrew Bonventre');
+
+ assert.equal(
+ element._computeAccountTitle({
+ email: 'andybons+gerrit@gmail.com',
+ }, /* additionalText= */ ''),
+ 'Anonymous <andybons+gerrit@gmail.com>');
+
+ assert.equal(element._computeShowEmailClass(
+ {
+ name: 'Andrew Bonventre',
+ email: 'andybons+gerrit@gmail.com',
+ }, /* additionalText= */ ''), '');
+
+ assert.equal(element._computeShowEmailClass(
+ {
+ email: 'andybons+gerrit@gmail.com',
+ }, /* additionalText= */ ''), 'showEmail');
+
+ assert.equal(element._computeShowEmailClass(
+ {name: 'Andrew Bonventre'},
+ /* additionalText= */ ''
+ ),
+ '');
+
+ assert.equal(element._computeShowEmailClass(undefined), '');
+
+ assert.equal(
+ element._computeEmailStr({name: 'test', email: 'test'}), '(test)');
+ assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test');
+ });
+
+ suite('_computeName', () => {
+ test('not showing anonymous', () => {
+ const account = {name: 'Wyatt'};
+ assert.deepEqual(element._computeName(account, null), 'Wyatt');
+ });
+
+ test('showing anonymous but no config', () => {
+ const account = {};
+ assert.deepEqual(element._computeName(account, null),
+ 'Anonymous');
+ });
+
+ test('test for Anonymous Coward user and replace with Anonymous', () => {
+ const config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
+ const account = {};
+ assert.deepEqual(element._computeName(account, config),
+ 'Anonymous');
+ });
+
+ test('test for anonymous_coward_name', () => {
+ const config = {
+ user: {
+ anonymous_coward_name: 'TestAnon',
+ },
+ };
+ const account = {};
+ assert.deepEqual(element._computeName(account, config),
+ 'TestAnon');
+ });
+ });
+
+ suite('status in tooltip', () => {
setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getLoggedIn() { return Promise.resolve(false); },
- });
element = fixture('basic');
+ element.account = {
+ name: 'test',
+ email: 'test@google.com',
+ status: 'OOO until Aug 10th',
+ };
element._config = {
user: {
anonymous_coward_name: 'Anonymous Coward',
@@ -55,133 +168,29 @@
};
});
- test('null guard', () => {
- assert.doesNotThrow(() => {
- element.account = null;
- });
+ test('tooltip should contain status text', () => {
+ assert.deepEqual(element.title,
+ 'test <test@google.com> (OOO until Aug 10th)');
});
- test('missing email', () => {
- assert.equal('', element._computeEmailStr({name: 'foo'}));
+ test('status text should not have tooltip', () => {
+ flushAsynchronousOperations();
+ assert.deepEqual(element.shadowRoot
+ .querySelector('gr-limited-text').title, '');
});
- test('computed fields', () => {
- assert.equal(
- element._computeAccountTitle({
- name: 'Andrew Bonventre',
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''),
- 'Andrew Bonventre <andybons+gerrit@gmail.com>');
-
- assert.equal(
- element._computeAccountTitle({
- name: 'Andrew Bonventre',
- }, /* additionalText= */ ''),
- 'Andrew Bonventre');
-
- assert.equal(
- element._computeAccountTitle({
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''),
- 'Anonymous <andybons+gerrit@gmail.com>');
-
- assert.equal(element._computeShowEmailClass(
- {
- name: 'Andrew Bonventre',
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''), '');
-
- assert.equal(element._computeShowEmailClass(
- {
- email: 'andybons+gerrit@gmail.com',
- }, /* additionalText= */ ''), 'showEmail');
-
- assert.equal(element._computeShowEmailClass(
- {name: 'Andrew Bonventre'},
- /* additionalText= */ ''
- ),
- '');
-
- assert.equal(element._computeShowEmailClass(undefined), '');
-
- assert.equal(
- element._computeEmailStr({name: 'test', email: 'test'}), '(test)');
- assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test');
- });
-
- suite('_computeName', () => {
- test('not showing anonymous', () => {
- const account = {name: 'Wyatt'};
- assert.deepEqual(element._computeName(account, null), 'Wyatt');
- });
-
- test('showing anonymous but no config', () => {
- const account = {};
- assert.deepEqual(element._computeName(account, null),
- 'Anonymous');
- });
-
- test('test for Anonymous Coward user and replace with Anonymous', () => {
- const config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
- const account = {};
- assert.deepEqual(element._computeName(account, config),
- 'Anonymous');
- });
-
- test('test for anonymous_coward_name', () => {
- const config = {
- user: {
- anonymous_coward_name: 'TestAnon',
- },
- };
- const account = {};
- assert.deepEqual(element._computeName(account, config),
- 'TestAnon');
- });
- });
-
- suite('status in tooltip', () => {
- setup(() => {
- element = fixture('basic');
- element.account = {
- name: 'test',
- email: 'test@google.com',
- status: 'OOO until Aug 10th',
- };
- element._config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward',
- },
- };
- });
-
- test('tooltip should contain status text', () => {
- assert.deepEqual(element.title,
- 'test <test@google.com> (OOO until Aug 10th)');
- });
-
- test('status text should not have tooltip', () => {
- flushAsynchronousOperations();
- assert.deepEqual(element.shadowRoot
- .querySelector('gr-limited-text').title, '');
- });
-
- test('status text should honor the name length and total length', () => {
- assert.deepEqual(
- element._computeStatusTextLength(element.account, element._config),
- 31
- );
- assert.deepEqual(
- element._computeStatusTextLength({
- name: 'a very long long long long name',
- }, element._config),
- 10
- );
- });
+ test('status text should honor the name length and total length', () => {
+ assert.deepEqual(
+ element._computeStatusTextLength(element.account, element._config),
+ 31
+ );
+ assert.deepEqual(
+ element._computeStatusTextLength({
+ name: 'a very long long long long name',
+ }, element._config),
+ 10
+ );
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index b0ce04c..4a38427 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.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,38 +14,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrAccountLink extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-link'; }
+import '../../../scripts/bundled-polymer.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../gr-account-label/gr-account-label.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-account-link_html.js';
- static get properties() {
- return {
- additionalText: String,
- account: Object,
- avatarImageSize: {
- type: Number,
- value: 32,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrAccountLink extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _computeOwnerLink(account) {
- if (!account) { return; }
- return Gerrit.Nav.getUrlForOwner(
- account.email || account.username || account.name ||
- account._account_id);
- }
+ static get is() { return 'gr-account-link'; }
+
+ static get properties() {
+ return {
+ additionalText: String,
+ account: Object,
+ avatarImageSize: {
+ type: Number,
+ value: 32,
+ },
+ };
}
- customElements.define(GrAccountLink.is, GrAccountLink);
-})();
+ _computeOwnerLink(account) {
+ if (!account) { return; }
+ return Gerrit.Nav.getUrlForOwner(
+ account.email || account.username || account.name ||
+ account._account_id);
+ }
+}
+
+customElements.define(GrAccountLink.is, GrAccountLink);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
index d3575b2..4ea343e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
@@ -1,28 +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/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../gr-account-label/gr-account-label.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-account-link">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: inline-block;
@@ -38,12 +32,8 @@
}
</style>
<span>
- <a href$="[[_computeOwnerLink(account)]]" tabindex="-1">
- <gr-account-label account="[[account]]"
- additional-text="[[additionalText]]"
- avatar-image-size="[[avatarImageSize]]"></gr-account-label>
+ <a href\$="[[_computeOwnerLink(account)]]" tabindex="-1">
+ <gr-account-label account="[[account]]" additional-text="[[additionalText]]" avatar-image-size="[[avatarImageSize]]"></gr-account-label>
</a>
</span>
- </template>
- <script src="gr-account-link.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index c648661..ff89a78 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_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-link</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-link.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-link.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-account-link.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,48 +40,50 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-link 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-account-link.js';
+suite('gr-account-link tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('computed fields', () => {
- const url = 'test/url';
- const urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForOwner').returns(url);
- const account = {
- email: 'email',
- username: 'username',
- name: 'name',
- _account_id: '_account_id',
- };
- assert.isNotOk(element._computeOwnerLink());
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('email'));
-
- delete account.email;
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('username'));
-
- delete account.username;
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('name'));
-
- delete account.name;
- assert.equal(element._computeOwnerLink(account), url);
- assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id'));
- });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('computed fields', () => {
+ const url = 'test/url';
+ const urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForOwner').returns(url);
+ const account = {
+ email: 'email',
+ username: 'username',
+ name: 'name',
+ _account_id: '_account_id',
+ };
+ assert.isNotOk(element._computeOwnerLink());
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('email'));
+
+ delete account.email;
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('username'));
+
+ delete account.username;
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('name'));
+
+ delete account.name;
+ assert.equal(element._computeOwnerLink(account), url);
+ assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 7955d50..2e8f768 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -14,342 +14,353 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const VALID_EMAIL_ALERT = 'Please input a valid email.';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-account-chip/gr-account-chip.js';
+import '../gr-account-entry/gr-account-entry.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-account-list_html.js';
+const VALID_EMAIL_ALERT = 'Please input a valid email.';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrAccountList extends mixinBehaviors( [
+ // Used in the tests for gr-account-list and other elements tests.
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-account-list'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when user inputs an invalid email address.
+ *
+ * @event show-alert
*/
- class GrAccountList extends Polymer.mixinBehaviors( [
- // Used in the tests for gr-account-list and other elements tests.
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-account-list'; }
- /**
- * Fired when user inputs an invalid email address.
- *
- * @event show-alert
- */
- static get properties() {
- return {
- accounts: {
- type: Array,
- value() { return []; },
- notify: true,
- },
- change: Object,
- filter: Function,
- placeholder: String,
- disabled: {
- type: Function,
- value: false,
- },
+ static get properties() {
+ return {
+ accounts: {
+ type: Array,
+ value() { return []; },
+ notify: true,
+ },
+ change: Object,
+ filter: Function,
+ placeholder: String,
+ disabled: {
+ type: Function,
+ value: false,
+ },
- /**
- * Returns suggestions and convert them to list item
- *
- * @type {Gerrit.GrSuggestionsProvider}
- */
- suggestionsProvider: {
- type: Object,
- },
+ /**
+ * Returns suggestions and convert them to list item
+ *
+ * @type {Gerrit.GrSuggestionsProvider}
+ */
+ suggestionsProvider: {
+ type: Object,
+ },
- /**
- * Needed for template checking since value is initially set to null.
- *
- * @type {?Object}
- */
- pendingConfirmation: {
- type: Object,
- value: null,
- notify: true,
- },
- readonly: {
- type: Boolean,
- value: false,
- },
- /**
- * When true, allows for non-suggested inputs to be added.
- */
- allowAnyInput: {
- type: Boolean,
- value: false,
- },
+ /**
+ * Needed for template checking since value is initially set to null.
+ *
+ * @type {?Object}
+ */
+ pendingConfirmation: {
+ type: Object,
+ value: null,
+ notify: true,
+ },
+ readonly: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * When true, allows for non-suggested inputs to be added.
+ */
+ allowAnyInput: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * Array of values (groups/accounts) that are removable. When this prop is
- * undefined, all values are removable.
- */
- removableValues: Array,
- maxCount: {
- type: Number,
- value: 0,
- },
+ /**
+ * Array of values (groups/accounts) that are removable. When this prop is
+ * undefined, all values are removable.
+ */
+ removableValues: Array,
+ maxCount: {
+ type: Number,
+ value: 0,
+ },
- /**
- * Returns suggestion items
- *
- * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>}
- */
- _querySuggestions: {
- type: Function,
- value() {
- return this._getSuggestions.bind(this);
- },
+ /**
+ * Returns suggestion items
+ *
+ * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>}
+ */
+ _querySuggestions: {
+ type: Function,
+ value() {
+ return this._getSuggestions.bind(this);
},
+ },
- /**
- * Set to true to disable suggestions on empty input.
- */
- skipSuggestOnEmpty: {
- type: Boolean,
- value: false,
- },
- };
+ /**
+ * Set to true to disable suggestions on empty input.
+ */
+ skipSuggestOnEmpty: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('remove',
+ e => this._handleRemove(e));
+ }
+
+ get accountChips() {
+ return Array.from(
+ dom(this.root).querySelectorAll('gr-account-chip'));
+ }
+
+ get focusStart() {
+ return this.$.entry.focusStart;
+ }
+
+ _getSuggestions(input) {
+ if (this.skipSuggestOnEmpty && !input) {
+ return Promise.resolve([]);
}
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('remove',
- e => this._handleRemove(e));
+ const provider = this.suggestionsProvider;
+ if (!provider) {
+ return Promise.resolve([]);
}
-
- get accountChips() {
- return Array.from(
- Polymer.dom(this.root).querySelectorAll('gr-account-chip'));
- }
-
- get focusStart() {
- return this.$.entry.focusStart;
- }
-
- _getSuggestions(input) {
- if (this.skipSuggestOnEmpty && !input) {
- return Promise.resolve([]);
+ return provider.getSuggestions(input).then(suggestions => {
+ if (!suggestions) { return []; }
+ if (this.filter) {
+ suggestions = suggestions.filter(this.filter);
}
- const provider = this.suggestionsProvider;
- if (!provider) {
- return Promise.resolve([]);
+ return suggestions.map(suggestion =>
+ provider.makeSuggestionItem(suggestion));
+ });
+ }
+
+ _handleAdd(e) {
+ this._addAccountItem(e.detail.value);
+ }
+
+ _addAccountItem(item) {
+ // Append new account or group to the accounts property. We add our own
+ // internal properties to the account/group here, so we clone the object
+ // to avoid cluttering up the shared change object.
+ if (item.account) {
+ const account =
+ Object.assign({}, item.account, {_pendingAdd: true});
+ this.push('accounts', account);
+ } else if (item.group) {
+ if (item.confirm) {
+ this.pendingConfirmation = item;
+ return;
}
- return provider.getSuggestions(input).then(suggestions => {
- if (!suggestions) { return []; }
- if (this.filter) {
- suggestions = suggestions.filter(this.filter);
- }
- return suggestions.map(suggestion =>
- provider.makeSuggestionItem(suggestion));
- });
- }
-
- _handleAdd(e) {
- this._addAccountItem(e.detail.value);
- }
-
- _addAccountItem(item) {
- // Append new account or group to the accounts property. We add our own
- // internal properties to the account/group here, so we clone the object
- // to avoid cluttering up the shared change object.
- if (item.account) {
- const account =
- Object.assign({}, item.account, {_pendingAdd: true});
- this.push('accounts', account);
- } else if (item.group) {
- if (item.confirm) {
- this.pendingConfirmation = item;
- return;
- }
- const group = Object.assign({}, item.group,
- {_pendingAdd: true, _group: true});
- this.push('accounts', group);
- } else if (this.allowAnyInput) {
- if (!item.includes('@')) {
- // Repopulate the input with what the user tried to enter and have
- // a toast tell them why they can't enter it.
- this.$.entry.setText(item);
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: VALID_EMAIL_ALERT},
- bubbles: true,
- composed: true,
- }));
- return false;
- } else {
- const account = {email: item, _pendingAdd: true};
- this.push('accounts', account);
- }
- }
- this.pendingConfirmation = null;
- return true;
- }
-
- confirmGroup(group) {
- group = Object.assign(
- {}, group, {confirmed: true, _pendingAdd: true, _group: true});
+ const group = Object.assign({}, item.group,
+ {_pendingAdd: true, _group: true});
this.push('accounts', group);
- this.pendingConfirmation = null;
- }
-
- _computeChipClass(account) {
- const classes = [];
- if (account._group) {
- classes.push('group');
+ } else if (this.allowAnyInput) {
+ if (!item.includes('@')) {
+ // Repopulate the input with what the user tried to enter and have
+ // a toast tell them why they can't enter it.
+ this.$.entry.setText(item);
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: VALID_EMAIL_ALERT},
+ bubbles: true,
+ composed: true,
+ }));
+ return false;
+ } else {
+ const account = {email: item, _pendingAdd: true};
+ this.push('accounts', account);
}
- if (account._pendingAdd) {
- classes.push('pendingAdd');
- }
- return classes.join(' ');
}
+ this.pendingConfirmation = null;
+ return true;
+ }
- _accountMatches(a, b) {
- if (a && b) {
- if (a._account_id) {
- return a._account_id === b._account_id;
- }
- if (a.email) {
- return a.email === b.email;
+ confirmGroup(group) {
+ group = Object.assign(
+ {}, group, {confirmed: true, _pendingAdd: true, _group: true});
+ this.push('accounts', group);
+ this.pendingConfirmation = null;
+ }
+
+ _computeChipClass(account) {
+ const classes = [];
+ if (account._group) {
+ classes.push('group');
+ }
+ if (account._pendingAdd) {
+ classes.push('pendingAdd');
+ }
+ return classes.join(' ');
+ }
+
+ _accountMatches(a, b) {
+ if (a && b) {
+ if (a._account_id) {
+ return a._account_id === b._account_id;
+ }
+ if (a.email) {
+ return a.email === b.email;
+ }
+ }
+ return a === b;
+ }
+
+ _computeRemovable(account, readonly) {
+ if (readonly) { return false; }
+ if (this.removableValues) {
+ for (let i = 0; i < this.removableValues.length; i++) {
+ if (this._accountMatches(this.removableValues[i], account)) {
+ return true;
}
}
- return a === b;
+ return !!account._pendingAdd;
}
+ return true;
+ }
- _computeRemovable(account, readonly) {
- if (readonly) { return false; }
- if (this.removableValues) {
- for (let i = 0; i < this.removableValues.length; i++) {
- if (this._accountMatches(this.removableValues[i], account)) {
- return true;
- }
- }
- return !!account._pendingAdd;
+ _handleRemove(e) {
+ const toRemove = e.detail.account;
+ this._removeAccount(toRemove);
+ this.$.entry.focus();
+ }
+
+ _removeAccount(toRemove) {
+ if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ return;
+ }
+ for (let i = 0; i < this.accounts.length; i++) {
+ let matches;
+ const account = this.accounts[i];
+ if (toRemove._group) {
+ matches = toRemove.id === account.id;
+ } else {
+ matches = this._accountMatches(toRemove, account);
}
- return true;
- }
-
- _handleRemove(e) {
- const toRemove = e.detail.account;
- this._removeAccount(toRemove);
- this.$.entry.focus();
- }
-
- _removeAccount(toRemove) {
- if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ if (matches) {
+ this.splice('accounts', i, 1);
return;
}
- for (let i = 0; i < this.accounts.length; i++) {
- let matches;
- const account = this.accounts[i];
- if (toRemove._group) {
- matches = toRemove.id === account.id;
- } else {
- matches = this._accountMatches(toRemove, account);
+ }
+ console.warn('received remove event for missing account', toRemove);
+ }
+
+ _getNativeInput(paperInput) {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ return paperInput.$.nativeInput || paperInput.inputElement;
+ }
+
+ _handleInputKeydown(e) {
+ const input = this._getNativeInput(e.detail.input);
+ if (input.selectionStart !== input.selectionEnd ||
+ input.selectionStart !== 0) {
+ return;
+ }
+ switch (e.detail.keyCode) {
+ case 8: // Backspace
+ this._removeAccount(this.accounts[this.accounts.length - 1]);
+ break;
+ case 37: // Left arrow
+ if (this.accountChips[this.accountChips.length - 1]) {
+ this.accountChips[this.accountChips.length - 1].focus();
}
- if (matches) {
- this.splice('accounts', i, 1);
- return;
- }
- }
- console.warn('received remove event for missing account', toRemove);
- }
-
- _getNativeInput(paperInput) {
- // In Polymer 2 inputElement isn't nativeInput anymore
- return paperInput.$.nativeInput || paperInput.inputElement;
- }
-
- _handleInputKeydown(e) {
- const input = this._getNativeInput(e.detail.input);
- if (input.selectionStart !== input.selectionEnd ||
- input.selectionStart !== 0) {
- return;
- }
- switch (e.detail.keyCode) {
- case 8: // Backspace
- this._removeAccount(this.accounts[this.accounts.length - 1]);
- break;
- case 37: // Left arrow
- if (this.accountChips[this.accountChips.length - 1]) {
- this.accountChips[this.accountChips.length - 1].focus();
- }
- break;
- }
- }
-
- _handleChipKeydown(e) {
- const chip = e.target;
- const chips = this.accountChips;
- const index = chips.indexOf(chip);
- switch (e.keyCode) {
- case 8: // Backspace
- case 13: // Enter
- case 32: // Spacebar
- case 46: // Delete
- this._removeAccount(chip.account);
- // Splice from this array to avoid inconsistent ordering of
- // event handling.
- chips.splice(index, 1);
- if (index < chips.length) {
- chips[index].focus();
- } else if (index > 0) {
- chips[index - 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- case 37: // Left arrow
- if (index > 0) {
- chip.blur();
- chips[index - 1].focus();
- }
- break;
- case 39: // Right arrow
- chip.blur();
- if (index < chips.length - 1) {
- chips[index + 1].focus();
- } else {
- this.$.entry.focus();
- }
- break;
- }
- }
-
- /**
- * Submit the text of the entry as a reviewer value, if it exists. If it is
- * a successful submit of the text, clear the entry value.
- *
- * @return {boolean} If there is text in the entry, return true if the
- * submission was successful and false if not. If there is no text,
- * return true.
- */
- submitEntryText() {
- const text = this.$.entry.getText();
- if (!text.length) { return true; }
- const wasSubmitted = this._addAccountItem(text);
- if (wasSubmitted) { this.$.entry.clear(); }
- return wasSubmitted;
- }
-
- additions() {
- return this.accounts
- .filter(account => account._pendingAdd)
- .map(account => {
- if (account._group) {
- return {group: account};
- } else {
- return {account};
- }
- });
- }
-
- _computeEntryHidden(maxCount, accountsRecord, readonly) {
- return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
+ break;
}
}
- customElements.define(GrAccountList.is, GrAccountList);
-})();
+ _handleChipKeydown(e) {
+ const chip = e.target;
+ const chips = this.accountChips;
+ const index = chips.indexOf(chip);
+ switch (e.keyCode) {
+ case 8: // Backspace
+ case 13: // Enter
+ case 32: // Spacebar
+ case 46: // Delete
+ this._removeAccount(chip.account);
+ // Splice from this array to avoid inconsistent ordering of
+ // event handling.
+ chips.splice(index, 1);
+ if (index < chips.length) {
+ chips[index].focus();
+ } else if (index > 0) {
+ chips[index - 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ case 37: // Left arrow
+ if (index > 0) {
+ chip.blur();
+ chips[index - 1].focus();
+ }
+ break;
+ case 39: // Right arrow
+ chip.blur();
+ if (index < chips.length - 1) {
+ chips[index + 1].focus();
+ } else {
+ this.$.entry.focus();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Submit the text of the entry as a reviewer value, if it exists. If it is
+ * a successful submit of the text, clear the entry value.
+ *
+ * @return {boolean} If there is text in the entry, return true if the
+ * submission was successful and false if not. If there is no text,
+ * return true.
+ */
+ submitEntryText() {
+ const text = this.$.entry.getText();
+ if (!text.length) { return true; }
+ const wasSubmitted = this._addAccountItem(text);
+ if (wasSubmitted) { this.$.entry.clear(); }
+ return wasSubmitted;
+ }
+
+ additions() {
+ return this.accounts
+ .filter(account => account._pendingAdd)
+ .map(account => {
+ if (account._group) {
+ return {group: account};
+ } else {
+ return {account};
+ }
+ });
+ }
+
+ _computeEntryHidden(maxCount, accountsRecord, readonly) {
+ return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
+ }
+}
+
+customElements.define(GrAccountList.is, GrAccountList);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
index 37591d8..2438bc1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
@@ -1,28 +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="../gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../gr-account-entry/gr-account-entry.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-account-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
gr-account-chip {
display: inline-block;
@@ -53,27 +47,11 @@
-->
<div class="list">
<template id="chips" is="dom-repeat" items="[[accounts]]" as="account">
- <gr-account-chip
- account="[[account]]"
- class$="[[_computeChipClass(account)]]"
- data-account-id$="[[account._account_id]]"
- removable="[[_computeRemovable(account, readonly)]]"
- on-keydown="_handleChipKeydown"
- tabindex="-1">
+ <gr-account-chip account="[[account]]" class\$="[[_computeChipClass(account)]]" data-account-id\$="[[account._account_id]]" removable="[[_computeRemovable(account, readonly)]]" on-keydown="_handleChipKeydown" tabindex="-1">
</gr-account-chip>
</template>
</div>
- <gr-account-entry
- borderless
- hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
- id="entry"
- placeholder="[[placeholder]]"
- on-add="_handleAdd"
- on-input-keydown="_handleInputKeydown"
- allow-any-input="[[allowAnyInput]]"
- query-suggestions="[[_querySuggestions]]">
+ <gr-account-entry borderless="" hidden\$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]" id="entry" placeholder="[[placeholder]]" on-add="_handleAdd" on-input-keydown="_handleInputKeydown" allow-any-input="[[allowAnyInput]]" query-suggestions="[[_querySuggestions]]">
</gr-account-entry>
<slot></slot>
- </template>
- <script src="gr-account-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index 39d0a88..1fc78ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_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-list</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-list.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-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-account-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,505 +40,509 @@
</template>
</test-fixture>
-<script>
- class MockSuggestionsProvider {
- getSuggestions(input) {
- return Promise.resolve([]);
- }
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-account-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
- makeSuggestionItem(item) {
- return item;
- }
+class MockSuggestionsProvider {
+ getSuggestions(input) {
+ return Promise.resolve([]);
}
- suite('gr-account-list tests', async () => {
- await readyToTest();
- let _nextAccountId = 0;
- const makeAccount = function() {
- const accountId = ++_nextAccountId;
- return {
- _account_id: accountId,
- };
+ makeSuggestionItem(item) {
+ return item;
+ }
+}
+
+suite('gr-account-list tests', () => {
+ let _nextAccountId = 0;
+ const makeAccount = function() {
+ const accountId = ++_nextAccountId;
+ return {
+ _account_id: accountId,
};
- const makeGroup = function() {
- const groupId = 'group' + (++_nextAccountId);
- return {
- id: groupId,
- _group: true,
- };
+ };
+ const makeGroup = function() {
+ const groupId = 'group' + (++_nextAccountId);
+ return {
+ id: groupId,
+ _group: true,
};
+ };
- let existingAccount1;
- let existingAccount2;
- let sandbox;
- let element;
- let suggestionsProvider;
+ let existingAccount1;
+ let existingAccount2;
+ let sandbox;
+ let element;
+ let suggestionsProvider;
- function getChips() {
- return Polymer.dom(element.root).querySelectorAll('gr-account-chip');
- }
+ function getChips() {
+ return dom(element.root).querySelectorAll('gr-account-chip');
+ }
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ existingAccount1 = makeAccount();
+ existingAccount2 = makeAccount();
+
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ element.accounts = [existingAccount1, existingAccount2];
+ suggestionsProvider = new MockSuggestionsProvider();
+ element.suggestionsProvider = suggestionsProvider;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('account entry only appears when editable', () => {
+ element.readonly = false;
+ assert.isFalse(element.$.entry.hasAttribute('hidden'));
+ element.readonly = true;
+ assert.isTrue(element.$.entry.hasAttribute('hidden'));
+ });
+
+ test('addition and removal of account/group chips', () => {
+ flushAsynchronousOperations();
+ sandbox.stub(element, '_computeRemovable').returns(true);
+ // Existing accounts are listed.
+ let chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isFalse(chips[1].classList.contains('pendingAdd'));
+
+ // New accounts are added to end with pendingAdd class.
+ const newAccount = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: newAccount,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 3);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isFalse(chips[1].classList.contains('pendingAdd'));
+ assert.isTrue(chips[2].classList.contains('pendingAdd'));
+
+ // Removed accounts are taken out of the list.
+ element.fire('remove', {account: existingAccount1});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+ // Invalid remove is ignored.
+ element.fire('remove', {account: existingAccount1});
+ element.fire('remove', {account: newAccount});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 1);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+
+ // New groups are added to end with pendingAdd and group classes.
+ const newGroup = makeGroup();
+ element._handleAdd({
+ detail: {
+ value: {
+ group: newGroup,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 2);
+ assert.isTrue(chips[1].classList.contains('group'));
+ assert.isTrue(chips[1].classList.contains('pendingAdd'));
+
+ // Removed groups are taken out of the list.
+ element.fire('remove', {account: newGroup});
+ flushAsynchronousOperations();
+ chips = getChips();
+ assert.equal(chips.length, 1);
+ assert.isFalse(chips[0].classList.contains('pendingAdd'));
+ });
+
+ test('_getSuggestions uses filter correctly', done => {
+ const originalSuggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ _account_id: 3,
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ _account_id: 1,
+ },
+ {
+ email: 'xyz@example.com',
+ text: 'aaaaa',
+ _account_id: 25,
+ },
+ ];
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(originalSuggestions));
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
+ return {
+ name: suggestion.email,
+ value: suggestion._account_id,
+ };
+ });
+
+ element._getSuggestions().then(suggestions => {
+ // Default is no filtering.
+ assert.equal(suggestions.length, 3);
+
+ // Set up filter that only accepts suggestion1.
+ const accountId = originalSuggestions[0]._account_id;
+ element.filter = function(suggestion) {
+ return suggestion._account_id === accountId;
+ };
+
+ element._getSuggestions()
+ .then(suggestions => {
+ assert.deepEqual(suggestions,
+ [{name: originalSuggestions[0].email,
+ value: originalSuggestions[0]._account_id}]);
+ })
+ .then(done);
+ });
+ });
+
+ test('_computeChipClass', () => {
+ const account = makeAccount();
+ assert.equal(element._computeChipClass(account), '');
+ account._pendingAdd = true;
+ assert.equal(element._computeChipClass(account), 'pendingAdd');
+ account._group = true;
+ assert.equal(element._computeChipClass(account), 'group pendingAdd');
+ account._pendingAdd = false;
+ assert.equal(element._computeChipClass(account), 'group');
+ });
+
+ test('_computeRemovable', () => {
+ const newAccount = makeAccount();
+ newAccount._pendingAdd = true;
+ element.readonly = false;
+ element.removableValues = [];
+ assert.isFalse(element._computeRemovable(existingAccount1, false));
+ assert.isTrue(element._computeRemovable(newAccount, false));
+
+ element.removableValues = [existingAccount1];
+ assert.isTrue(element._computeRemovable(existingAccount1, false));
+ assert.isTrue(element._computeRemovable(newAccount, false));
+ assert.isFalse(element._computeRemovable(existingAccount2, false));
+
+ element.readonly = true;
+ assert.isFalse(element._computeRemovable(existingAccount1, true));
+ assert.isFalse(element._computeRemovable(newAccount, true));
+ });
+
+ test('submitEntryText', () => {
+ element.allowAnyInput = true;
+ flushAsynchronousOperations();
+
+ const getTextStub = sandbox.stub(element.$.entry, 'getText');
+ getTextStub.onFirstCall().returns('');
+ getTextStub.onSecondCall().returns('test');
+ getTextStub.onThirdCall().returns('test@test');
+
+ // When entry is empty, return true.
+ const clearStub = sandbox.stub(element.$.entry, 'clear');
+ assert.isTrue(element.submitEntryText());
+ assert.isFalse(clearStub.called);
+
+ // When entry is invalid, return false.
+ assert.isFalse(element.submitEntryText());
+ assert.isFalse(clearStub.called);
+
+ // When entry is valid, return true and clear text.
+ assert.isTrue(element.submitEntryText());
+ assert.isTrue(clearStub.called);
+ assert.equal(element.additions()[0].account.email, 'test@test');
+ });
+
+ test('additions returns sanitized new accounts and groups', () => {
+ assert.equal(element.additions().length, 0);
+
+ const newAccount = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: newAccount,
+ },
+ },
+ });
+ const newGroup = makeGroup();
+ element._handleAdd({
+ detail: {
+ value: {
+ group: newGroup,
+ },
+ },
+ });
+
+ assert.deepEqual(element.additions(), [
+ {
+ account: {
+ _account_id: newAccount._account_id,
+ _pendingAdd: true,
+ },
+ },
+ {
+ group: {
+ id: newGroup.id,
+ _group: true,
+ _pendingAdd: true,
+ },
+ },
+ ]);
+ });
+
+ test('large group confirmations', () => {
+ assert.isNull(element.pendingConfirmation);
+ assert.deepEqual(element.additions(), []);
+
+ const group = makeGroup();
+ const reviewer = {
+ group,
+ count: 10,
+ confirm: true,
+ };
+ element._handleAdd({
+ detail: {
+ value: reviewer,
+ },
+ });
+
+ assert.deepEqual(element.pendingConfirmation, reviewer);
+ assert.deepEqual(element.additions(), []);
+
+ element.confirmGroup(group);
+ assert.isNull(element.pendingConfirmation);
+ assert.deepEqual(element.additions(), [
+ {
+ group: {
+ id: group.id,
+ _group: true,
+ _pendingAdd: true,
+ confirmed: true,
+ },
+ },
+ ]);
+ });
+
+ test('removeAccount fails if account is not removable', () => {
+ element.readonly = true;
+ const acct = makeAccount();
+ element.accounts = [acct];
+ element._removeAccount(acct);
+ assert.equal(element.accounts.length, 1);
+ });
+
+ test('max-count', () => {
+ element.maxCount = 1;
+ const acct = makeAccount();
+ element._handleAdd({
+ detail: {
+ value: {
+ account: acct,
+ },
+ },
+ });
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.entry.hasAttribute('hidden'));
+ });
+
+ test('enter text calls suggestions provider', done => {
+ const suggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ },
+ ];
+ const getSuggestionsStub =
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(suggestions));
+
+ const makeSuggestionItemStub =
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+
+ const input = element.$.entry.$.input;
+
+ input.text = 'newTest';
+ MockInteractions.focus(input.$.input);
+ input.noDebounce = true;
+ flushAsynchronousOperations();
+ flush(() => {
+ assert.isTrue(getSuggestionsStub.calledOnce);
+ assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
+ assert.equal(makeSuggestionItemStub.getCalls().length, 2);
+ done();
+ });
+ });
+
+ test('suggestion on empty', done => {
+ element.skipSuggestOnEmpty = false;
+ const suggestions = [
+ {
+ email: 'abc@example.com',
+ text: 'abcd',
+ },
+ {
+ email: 'qwe@example.com',
+ text: 'qwer',
+ },
+ ];
+ const getSuggestionsStub =
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve(suggestions));
+
+ const makeSuggestionItemStub =
+ sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
+
+ const input = element.$.entry.$.input;
+
+ input.text = '';
+ MockInteractions.focus(input.$.input);
+ input.noDebounce = true;
+ flushAsynchronousOperations();
+ flush(() => {
+ assert.isTrue(getSuggestionsStub.calledOnce);
+ assert.equal(getSuggestionsStub.lastCall.args[0], '');
+ assert.equal(makeSuggestionItemStub.getCalls().length, 2);
+ done();
+ });
+ });
+
+ test('skip suggestion on empty', done => {
+ element.skipSuggestOnEmpty = true;
+ const getSuggestionsStub =
+ sandbox.stub(suggestionsProvider, 'getSuggestions')
+ .returns(Promise.resolve([]));
+
+ const input = element.$.entry.$.input;
+
+ input.text = '';
+ MockInteractions.focus(input.$.input);
+ input.noDebounce = true;
+ flushAsynchronousOperations();
+ flush(() => {
+ assert.isTrue(getSuggestionsStub.notCalled);
+ done();
+ });
+ });
+
+ suite('allowAnyInput', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- existingAccount1 = makeAccount();
- existingAccount2 = makeAccount();
-
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- element.accounts = [existingAccount1, existingAccount2];
- suggestionsProvider = new MockSuggestionsProvider();
- element.suggestionsProvider = suggestionsProvider;
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('account entry only appears when editable', () => {
- element.readonly = false;
- assert.isFalse(element.$.entry.hasAttribute('hidden'));
- element.readonly = true;
- assert.isTrue(element.$.entry.hasAttribute('hidden'));
- });
-
- test('addition and removal of account/group chips', () => {
- flushAsynchronousOperations();
- sandbox.stub(element, '_computeRemovable').returns(true);
- // Existing accounts are listed.
- let chips = getChips();
- assert.equal(chips.length, 2);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- assert.isFalse(chips[1].classList.contains('pendingAdd'));
-
- // New accounts are added to end with pendingAdd class.
- const newAccount = makeAccount();
- element._handleAdd({
- detail: {
- value: {
- account: newAccount,
- },
- },
- });
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 3);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- assert.isFalse(chips[1].classList.contains('pendingAdd'));
- assert.isTrue(chips[2].classList.contains('pendingAdd'));
-
- // Removed accounts are taken out of the list.
- element.fire('remove', {account: existingAccount1});
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 2);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- assert.isTrue(chips[1].classList.contains('pendingAdd'));
-
- // Invalid remove is ignored.
- element.fire('remove', {account: existingAccount1});
- element.fire('remove', {account: newAccount});
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 1);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
-
- // New groups are added to end with pendingAdd and group classes.
- const newGroup = makeGroup();
- element._handleAdd({
- detail: {
- value: {
- group: newGroup,
- },
- },
- });
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 2);
- assert.isTrue(chips[1].classList.contains('group'));
- assert.isTrue(chips[1].classList.contains('pendingAdd'));
-
- // Removed groups are taken out of the list.
- element.fire('remove', {account: newGroup});
- flushAsynchronousOperations();
- chips = getChips();
- assert.equal(chips.length, 1);
- assert.isFalse(chips[0].classList.contains('pendingAdd'));
- });
-
- test('_getSuggestions uses filter correctly', done => {
- const originalSuggestions = [
- {
- email: 'abc@example.com',
- text: 'abcd',
- _account_id: 3,
- },
- {
- email: 'qwe@example.com',
- text: 'qwer',
- _account_id: 1,
- },
- {
- email: 'xyz@example.com',
- text: 'aaaaa',
- _account_id: 25,
- },
- ];
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve(originalSuggestions));
- sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => {
- return {
- name: suggestion.email,
- value: suggestion._account_id,
- };
- });
-
- element._getSuggestions().then(suggestions => {
- // Default is no filtering.
- assert.equal(suggestions.length, 3);
-
- // Set up filter that only accepts suggestion1.
- const accountId = originalSuggestions[0]._account_id;
- element.filter = function(suggestion) {
- return suggestion._account_id === accountId;
- };
-
- element._getSuggestions()
- .then(suggestions => {
- assert.deepEqual(suggestions,
- [{name: originalSuggestions[0].email,
- value: originalSuggestions[0]._account_id}]);
- })
- .then(done);
- });
- });
-
- test('_computeChipClass', () => {
- const account = makeAccount();
- assert.equal(element._computeChipClass(account), '');
- account._pendingAdd = true;
- assert.equal(element._computeChipClass(account), 'pendingAdd');
- account._group = true;
- assert.equal(element._computeChipClass(account), 'group pendingAdd');
- account._pendingAdd = false;
- assert.equal(element._computeChipClass(account), 'group');
- });
-
- test('_computeRemovable', () => {
- const newAccount = makeAccount();
- newAccount._pendingAdd = true;
- element.readonly = false;
- element.removableValues = [];
- assert.isFalse(element._computeRemovable(existingAccount1, false));
- assert.isTrue(element._computeRemovable(newAccount, false));
-
- element.removableValues = [existingAccount1];
- assert.isTrue(element._computeRemovable(existingAccount1, false));
- assert.isTrue(element._computeRemovable(newAccount, false));
- assert.isFalse(element._computeRemovable(existingAccount2, false));
-
- element.readonly = true;
- assert.isFalse(element._computeRemovable(existingAccount1, true));
- assert.isFalse(element._computeRemovable(newAccount, true));
- });
-
- test('submitEntryText', () => {
element.allowAnyInput = true;
- flushAsynchronousOperations();
-
- const getTextStub = sandbox.stub(element.$.entry, 'getText');
- getTextStub.onFirstCall().returns('');
- getTextStub.onSecondCall().returns('test');
- getTextStub.onThirdCall().returns('test@test');
-
- // When entry is empty, return true.
- const clearStub = sandbox.stub(element.$.entry, 'clear');
- assert.isTrue(element.submitEntryText());
- assert.isFalse(clearStub.called);
-
- // When entry is invalid, return false.
- assert.isFalse(element.submitEntryText());
- assert.isFalse(clearStub.called);
-
- // When entry is valid, return true and clear text.
- assert.isTrue(element.submitEntryText());
- assert.isTrue(clearStub.called);
- assert.equal(element.additions()[0].account.email, 'test@test');
});
- test('additions returns sanitized new accounts and groups', () => {
- assert.equal(element.additions().length, 0);
-
- const newAccount = makeAccount();
- element._handleAdd({
- detail: {
- value: {
- account: newAccount,
- },
- },
- });
- const newGroup = makeGroup();
- element._handleAdd({
- detail: {
- value: {
- group: newGroup,
- },
- },
- });
-
- assert.deepEqual(element.additions(), [
- {
- account: {
- _account_id: newAccount._account_id,
- _pendingAdd: true,
- },
- },
- {
- group: {
- id: newGroup.id,
- _group: true,
- _pendingAdd: true,
- },
- },
- ]);
+ test('adds emails', () => {
+ const accountLen = element.accounts.length;
+ element._handleAdd({detail: {value: 'test@test'}});
+ assert.equal(element.accounts.length, accountLen + 1);
+ assert.equal(element.accounts[accountLen].email, 'test@test');
});
- test('large group confirmations', () => {
- assert.isNull(element.pendingConfirmation);
- assert.deepEqual(element.additions(), []);
-
- const group = makeGroup();
- const reviewer = {
- group,
- count: 10,
- confirm: true,
- };
- element._handleAdd({
- detail: {
- value: reviewer,
- },
- });
-
- assert.deepEqual(element.pendingConfirmation, reviewer);
- assert.deepEqual(element.additions(), []);
-
- element.confirmGroup(group);
- assert.isNull(element.pendingConfirmation);
- assert.deepEqual(element.additions(), [
- {
- group: {
- id: group.id,
- _group: true,
- _pendingAdd: true,
- confirmed: true,
- },
- },
- ]);
+ test('toasts on invalid email', () => {
+ const toastHandler = sandbox.stub();
+ element.addEventListener('show-alert', toastHandler);
+ element._handleAdd({detail: {value: 'test'}});
+ assert.isTrue(toastHandler.called);
});
+ });
- test('removeAccount fails if account is not removable', () => {
- element.readonly = true;
- const acct = makeAccount();
- element.accounts = [acct];
- element._removeAccount(acct);
- assert.equal(element.accounts.length, 1);
- });
+ test('_accountMatches', () => {
+ const acct = makeAccount();
- test('max-count', () => {
- element.maxCount = 1;
- const acct = makeAccount();
- element._handleAdd({
- detail: {
- value: {
- account: acct,
- },
- },
- });
- flushAsynchronousOperations();
- assert.isTrue(element.$.entry.hasAttribute('hidden'));
- });
+ assert.isTrue(element._accountMatches(acct, acct));
+ acct.email = 'test';
+ assert.isTrue(element._accountMatches(acct, acct));
+ assert.isTrue(element._accountMatches({email: 'test'}, acct));
- test('enter text calls suggestions provider', done => {
- const suggestions = [
- {
- email: 'abc@example.com',
- text: 'abcd',
- },
- {
- email: 'qwe@example.com',
- text: 'qwer',
- },
- ];
- const getSuggestionsStub =
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve(suggestions));
+ assert.isFalse(element._accountMatches({}, acct));
+ assert.isFalse(element._accountMatches({email: 'test2'}, acct));
+ assert.isFalse(element._accountMatches({_account_id: -1}, acct));
+ });
- const makeSuggestionItemStub =
- sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
-
+ suite('keyboard interactions', () => {
+ test('backspace at text input start removes last account', done => {
const input = element.$.entry.$.input;
-
- input.text = 'newTest';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- flushAsynchronousOperations();
+ sandbox.stub(input, '_updateSuggestions');
+ sandbox.stub(element, '_computeRemovable').returns(true);
flush(() => {
- assert.isTrue(getSuggestionsStub.calledOnce);
- assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
- assert.equal(makeSuggestionItemStub.getCalls().length, 2);
- done();
- });
- });
-
- test('suggestion on empty', done => {
- element.skipSuggestOnEmpty = false;
- const suggestions = [
- {
- email: 'abc@example.com',
- text: 'abcd',
- },
- {
- email: 'qwe@example.com',
- text: 'qwer',
- },
- ];
- const getSuggestionsStub =
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve(suggestions));
-
- const makeSuggestionItemStub =
- sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item);
-
- const input = element.$.entry.$.input;
-
- input.text = '';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- flushAsynchronousOperations();
- flush(() => {
- assert.isTrue(getSuggestionsStub.calledOnce);
- assert.equal(getSuggestionsStub.lastCall.args[0], '');
- assert.equal(makeSuggestionItemStub.getCalls().length, 2);
- done();
- });
- });
-
- test('skip suggestion on empty', done => {
- element.skipSuggestOnEmpty = true;
- const getSuggestionsStub =
- sandbox.stub(suggestionsProvider, 'getSuggestions')
- .returns(Promise.resolve([]));
-
- const input = element.$.entry.$.input;
-
- input.text = '';
- MockInteractions.focus(input.$.input);
- input.noDebounce = true;
- flushAsynchronousOperations();
- flush(() => {
- assert.isTrue(getSuggestionsStub.notCalled);
- done();
- });
- });
-
- suite('allowAnyInput', () => {
- setup(() => {
- element.allowAnyInput = true;
- });
-
- test('adds emails', () => {
- const accountLen = element.accounts.length;
- element._handleAdd({detail: {value: 'test@test'}});
- assert.equal(element.accounts.length, accountLen + 1);
- assert.equal(element.accounts[accountLen].email, 'test@test');
- });
-
- test('toasts on invalid email', () => {
- const toastHandler = sandbox.stub();
- element.addEventListener('show-alert', toastHandler);
- element._handleAdd({detail: {value: 'test'}});
- assert.isTrue(toastHandler.called);
- });
- });
-
- test('_accountMatches', () => {
- const acct = makeAccount();
-
- assert.isTrue(element._accountMatches(acct, acct));
- acct.email = 'test';
- assert.isTrue(element._accountMatches(acct, acct));
- assert.isTrue(element._accountMatches({email: 'test'}, acct));
-
- assert.isFalse(element._accountMatches({}, acct));
- assert.isFalse(element._accountMatches({email: 'test2'}, acct));
- assert.isFalse(element._accountMatches({_account_id: -1}, acct));
- });
-
- suite('keyboard interactions', () => {
- test('backspace at text input start removes last account', done => {
- const input = element.$.entry.$.input;
- sandbox.stub(input, '_updateSuggestions');
- sandbox.stub(element, '_computeRemovable').returns(true);
- flush(() => {
- // Next line is a workaround for Firefix not moving cursor
- // on input field update
- assert.equal(
- element._getNativeInput(input.$.input).selectionStart, 0);
- input.text = 'test';
- MockInteractions.focus(input.$.input);
- flushAsynchronousOperations();
- assert.equal(element.accounts.length, 2);
- MockInteractions.pressAndReleaseKeyOn(
- element._getNativeInput(input.$.input), 8); // Backspace
- assert.equal(element.accounts.length, 2);
- input.text = '';
- MockInteractions.pressAndReleaseKeyOn(
- element._getNativeInput(input.$.input), 8); // Backspace
- flushAsynchronousOperations();
- assert.equal(element.accounts.length, 1);
- done();
- });
- });
-
- test('arrow key navigation', done => {
- const input = element.$.entry.$.input;
+ // Next line is a workaround for Firefix not moving cursor
+ // on input field update
+ assert.equal(
+ element._getNativeInput(input.$.input).selectionStart, 0);
+ input.text = 'test';
+ MockInteractions.focus(input.$.input);
+ flushAsynchronousOperations();
+ assert.equal(element.accounts.length, 2);
+ MockInteractions.pressAndReleaseKeyOn(
+ element._getNativeInput(input.$.input), 8); // Backspace
+ assert.equal(element.accounts.length, 2);
input.text = '';
- element.accounts = [makeAccount(), makeAccount()];
- flush(() => {
- MockInteractions.focus(input.$.input);
- flushAsynchronousOperations();
- const chips = element.accountChips;
- const chipsOneSpy = sandbox.spy(chips[1], 'focus');
- MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
- assert.isTrue(chipsOneSpy.called);
- const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
- MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
- assert.isTrue(chipsZeroSpy.called);
- MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
- assert.isTrue(chipsZeroSpy.calledOnce);
- MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
- assert.isTrue(chipsOneSpy.calledTwice);
- done();
- });
+ MockInteractions.pressAndReleaseKeyOn(
+ element._getNativeInput(input.$.input), 8); // Backspace
+ flushAsynchronousOperations();
+ assert.equal(element.accounts.length, 1);
+ done();
});
+ });
- test('delete', done => {
- element.accounts = [makeAccount(), makeAccount()];
- flush(() => {
- const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
- const removeSpy = sandbox.spy(element, '_removeAccount');
- MockInteractions.pressAndReleaseKeyOn(
- element.accountChips[0], 8); // Backspace
- assert.isTrue(focusSpy.called);
- assert.isTrue(removeSpy.calledOnce);
+ test('arrow key navigation', done => {
+ const input = element.$.entry.$.input;
+ input.text = '';
+ element.accounts = [makeAccount(), makeAccount()];
+ flush(() => {
+ MockInteractions.focus(input.$.input);
+ flushAsynchronousOperations();
+ const chips = element.accountChips;
+ const chipsOneSpy = sandbox.spy(chips[1], 'focus');
+ MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
+ assert.isTrue(chipsOneSpy.called);
+ const chipsZeroSpy = sandbox.spy(chips[0], 'focus');
+ MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
+ assert.isTrue(chipsZeroSpy.called);
+ MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
+ assert.isTrue(chipsZeroSpy.calledOnce);
+ MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
+ assert.isTrue(chipsOneSpy.calledTwice);
+ done();
+ });
+ });
- MockInteractions.pressAndReleaseKeyOn(
- element.accountChips[1], 46); // Delete
- assert.isTrue(removeSpy.calledTwice);
- done();
- });
+ test('delete', done => {
+ element.accounts = [makeAccount(), makeAccount()];
+ flush(() => {
+ const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
+ const removeSpy = sandbox.spy(element, '_removeAccount');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.accountChips[0], 8); // Backspace
+ assert.isTrue(focusSpy.called);
+ assert.isTrue(removeSpy.calledOnce);
+
+ MockInteractions.pressAndReleaseKeyOn(
+ element.accountChips[1], 46); // Delete
+ assert.isTrue(removeSpy.calledTwice);
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 6a0769d..dc8eea3 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -14,94 +14,102 @@
* 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 GrAlert extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-alert'; }
- /**
- * Fired when the action button is pressed.
- *
- * @event action
- */
+import '../gr-button/gr-button.js';
+import '../../../styles/shared-styles.js';
+import '../../../scripts/rootElement.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-alert_html.js';
- static get properties() {
- return {
- text: String,
- actionText: String,
- /** @type {?string} */
- type: String,
- shown: {
- type: Boolean,
- value: true,
- readOnly: true,
- reflectToAttribute: true,
- },
- toast: {
- type: Boolean,
- value: true,
- reflectToAttribute: true,
- },
+/** @extends Polymer.Element */
+class GrAlert extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _hideActionButton: Boolean,
- _boundTransitionEndHandler: {
- type: Function,
- value() { return this._handleTransitionEnd.bind(this); },
- },
- _actionCallback: Function,
- };
- }
+ static get is() { return 'gr-alert'; }
+ /**
+ * Fired when the action button is pressed.
+ *
+ * @event action
+ */
- /** @override */
- attached() {
- super.attached();
- this.addEventListener('transitionend', this._boundTransitionEndHandler);
- }
+ static get properties() {
+ return {
+ text: String,
+ actionText: String,
+ /** @type {?string} */
+ type: String,
+ shown: {
+ type: Boolean,
+ value: true,
+ readOnly: true,
+ reflectToAttribute: true,
+ },
+ toast: {
+ type: Boolean,
+ value: true,
+ reflectToAttribute: true,
+ },
- /** @override */
- detached() {
- super.detached();
- this.removeEventListener('transitionend',
- this._boundTransitionEndHandler);
- }
+ _hideActionButton: Boolean,
+ _boundTransitionEndHandler: {
+ type: Function,
+ value() { return this._handleTransitionEnd.bind(this); },
+ },
+ _actionCallback: Function,
+ };
+ }
- show(text, opt_actionText, opt_actionCallback) {
- this.text = text;
- this.actionText = opt_actionText;
- this._hideActionButton = !opt_actionText;
- this._actionCallback = opt_actionCallback;
- Gerrit.getRootElement().appendChild(this);
- this._setShown(true);
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this.addEventListener('transitionend', this._boundTransitionEndHandler);
+ }
- hide() {
- this._setShown(false);
- if (this._hasZeroTransitionDuration()) {
- Gerrit.getRootElement().removeChild(this);
- }
- }
+ /** @override */
+ detached() {
+ super.detached();
+ this.removeEventListener('transitionend',
+ this._boundTransitionEndHandler);
+ }
- _hasZeroTransitionDuration() {
- const style = window.getComputedStyle(this);
- // transitionDuration is always given in seconds.
- const duration = Math.round(parseFloat(style.transitionDuration) * 100);
- return duration === 0;
- }
+ show(text, opt_actionText, opt_actionCallback) {
+ this.text = text;
+ this.actionText = opt_actionText;
+ this._hideActionButton = !opt_actionText;
+ this._actionCallback = opt_actionCallback;
+ Gerrit.getRootElement().appendChild(this);
+ this._setShown(true);
+ }
- _handleTransitionEnd(e) {
- if (this.shown) { return; }
-
+ hide() {
+ this._setShown(false);
+ if (this._hasZeroTransitionDuration()) {
Gerrit.getRootElement().removeChild(this);
}
-
- _handleActionTap(e) {
- e.preventDefault();
- if (this._actionCallback) { this._actionCallback(); }
- }
}
- customElements.define(GrAlert.is, GrAlert);
-})();
+ _hasZeroTransitionDuration() {
+ const style = window.getComputedStyle(this);
+ // transitionDuration is always given in seconds.
+ const duration = Math.round(parseFloat(style.transitionDuration) * 100);
+ return duration === 0;
+ }
+
+ _handleTransitionEnd(e) {
+ if (this.shown) { return; }
+
+ Gerrit.getRootElement().removeChild(this);
+ }
+
+ _handleActionTap(e) {
+ e.preventDefault();
+ if (this._actionCallback) { this._actionCallback(); }
+ }
+}
+
+customElements.define(GrAlert.is, GrAlert);
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
index 0d44164..1190516 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
@@ -1,28 +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="../gr-button/gr-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<script src="../../../scripts/rootElement.js"></script>
-
-<dom-module id="gr-alert">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/**
* ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING
@@ -48,7 +42,7 @@
* (as outside styles always win), .content-wrapper is introduced as a
* wrapper around main content to have better encapsulation, styles that
* may be affected by outside should be defined on it.
- * In this case, `padding:0px` is defined in main.css for all elements
+ * In this case, \`padding:0px\` is defined in main.css for all elements
* with the universal selector: *.
*/
.content-wrapper {
@@ -74,13 +68,6 @@
</style>
<div class="content-wrapper">
<span class="text">[[text]]</span>
- <gr-button
- link
- class="action"
- hidden$="[[_hideActionButton]]"
- on-click="_handleActionTap">[[actionText]]</gr-button>
+ <gr-button link="" class="action" hidden\$="[[_hideActionButton]]" on-click="_handleActionTap">[[actionText]]</gr-button>
</div>
- </template>
- <script src="gr-alert.js"></script>
-</dom-module>
-
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
index 68d782b..d291fc7 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -19,43 +19,45 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-alert</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-alert.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-alert.js"></script>
-<script>
- suite('gr-alert tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-alert.js';
+suite('gr-alert tests', () => {
+ let element;
- setup(() => {
- element = document.createElement('gr-alert');
- });
-
- teardown(() => {
- if (element.parentNode) {
- element.parentNode.removeChild(element);
- }
- });
-
- test('show/hide', () => {
- assert.isNull(element.parentNode);
- element.show();
- assert.equal(element.parentNode, document.body);
- element.updateStyles({'--gr-alert-transition-duration': '0ms'});
- element.hide();
- assert.isNull(element.parentNode);
- });
-
- test('action event', done => {
- element.show();
- element._actionCallback = done;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.action'));
- });
+ setup(() => {
+ element = document.createElement('gr-alert');
});
+
+ teardown(() => {
+ if (element.parentNode) {
+ element.parentNode.removeChild(element);
+ }
+ });
+
+ test('show/hide', () => {
+ assert.isNull(element.parentNode);
+ element.show();
+ assert.equal(element.parentNode, document.body);
+ element.updateStyles({'--gr-alert-transition-duration': '0ms'});
+ element.hide();
+ assert.isNull(element.parentNode);
+ });
+
+ test('action event', done => {
+ element.show();
+ element._actionCallback = done;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.action'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 5ca95e1..813d45d 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,179 +14,193 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../../../scripts/rootElement.js';
+import '../../../styles/shared-styles.js';
+import {flush} 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-autocomplete-dropdown_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Polymer.IronFitMixin
+ * @extends Polymer.Element
+ */
+class GrAutocompleteDropdown extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ IronFitBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-autocomplete-dropdown'; }
+ /**
+ * Fired when the dropdown is closed.
+ *
+ * @event dropdown-closed
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Polymer.IronFitMixin
- * @extends Polymer.Element
+ * Fired when item is selected.
+ *
+ * @event item-selected
*/
- class GrAutocompleteDropdown extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Polymer.IronFitBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-autocomplete-dropdown'; }
- /**
- * Fired when the dropdown is closed.
- *
- * @event dropdown-closed
- */
- /**
- * Fired when item is selected.
- *
- * @event item-selected
- */
+ static get properties() {
+ return {
+ index: Number,
+ isHidden: {
+ type: Boolean,
+ value: true,
+ reflectToAttribute: true,
+ },
+ verticalOffset: {
+ type: Number,
+ value: null,
+ },
+ horizontalOffset: {
+ type: Number,
+ value: null,
+ },
+ suggestions: {
+ type: Array,
+ value: () => [],
+ observer: '_resetCursorStops',
+ },
+ _suggestionEls: Array,
+ };
+ }
- static get properties() {
- return {
- index: Number,
- isHidden: {
- type: Boolean,
- value: true,
- reflectToAttribute: true,
- },
- verticalOffset: {
- type: Number,
- value: null,
- },
- horizontalOffset: {
- type: Number,
- value: null,
- },
- suggestions: {
- type: Array,
- value: () => [],
- observer: '_resetCursorStops',
- },
- _suggestionEls: Array,
- };
- }
+ get keyBindings() {
+ return {
+ up: '_handleUp',
+ down: '_handleDown',
+ enter: '_handleEnter',
+ esc: '_handleEscape',
+ tab: '_handleTab',
+ };
+ }
- get keyBindings() {
- return {
- up: '_handleUp',
- down: '_handleDown',
- enter: '_handleEnter',
- esc: '_handleEscape',
- tab: '_handleTab',
- };
- }
+ close() {
+ this.isHidden = true;
+ }
- close() {
- this.isHidden = true;
- }
+ open() {
+ this.isHidden = false;
+ this._resetCursorStops();
+ // Refit should run after we call Polymer.flush inside _resetCursorStops
+ this.refit();
+ }
- open() {
- this.isHidden = false;
- this._resetCursorStops();
- // Refit should run after we call Polymer.flush inside _resetCursorStops
- this.refit();
- }
+ getCurrentText() {
+ return this.getCursorTarget().dataset.value;
+ }
- getCurrentText() {
- return this.getCursorTarget().dataset.value;
- }
-
- _handleUp(e) {
- if (!this.isHidden) {
- e.preventDefault();
- e.stopPropagation();
- this.cursorUp();
- }
- }
-
- _handleDown(e) {
- if (!this.isHidden) {
- e.preventDefault();
- e.stopPropagation();
- this.cursorDown();
- }
- }
-
- cursorDown() {
- if (!this.isHidden) {
- this.$.cursor.next();
- }
- }
-
- cursorUp() {
- if (!this.isHidden) {
- this.$.cursor.previous();
- }
- }
-
- _handleTab(e) {
+ _handleUp(e) {
+ if (!this.isHidden) {
e.preventDefault();
e.stopPropagation();
- this.fire('item-selected', {
- trigger: 'tab',
- selected: this.$.cursor.target,
- });
- }
-
- _handleEnter(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('item-selected', {
- trigger: 'enter',
- selected: this.$.cursor.target,
- });
- }
-
- _handleEscape() {
- this._fireClose();
- this.close();
- }
-
- _handleClickItem(e) {
- e.preventDefault();
- e.stopPropagation();
- let selected = e.target;
- while (!selected.classList.contains('autocompleteOption')) {
- if (!selected || selected === this) { return; }
- selected = selected.parentElement;
- }
- this.fire('item-selected', {
- trigger: 'click',
- selected,
- });
- }
-
- _fireClose() {
- this.fire('dropdown-closed');
- }
-
- getCursorTarget() {
- return this.$.cursor.target;
- }
-
- _resetCursorStops() {
- if (this.suggestions.length > 0) {
- if (!this.isHidden) {
- Polymer.dom.flush();
- this._suggestionEls = Array.from(
- this.$.suggestions.querySelectorAll('li'));
- this._resetCursorIndex();
- }
- } else {
- this._suggestionEls = [];
- }
- }
-
- _resetCursorIndex() {
- this.$.cursor.setCursorAtIndex(0);
- }
-
- _computeLabelClass(item) {
- return item.label ? '' : 'hide';
+ this.cursorUp();
}
}
- customElements.define(GrAutocompleteDropdown.is, GrAutocompleteDropdown);
-})();
+ _handleDown(e) {
+ if (!this.isHidden) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.cursorDown();
+ }
+ }
+
+ cursorDown() {
+ if (!this.isHidden) {
+ this.$.cursor.next();
+ }
+ }
+
+ cursorUp() {
+ if (!this.isHidden) {
+ this.$.cursor.previous();
+ }
+ }
+
+ _handleTab(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('item-selected', {
+ trigger: 'tab',
+ selected: this.$.cursor.target,
+ });
+ }
+
+ _handleEnter(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('item-selected', {
+ trigger: 'enter',
+ selected: this.$.cursor.target,
+ });
+ }
+
+ _handleEscape() {
+ this._fireClose();
+ this.close();
+ }
+
+ _handleClickItem(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ let selected = e.target;
+ while (!selected.classList.contains('autocompleteOption')) {
+ if (!selected || selected === this) { return; }
+ selected = selected.parentElement;
+ }
+ this.fire('item-selected', {
+ trigger: 'click',
+ selected,
+ });
+ }
+
+ _fireClose() {
+ this.fire('dropdown-closed');
+ }
+
+ getCursorTarget() {
+ return this.$.cursor.target;
+ }
+
+ _resetCursorStops() {
+ if (this.suggestions.length > 0) {
+ if (!this.isHidden) {
+ flush();
+ this._suggestionEls = Array.from(
+ this.$.suggestions.querySelectorAll('li'));
+ this._resetCursorIndex();
+ }
+ } else {
+ this._suggestionEls = [];
+ }
+ }
+
+ _resetCursorIndex() {
+ this.$.cursor.setCursorAtIndex(0);
+ }
+
+ _computeLabelClass(item) {
+ return item.label ? '' : 'hide';
+ }
+}
+
+customElements.define(GrAutocompleteDropdown.is, GrAutocompleteDropdown);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
index 649cd22..711315d 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
@@ -1,32 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/iron-fit-behavior/iron-fit-behavior.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<script src="../../../scripts/rootElement.js"></script>
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-autocomplete-dropdown">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
z-index: 100;
@@ -76,33 +66,15 @@
display: none;
}
</style>
- <div
- class="dropdown-content"
- slot="dropdown-content"
- id="suggestions"
- role="listbox">
+ <div class="dropdown-content" slot="dropdown-content" id="suggestions" role="listbox">
<ul>
<template is="dom-repeat" items="[[suggestions]]">
- <li data-index$="[[index]]"
- data-value$="[[item.dataValue]]"
- tabindex="-1"
- aria-label$="[[item.name]]"
- class="autocompleteOption"
- role="option"
- on-click="_handleClickItem">
+ <li data-index\$="[[index]]" data-value\$="[[item.dataValue]]" tabindex="-1" aria-label\$="[[item.name]]" class="autocompleteOption" role="option" on-click="_handleClickItem">
<span>[[item.text]]</span>
- <span class$="label [[_computeLabelClass(item)]]">[[item.label]]</span>
+ <span class\$="label [[_computeLabelClass(item)]]">[[item.label]]</span>
</li>
</template>
</ul>
</div>
- <gr-cursor-manager
- id="cursor"
- index="{{index}}"
- cursor-target-class="selected"
- scroll-behavior="never"
- focus-on-move
- stops="[[_suggestionEls]]"></gr-cursor-manager>
- </template>
- <script src="gr-autocomplete-dropdown.js"></script>
-</dom-module>
+ <gr-cursor-manager id="cursor" index="{{index}}" cursor-target-class="selected" scroll-behavior="never" focus-on-move="" stops="[[_suggestionEls]]"></gr-cursor-manager>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
index 9ea8259..a18fcac 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-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-autocomplete-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-autocomplete-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-autocomplete-dropdown.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-autocomplete-dropdown.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,124 +40,125 @@
</template>
</test-fixture>
-<script>
- suite('gr-autocomplete-dropdown', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-autocomplete-dropdown.js';
+suite('gr-autocomplete-dropdown', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.open();
- element.suggestions = [
- {dataValue: 'test value 1', name: 'test name 1', text: 1, label: 'hi'},
- {dataValue: 'test value 2', name: 'test name 2', text: 2}];
- flushAsynchronousOperations();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.open();
+ element.suggestions = [
+ {dataValue: 'test value 1', name: 'test name 1', text: 1, label: 'hi'},
+ {dataValue: 'test value 2', name: 'test name 2', text: 2}];
+ flushAsynchronousOperations();
+ });
- teardown(() => {
- sandbox.restore();
- if (element.isOpen) element.close();
- });
+ teardown(() => {
+ sandbox.restore();
+ if (element.isOpen) element.close();
+ });
- test('shows labels', () => {
- const els = element.$.suggestions.querySelectorAll('li');
- assert.equal(els[0].innerText.trim(), '1\nhi');
- assert.equal(els[1].innerText.trim(), '2');
- });
+ test('shows labels', () => {
+ const els = element.$.suggestions.querySelectorAll('li');
+ assert.equal(els[0].innerText.trim(), '1\nhi');
+ assert.equal(els[1].innerText.trim(), '2');
+ });
- test('escape key', done => {
- const closeSpy = sandbox.spy(element, 'close');
- MockInteractions.pressAndReleaseKeyOn(element, 27);
- flushAsynchronousOperations();
- assert.isTrue(closeSpy.called);
- done();
- });
+ test('escape key', done => {
+ const closeSpy = sandbox.spy(element, 'close');
+ MockInteractions.pressAndReleaseKeyOn(element, 27);
+ flushAsynchronousOperations();
+ assert.isTrue(closeSpy.called);
+ done();
+ });
- test('tab key', () => {
- const handleTabSpy = sandbox.spy(element, '_handleTab');
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
- MockInteractions.pressAndReleaseKeyOn(element, 9);
- assert.isTrue(handleTabSpy.called);
- assert.equal(element.$.cursor.index, 0);
- assert.isTrue(itemSelectedStub.called);
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'tab',
- selected: element.getCursorTarget(),
- });
- });
-
- test('enter key', () => {
- const handleEnterSpy = sandbox.spy(element, '_handleEnter');
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
- MockInteractions.pressAndReleaseKeyOn(element, 13);
- assert.isTrue(handleEnterSpy.called);
- assert.equal(element.$.cursor.index, 0);
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'enter',
- selected: element.getCursorTarget(),
- });
- });
-
- test('down key', () => {
- element.isHidden = true;
- const nextSpy = sandbox.spy(element.$.cursor, 'next');
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isFalse(nextSpy.called);
- assert.equal(element.$.cursor.index, 0);
- element.isHidden = false;
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isTrue(nextSpy.called);
- assert.equal(element.$.cursor.index, 1);
- });
-
- test('up key', () => {
- element.isHidden = true;
- const prevSpy = sandbox.spy(element.$.cursor, 'previous');
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isFalse(prevSpy.called);
- assert.equal(element.$.cursor.index, 0);
- element.isHidden = false;
- element.$.cursor.setCursorAtIndex(1);
- assert.equal(element.$.cursor.index, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isTrue(prevSpy.called);
- assert.equal(element.$.cursor.index, 0);
- });
-
- test('tapping selects item', () => {
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
-
- MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]);
- flushAsynchronousOperations();
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'click',
- selected: element.$.suggestions.querySelectorAll('li')[1],
- });
- });
-
- test('tapping child still selects item', () => {
- const itemSelectedStub = sandbox.stub();
- element.addEventListener('item-selected', itemSelectedStub);
-
- MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[0]
- .lastElementChild);
- flushAsynchronousOperations();
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'click',
- selected: element.$.suggestions.querySelectorAll('li')[0],
- });
- });
-
- test('updated suggestions resets cursor stops', () => {
- const resetStopsSpy = sandbox.spy(element, '_resetCursorStops');
- element.suggestions = [];
- assert.isTrue(resetStopsSpy.called);
+ test('tab key', () => {
+ const handleTabSpy = sandbox.spy(element, '_handleTab');
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ MockInteractions.pressAndReleaseKeyOn(element, 9);
+ assert.isTrue(handleTabSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ assert.isTrue(itemSelectedStub.called);
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'tab',
+ selected: element.getCursorTarget(),
});
});
+ test('enter key', () => {
+ const handleEnterSpy = sandbox.spy(element, '_handleEnter');
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ MockInteractions.pressAndReleaseKeyOn(element, 13);
+ assert.isTrue(handleEnterSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'enter',
+ selected: element.getCursorTarget(),
+ });
+ });
+
+ test('down key', () => {
+ element.isHidden = true;
+ const nextSpy = sandbox.spy(element.$.cursor, 'next');
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isFalse(nextSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ element.isHidden = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isTrue(nextSpy.called);
+ assert.equal(element.$.cursor.index, 1);
+ });
+
+ test('up key', () => {
+ element.isHidden = true;
+ const prevSpy = sandbox.spy(element.$.cursor, 'previous');
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isFalse(prevSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ element.isHidden = false;
+ element.$.cursor.setCursorAtIndex(1);
+ assert.equal(element.$.cursor.index, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isTrue(prevSpy.called);
+ assert.equal(element.$.cursor.index, 0);
+ });
+
+ test('tapping selects item', () => {
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+
+ MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]);
+ flushAsynchronousOperations();
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'click',
+ selected: element.$.suggestions.querySelectorAll('li')[1],
+ });
+ });
+
+ test('tapping child still selects item', () => {
+ const itemSelectedStub = sandbox.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+
+ MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[0]
+ .lastElementChild);
+ flushAsynchronousOperations();
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'click',
+ selected: element.$.suggestions.querySelectorAll('li')[0],
+ });
+ });
+
+ test('updated suggestions resets cursor stops', () => {
+ const resetStopsSpy = sandbox.spy(element, '_resetCursorStops');
+ element.suggestions = [];
+ assert.isTrue(resetStopsSpy.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 60985c1..22b62db 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,449 +14,463 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
- const DEBOUNCE_WAIT_MS = 200;
+import '@polymer/paper-input/paper-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-icons/gr-icons.js';
+import '../../../styles/shared-styles.js';
+import {flush, 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-autocomplete_html.js';
+
+const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+const DEBOUNCE_WAIT_MS = 200;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrAutocomplete extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-autocomplete'; }
+ /**
+ * Fired when a value is chosen.
+ *
+ * @event commit
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when the user cancels.
+ *
+ * @event cancel
*/
- class GrAutocomplete extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-autocomplete'; }
- /**
- * Fired when a value is chosen.
- *
- * @event commit
- */
- /**
- * Fired when the user cancels.
- *
- * @event cancel
- */
+ /**
+ * Fired on keydown to allow for custom hooks into autocomplete textbox
+ * behavior.
+ *
+ * @event input-keydown
+ */
- /**
- * Fired on keydown to allow for custom hooks into autocomplete textbox
- * behavior.
- *
- * @event input-keydown
- */
+ static get properties() {
+ return {
- static get properties() {
- return {
-
- /**
- * Query for requesting autocomplete suggestions. The function should
- * accept the input as a string parameter and return a promise. The
- * promise yields an array of suggestion objects with "name", "label",
- * "value" properties. The "name" property will be displayed in the
- * suggestion entry. The "label" property will, when specified, appear
- * next to the "name" as label text. The "value" property will be emitted
- * if that suggestion is selected.
- *
- * @type {function(string): Promise<?>}
- */
- query: {
- type: Function,
- value() {
- return function() {
- return Promise.resolve([]);
- };
- },
+ /**
+ * Query for requesting autocomplete suggestions. The function should
+ * accept the input as a string parameter and return a promise. The
+ * promise yields an array of suggestion objects with "name", "label",
+ * "value" properties. The "name" property will be displayed in the
+ * suggestion entry. The "label" property will, when specified, appear
+ * next to the "name" as label text. The "value" property will be emitted
+ * if that suggestion is selected.
+ *
+ * @type {function(string): Promise<?>}
+ */
+ query: {
+ type: Function,
+ value() {
+ return function() {
+ return Promise.resolve([]);
+ };
},
+ },
- /**
- * The number of characters that must be typed before suggestions are
- * made. If threshold is zero, default suggestions are enabled.
- */
- threshold: {
- type: Number,
- value: 1,
- },
+ /**
+ * The number of characters that must be typed before suggestions are
+ * made. If threshold is zero, default suggestions are enabled.
+ */
+ threshold: {
+ type: Number,
+ value: 1,
+ },
- allowNonSuggestedValues: Boolean,
- borderless: Boolean,
- disabled: Boolean,
- showSearchIcon: {
- type: Boolean,
- value: false,
- },
- /**
- * Vertical offset needed for an element with 20px line-height, 4px
- * padding and 1px border (30px height total). Plus 1px spacing between
- * input and dropdown. Inputs with different line-height or padding will
- * need to tweak vertical offset.
- */
- verticalOffset: {
- type: Number,
- value: 31,
- },
+ allowNonSuggestedValues: Boolean,
+ borderless: Boolean,
+ disabled: Boolean,
+ showSearchIcon: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * Vertical offset needed for an element with 20px line-height, 4px
+ * padding and 1px border (30px height total). Plus 1px spacing between
+ * input and dropdown. Inputs with different line-height or padding will
+ * need to tweak vertical offset.
+ */
+ verticalOffset: {
+ type: Number,
+ value: 31,
+ },
- text: {
- type: String,
- value: '',
- notify: true,
- },
+ text: {
+ type: String,
+ value: '',
+ notify: true,
+ },
- placeholder: String,
+ placeholder: String,
- clearOnCommit: {
- type: Boolean,
- value: false,
- },
+ clearOnCommit: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * When true, tab key autocompletes but does not fire the commit event.
- * When false, tab key not caught, and focus is removed from the element.
- * See Issue 4556, Issue 6645.
- */
- tabComplete: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true, tab key autocompletes but does not fire the commit event.
+ * When false, tab key not caught, and focus is removed from the element.
+ * See Issue 4556, Issue 6645.
+ */
+ tabComplete: {
+ type: Boolean,
+ value: false,
+ },
- value: {
- type: String,
- notify: true,
- },
+ value: {
+ type: String,
+ notify: true,
+ },
- /**
- * Multi mode appends autocompleted entries to the value.
- * If false, autocompleted entries replace value.
- */
- multi: {
- type: Boolean,
- value: false,
- },
+ /**
+ * Multi mode appends autocompleted entries to the value.
+ * If false, autocompleted entries replace value.
+ */
+ multi: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * When true and uncommitted text is left in the autocomplete input after
- * blurring, the text will appear red.
- */
- warnUncommitted: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true and uncommitted text is left in the autocomplete input after
+ * blurring, the text will appear red.
+ */
+ warnUncommitted: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * When true, querying for suggestions is not debounced w/r/t keypresses
- */
- noDebounce: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true, querying for suggestions is not debounced w/r/t keypresses
+ */
+ noDebounce: {
+ type: Boolean,
+ value: false,
+ },
- /** @type {?} */
- _suggestions: {
- type: Array,
- value() { return []; },
- },
+ /** @type {?} */
+ _suggestions: {
+ type: Array,
+ value() { return []; },
+ },
- _suggestionEls: {
- type: Array,
- value() { return []; },
- },
+ _suggestionEls: {
+ type: Array,
+ value() { return []; },
+ },
- _index: Number,
- _disableSuggestions: {
- type: Boolean,
- value: false,
- },
- _focused: {
- type: Boolean,
- value: false,
- },
+ _index: Number,
+ _disableSuggestions: {
+ type: Boolean,
+ value: false,
+ },
+ _focused: {
+ type: Boolean,
+ value: false,
+ },
- /** The DOM element of the selected suggestion. */
- _selected: Object,
- };
+ /** The DOM element of the selected suggestion. */
+ _selected: Object,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_maybeOpenDropdown(_suggestions, _focused)',
+ '_updateSuggestions(text, threshold, noDebounce)',
+ ];
+ }
+
+ get _nativeInput() {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ return this.$.input.$.nativeInput || this.$.input.inputElement;
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ this.listen(document.body, 'click', '_handleBodyClick');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(document.body, 'click', '_handleBodyClick');
+ this.cancelDebouncer('update-suggestions');
+ }
+
+ get focusStart() {
+ return this.$.input;
+ }
+
+ focus() {
+ this._nativeInput.focus();
+ }
+
+ selectAll() {
+ const nativeInputElement = this._nativeInput;
+ if (!this.$.input.value) { return; }
+ nativeInputElement.setSelectionRange(0, this.$.input.value.length);
+ }
+
+ clear() {
+ this.text = '';
+ }
+
+ _handleItemSelect(e) {
+ // Let _handleKeydown deal with keyboard interaction.
+ if (e.detail.trigger !== 'click') { return; }
+ this._selected = e.detail.selected;
+ this._commit();
+ }
+
+ get _inputElement() {
+ // Polymer2: this.$ can be undefined when this is first evaluated.
+ return this.$ && this.$.input;
+ }
+
+ /**
+ * Set the text of the input without triggering the suggestion dropdown.
+ *
+ * @param {string} text The new text for the input.
+ */
+ setText(text) {
+ this._disableSuggestions = true;
+ this.text = text;
+ this._disableSuggestions = false;
+ }
+
+ _onInputFocus() {
+ this._focused = true;
+ this._updateSuggestions(this.text, this.threshold, this.noDebounce);
+ this.$.input.classList.remove('warnUncommitted');
+ // Needed so that --paper-input-container-input updated style is applied.
+ this.updateStyles();
+ }
+
+ _onInputBlur() {
+ this.$.input.classList.toggle('warnUncommitted',
+ this.warnUncommitted && this.text.length && !this._focused);
+ // Needed so that --paper-input-container-input updated style is applied.
+ this.updateStyles();
+ }
+
+ _updateSuggestions(text, threshold, noDebounce) {
+ // Polymer 2: check for undefined
+ if ([text, threshold, noDebounce].some(arg => arg === undefined)) {
+ return;
}
- static get observers() {
- return [
- '_maybeOpenDropdown(_suggestions, _focused)',
- '_updateSuggestions(text, threshold, noDebounce)',
- ];
+ // Reset _suggestions for every update
+ // This will also prevent from carrying over suggestions:
+ // @see Issue 12039
+ this._suggestions = [];
+
+ // TODO(taoalpha): Also skip if text has not changed
+
+ if (this._disableSuggestions) { return; }
+ if (text.length < threshold) {
+ this.value = '';
+ return;
}
- get _nativeInput() {
- // In Polymer 2 inputElement isn't nativeInput anymore
- return this.$.input.$.nativeInput || this.$.input.inputElement;
+ if (!this._focused) {
+ return;
}
- /** @override */
- attached() {
- super.attached();
- this.listen(document.body, 'click', '_handleBodyClick');
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(document.body, 'click', '_handleBodyClick');
- this.cancelDebouncer('update-suggestions');
- }
-
- get focusStart() {
- return this.$.input;
- }
-
- focus() {
- this._nativeInput.focus();
- }
-
- selectAll() {
- const nativeInputElement = this._nativeInput;
- if (!this.$.input.value) { return; }
- nativeInputElement.setSelectionRange(0, this.$.input.value.length);
- }
-
- clear() {
- this.text = '';
- }
-
- _handleItemSelect(e) {
- // Let _handleKeydown deal with keyboard interaction.
- if (e.detail.trigger !== 'click') { return; }
- this._selected = e.detail.selected;
- this._commit();
- }
-
- get _inputElement() {
- // Polymer2: this.$ can be undefined when this is first evaluated.
- return this.$ && this.$.input;
- }
-
- /**
- * Set the text of the input without triggering the suggestion dropdown.
- *
- * @param {string} text The new text for the input.
- */
- setText(text) {
- this._disableSuggestions = true;
- this.text = text;
- this._disableSuggestions = false;
- }
-
- _onInputFocus() {
- this._focused = true;
- this._updateSuggestions(this.text, this.threshold, this.noDebounce);
- this.$.input.classList.remove('warnUncommitted');
- // Needed so that --paper-input-container-input updated style is applied.
- this.updateStyles();
- }
-
- _onInputBlur() {
- this.$.input.classList.toggle('warnUncommitted',
- this.warnUncommitted && this.text.length && !this._focused);
- // Needed so that --paper-input-container-input updated style is applied.
- this.updateStyles();
- }
-
- _updateSuggestions(text, threshold, noDebounce) {
- // Polymer 2: check for undefined
- if ([text, threshold, noDebounce].some(arg => arg === undefined)) {
- return;
- }
-
- // Reset _suggestions for every update
- // This will also prevent from carrying over suggestions:
- // @see Issue 12039
- this._suggestions = [];
-
- // TODO(taoalpha): Also skip if text has not changed
-
- if (this._disableSuggestions) { return; }
- if (text.length < threshold) {
- this.value = '';
- return;
- }
-
- if (!this._focused) {
- return;
- }
-
- const update = () => {
- this.query(text).then(suggestions => {
- if (text !== this.text) {
- // Late response.
- return;
- }
- for (const suggestion of suggestions) {
- suggestion.text = suggestion.name;
- }
- this._suggestions = suggestions;
- Polymer.dom.flush();
- if (this._index === -1) {
- this.value = '';
- }
- });
- };
-
- if (noDebounce) {
- update();
- } else {
- this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
- }
- }
-
- _maybeOpenDropdown(suggestions, focused) {
- if (suggestions.length > 0 && focused) {
- return this.$.suggestions.open();
- }
- return this.$.suggestions.close();
- }
-
- _computeClass(borderless) {
- return borderless ? 'borderless' : '';
- }
-
- /**
- * _handleKeydown used for key handling in the this.$.input AND all child
- * autocomplete options.
- */
- _handleKeydown(e) {
- this._focused = true;
- switch (e.keyCode) {
- case 38: // Up
- e.preventDefault();
- this.$.suggestions.cursorUp();
- break;
- case 40: // Down
- e.preventDefault();
- this.$.suggestions.cursorDown();
- break;
- case 27: // Escape
- e.preventDefault();
- this._cancel();
- break;
- case 9: // Tab
- if (this._suggestions.length > 0 && this.tabComplete) {
- e.preventDefault();
- this._handleInputCommit(true);
- this.focus();
- } else {
- this._focused = false;
- }
- break;
- case 13: // Enter
- if (this.modifierPressed(e)) { break; }
- e.preventDefault();
- this._handleInputCommit();
- break;
- default:
- // For any normal keypress, return focus to the input to allow for
- // unbroken user input.
- this.focus();
-
- // Since this has been a normal keypress, the suggestions will have
- // been based on a previous input. Clear them. This prevents an
- // outdated suggestion from being used if the input keystroke is
- // immediately followed by a commit keystroke. @see Issue 8655
- this._suggestions = [];
- }
- this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input});
- }
-
- _cancel() {
- if (this._suggestions.length) {
- this.set('_suggestions', []);
- } else {
- this.fire('cancel');
- }
- }
-
- /**
- * @param {boolean=} opt_tabComplete
- */
- _handleInputCommit(opt_tabComplete) {
- // Nothing to do if the dropdown is not open.
- if (!this.allowNonSuggestedValues &&
- this.$.suggestions.isHidden) { return; }
-
- this._selected = this.$.suggestions.getCursorTarget();
- this._commit(opt_tabComplete);
- }
-
- _updateValue(suggestion, suggestions) {
- if (!suggestion) { return; }
- const completed = suggestions[suggestion.dataset.index].value;
- if (this.multi) {
- // Append the completed text to the end of the string.
- // Allow spaces within quoted terms.
- const tokens = this.text.match(TOKENIZE_REGEX);
- tokens[tokens.length - 1] = completed;
- this.value = tokens.join(' ');
- } else {
- this.value = completed;
- }
- }
-
- _handleBodyClick(e) {
- const eventPath = Polymer.dom(e).path;
- for (let i = 0; i < eventPath.length; i++) {
- if (eventPath[i] === this) {
+ const update = () => {
+ this.query(text).then(suggestions => {
+ if (text !== this.text) {
+ // Late response.
return;
}
- }
- this._focused = false;
- }
-
- _handleSuggestionTap(e) {
- e.stopPropagation();
- this.$.cursor.setCursor(e.target);
- this._commit();
- }
-
- /**
- * Commits the suggestion, optionally firing the commit event.
- *
- * @param {boolean=} opt_silent Allows for silent committing of an
- * autocomplete suggestion in order to handle cases like tab-to-complete
- * without firing the commit event.
- */
- _commit(opt_silent) {
- // Allow values that are not in suggestion list iff suggestions are empty.
- if (this._suggestions.length > 0) {
- this._updateValue(this._selected, this._suggestions);
- } else {
- this.value = this.text || '';
- }
-
- const value = this.value;
-
- // Value and text are mirrors of each other in multi mode.
- if (this.multi) {
- this.setText(this.value);
- } else {
- if (!this.clearOnCommit && this._selected) {
- this.setText(this._suggestions[this._selected.dataset.index].name);
- } else {
- this.clear();
+ for (const suggestion of suggestions) {
+ suggestion.text = suggestion.name;
}
- }
+ this._suggestions = suggestions;
+ flush();
+ if (this._index === -1) {
+ this.value = '';
+ }
+ });
+ };
- this._suggestions = [];
- if (!opt_silent) {
- this.fire('commit', {value});
- }
-
- this._textChangedSinceCommit = false;
- }
-
- _computeShowSearchIconClass(showSearchIcon) {
- return showSearchIcon ? 'showSearchIcon' : '';
+ if (noDebounce) {
+ update();
+ } else {
+ this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
}
}
- customElements.define(GrAutocomplete.is, GrAutocomplete);
-})();
+ _maybeOpenDropdown(suggestions, focused) {
+ if (suggestions.length > 0 && focused) {
+ return this.$.suggestions.open();
+ }
+ return this.$.suggestions.close();
+ }
+
+ _computeClass(borderless) {
+ return borderless ? 'borderless' : '';
+ }
+
+ /**
+ * _handleKeydown used for key handling in the this.$.input AND all child
+ * autocomplete options.
+ */
+ _handleKeydown(e) {
+ this._focused = true;
+ switch (e.keyCode) {
+ case 38: // Up
+ e.preventDefault();
+ this.$.suggestions.cursorUp();
+ break;
+ case 40: // Down
+ e.preventDefault();
+ this.$.suggestions.cursorDown();
+ break;
+ case 27: // Escape
+ e.preventDefault();
+ this._cancel();
+ break;
+ case 9: // Tab
+ if (this._suggestions.length > 0 && this.tabComplete) {
+ e.preventDefault();
+ this._handleInputCommit(true);
+ this.focus();
+ } else {
+ this._focused = false;
+ }
+ break;
+ case 13: // Enter
+ if (this.modifierPressed(e)) { break; }
+ e.preventDefault();
+ this._handleInputCommit();
+ break;
+ default:
+ // For any normal keypress, return focus to the input to allow for
+ // unbroken user input.
+ this.focus();
+
+ // Since this has been a normal keypress, the suggestions will have
+ // been based on a previous input. Clear them. This prevents an
+ // outdated suggestion from being used if the input keystroke is
+ // immediately followed by a commit keystroke. @see Issue 8655
+ this._suggestions = [];
+ }
+ this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input});
+ }
+
+ _cancel() {
+ if (this._suggestions.length) {
+ this.set('_suggestions', []);
+ } else {
+ this.fire('cancel');
+ }
+ }
+
+ /**
+ * @param {boolean=} opt_tabComplete
+ */
+ _handleInputCommit(opt_tabComplete) {
+ // Nothing to do if the dropdown is not open.
+ if (!this.allowNonSuggestedValues &&
+ this.$.suggestions.isHidden) { return; }
+
+ this._selected = this.$.suggestions.getCursorTarget();
+ this._commit(opt_tabComplete);
+ }
+
+ _updateValue(suggestion, suggestions) {
+ if (!suggestion) { return; }
+ const completed = suggestions[suggestion.dataset.index].value;
+ if (this.multi) {
+ // Append the completed text to the end of the string.
+ // Allow spaces within quoted terms.
+ const tokens = this.text.match(TOKENIZE_REGEX);
+ tokens[tokens.length - 1] = completed;
+ this.value = tokens.join(' ');
+ } else {
+ this.value = completed;
+ }
+ }
+
+ _handleBodyClick(e) {
+ const eventPath = dom(e).path;
+ for (let i = 0; i < eventPath.length; i++) {
+ if (eventPath[i] === this) {
+ return;
+ }
+ }
+ this._focused = false;
+ }
+
+ _handleSuggestionTap(e) {
+ e.stopPropagation();
+ this.$.cursor.setCursor(e.target);
+ this._commit();
+ }
+
+ /**
+ * Commits the suggestion, optionally firing the commit event.
+ *
+ * @param {boolean=} opt_silent Allows for silent committing of an
+ * autocomplete suggestion in order to handle cases like tab-to-complete
+ * without firing the commit event.
+ */
+ _commit(opt_silent) {
+ // Allow values that are not in suggestion list iff suggestions are empty.
+ if (this._suggestions.length > 0) {
+ this._updateValue(this._selected, this._suggestions);
+ } else {
+ this.value = this.text || '';
+ }
+
+ const value = this.value;
+
+ // Value and text are mirrors of each other in multi mode.
+ if (this.multi) {
+ this.setText(this.value);
+ } else {
+ if (!this.clearOnCommit && this._selected) {
+ this.setText(this._suggestions[this._selected.dataset.index].name);
+ } else {
+ this.clear();
+ }
+ }
+
+ this._suggestions = [];
+ if (!opt_silent) {
+ this.fire('commit', {value});
+ }
+
+ this._textChangedSinceCommit = false;
+ }
+
+ _computeShowSearchIconClass(showSearchIcon) {
+ return showSearchIcon ? 'showSearchIcon' : '';
+ }
+}
+
+customElements.define(GrAutocomplete.is, GrAutocomplete);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
index 381ef0b..11726d9 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
@@ -1,30 +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="/bower_components/paper-input/paper-input.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-autocomplete-dropdown/gr-autocomplete-dropdown.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-autocomplete">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.searchIcon {
display: none;
@@ -82,38 +74,14 @@
}
}
</style>
- <paper-input
- no-label-float
- id="input"
- class$="[[_computeClass(borderless)]]"
- disabled$="[[disabled]]"
- value="{{text}}"
- placeholder="[[placeholder]]"
- on-keydown="_handleKeydown"
- on-focus="_onInputFocus"
- on-blur="_onInputBlur"
- autocomplete="off">
+ <paper-input no-label-float="" id="input" class\$="[[_computeClass(borderless)]]" disabled\$="[[disabled]]" value="{{text}}" placeholder="[[placeholder]]" on-keydown="_handleKeydown" on-focus="_onInputFocus" on-blur="_onInputBlur" autocomplete="off">
<!-- prefix as attribute is required to for polymer 1 -->
- <div slot="prefix" prefix>
- <iron-icon
- icon="gr-icons:search"
- class$="searchIcon [[_computeShowSearchIconClass(showSearchIcon)]]">
+ <div slot="prefix" prefix="">
+ <iron-icon icon="gr-icons:search" class\$="searchIcon [[_computeShowSearchIconClass(showSearchIcon)]]">
</iron-icon>
</div>
</paper-input>
- <gr-autocomplete-dropdown
- vertical-align="top"
- vertical-offset="[[verticalOffset]]"
- horizontal-align="left"
- id="suggestions"
- on-item-selected="_handleItemSelect"
- on-keydown="_handleKeydown"
- suggestions="[[_suggestions]]"
- role="listbox"
- index="[[_index]]"
- position-target="[[_inputElement]]">
+ <gr-autocomplete-dropdown vertical-align="top" vertical-offset="[[verticalOffset]]" horizontal-align="left" id="suggestions" on-item-selected="_handleItemSelect" on-keydown="_handleKeydown" suggestions="[[_suggestions]]" role="listbox" index="[[_index]]" position-target="[[_inputElement]]">
</gr-autocomplete-dropdown>
- </template>
- <script src="gr-autocomplete.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 295572f..8a8f2f5 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_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-reviewer-list</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-autocomplete.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-autocomplete.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-autocomplete.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,580 +40,583 @@
</template>
</test-fixture>
-<script>
- suite('gr-autocomplete tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const focusOnInput = element => {
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-autocomplete.js';
+import {dom, flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-autocomplete tests', () => {
+ let element;
+ let sandbox;
+ const focusOnInput = element => {
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+ };
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('renders', () => {
+ let promise;
+ const queryStub = sandbox.spy(input => promise = Promise.resolve([
+ {name: input + ' 0', value: 0},
+ {name: input + ' 1', value: 1},
+ {name: input + ' 2', value: 2},
+ {name: input + ' 3', value: 3},
+ {name: input + ' 4', value: 4},
+ ]));
+ element.query = queryStub;
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.equal(element.$.suggestions.$.cursor.index, -1);
+
+ focusOnInput(element);
+ element.text = 'blah';
+
+ assert.isTrue(queryStub.called);
+ element._focused = true;
+
+ return promise.then(() => {
+ assert.isFalse(element.$.suggestions.isHidden);
+ const suggestions =
+ dom(element.$.suggestions.root).querySelectorAll('li');
+ assert.equal(suggestions.length, 5);
+
+ for (let i = 0; i < 5; i++) {
+ assert.equal(suggestions[i].innerText.trim(), 'blah ' + i);
+ }
+
+ assert.notEqual(element.$.suggestions.$.cursor.index, -1);
+ });
+ });
+
+ test('selectAll', done => {
+ flush(() => {
+ const nativeInput = element._nativeInput;
+ const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
+
+ element.selectAll();
+ assert.isFalse(selectionStub.called);
+
+ element.$.input.value = 'test';
+ element.selectAll();
+ assert.isTrue(selectionStub.called);
+ done();
+ });
+ });
+
+ test('esc key behavior', done => {
+ let promise;
+ const queryStub = sandbox.spy(() => promise = Promise.resolve([
+ {name: 'blah', value: 123},
+ ]));
+ element.query = queryStub;
+
+ assert.isTrue(element.$.suggestions.isHidden);
+
+ element._focused = true;
+ element.text = 'blah';
+
+ promise.then(() => {
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ const cancelHandler = sandbox.spy();
+ element.addEventListener('cancel', cancelHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
+ assert.isFalse(cancelHandler.called);
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.equal(element._suggestions.length, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
+ assert.isTrue(cancelHandler.called);
+ done();
+ });
+ });
+
+ test('emits commit and handles cursor movement', done => {
+ let promise;
+ const queryStub = sandbox.spy(input => promise = Promise.resolve([
+ {name: input + ' 0', value: 0},
+ {name: input + ' 1', value: 1},
+ {name: input + ' 2', value: 2},
+ {name: input + ' 3', value: 3},
+ {name: input + ' 4', value: 4},
+ ]));
+ element.query = queryStub;
+
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.equal(element.$.suggestions.$.cursor.index, -1);
+ element._focused = true;
+ element.text = 'blah';
+
+ promise.then(() => {
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ assert.equal(element.$.suggestions.$.cursor.index, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
+ 'down');
+
+ assert.equal(element.$.suggestions.$.cursor.index, 1);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
+ 'down');
+
+ assert.equal(element.$.suggestions.$.cursor.index, 2);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up');
+
+ assert.equal(element.$.suggestions.$.cursor.index, 1);
+
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
'enter');
- };
+
+ assert.equal(element.value, 1);
+ assert.isTrue(commitHandler.called);
+ assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
+ assert.isTrue(element.$.suggestions.isHidden);
+ assert.isTrue(element._focused);
+ done();
+ });
+ });
+
+ test('clear-on-commit behavior (off)', done => {
+ let promise;
+ const queryStub = sandbox.spy(() => {
+ promise = Promise.resolve([{name: 'suggestion', value: 0}]);
+ return promise;
+ });
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'blah';
+
+ promise.then(() => {
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+
+ assert.isTrue(commitHandler.called);
+ assert.equal(element.text, 'suggestion');
+ done();
+ });
+ });
+
+ test('clear-on-commit behavior (on)', done => {
+ let promise;
+ const queryStub = sandbox.spy(() => {
+ promise = Promise.resolve([{name: 'suggestion', value: 0}]);
+ return promise;
+ });
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'blah';
+ element.clearOnCommit = true;
+
+ promise.then(() => {
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+
+ assert.isTrue(commitHandler.called);
+ assert.equal(element.text, '');
+ done();
+ });
+ });
+
+ test('threshold guards the query', () => {
+ const queryStub = sandbox.spy(() => Promise.resolve([]));
+ element.query = queryStub;
+ element.threshold = 2;
+ focusOnInput(element);
+ element.text = 'a';
+ assert.isFalse(queryStub.called);
+ element.text = 'ab';
+ assert.isTrue(queryStub.called);
+ });
+
+ test('noDebounce=false debounces the query', () => {
+ const queryStub = sandbox.spy(() => Promise.resolve([]));
+ let callback;
+ const debounceStub = sandbox.stub(element, 'debounce',
+ (name, cb) => { callback = cb; });
+ element.query = queryStub;
+ element.noDebounce = false;
+ focusOnInput(element);
+ element.text = 'a';
+ assert.isFalse(queryStub.called);
+ assert.isTrue(debounceStub.called);
+ assert.equal(debounceStub.lastCall.args[2], 200);
+ assert.isFunction(callback);
+ callback();
+ assert.isTrue(queryStub.called);
+ });
+
+ test('_computeClass respects border property', () => {
+ assert.equal(element._computeClass(), '');
+ assert.equal(element._computeClass(false), '');
+ assert.equal(element._computeClass(true), 'borderless');
+ });
+
+ test('undefined or empty text results in no suggestions', () => {
+ element._updateSuggestions(undefined, 0, null);
+ assert.equal(element._suggestions.length, 0);
+ });
+
+ test('when focused', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ element.suggestOnlyWhenFocus = true;
+ focusOnInput(element);
+ element.text = 'bla';
+ assert.equal(element._focused, true);
+ flushAsynchronousOperations();
+ promise.then(() => {
+ assert.equal(element._suggestions.length, 1);
+ assert.equal(queryStub.notCalled, false);
+ done();
+ });
+ });
+
+ test('when not focused', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ element.suggestOnlyWhenFocus = true;
+ element.text = 'bla';
+ assert.equal(element._focused, false);
+ flushAsynchronousOperations();
+ promise.then(() => {
+ assert.equal(element._suggestions.length, 0);
+ done();
+ });
+ });
+
+ test('suggestions should not carry over', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'bla';
+ flushAsynchronousOperations();
+ promise.then(() => {
+ assert.equal(element._suggestions.length, 1);
+ element._updateSuggestions('', 0, false);
+ assert.equal(element._suggestions.length, 0);
+ done();
+ });
+ });
+
+ test('multi completes only the last part of the query', done => {
+ let promise;
+ const queryStub = sandbox.stub()
+ .returns(promise = Promise.resolve([
+ {name: 'suggestion', value: 0},
+ ]));
+ element.query = queryStub;
+ focusOnInput(element);
+ element.text = 'blah blah';
+ element.multi = true;
+
+ promise.then(() => {
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+
+ assert.isTrue(commitHandler.called);
+ assert.equal(element.text, 'blah 0');
+ done();
+ });
+ });
+
+ test('tabComplete flag functions', () => {
+ // commitHandler checks for the commit event, whereas commitSpy checks for
+ // the _commit function of the element.
+ const commitHandler = sandbox.spy();
+ element.addEventListener('commit', commitHandler);
+ const commitSpy = sandbox.spy(element, '_commit');
+ element._focused = true;
+
+ element._suggestions = ['tunnel snakes rule!'];
+ element.tabComplete = false;
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ assert.isFalse(commitHandler.called);
+ assert.isFalse(commitSpy.called);
+ assert.isFalse(element._focused);
+
+ element.tabComplete = true;
+ element._focused = true;
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ assert.isFalse(commitHandler.called);
+ assert.isTrue(commitSpy.called);
+ assert.isTrue(element._focused);
+ });
+
+ test('_focused flag properly triggered', done => {
+ flush(() => {
+ assert.isFalse(element._focused);
+ const input = element.shadowRoot
+ .querySelector('paper-input').inputElement;
+ MockInteractions.focus(input);
+ assert.isTrue(element._focused);
+ done();
+ });
+ });
+
+ test('search icon shows with showSearchIcon property', done => {
+ flush(() => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('iron-icon')).display,
+ 'none');
+ element.showSearchIcon = true;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('iron-icon')).display,
+ 'none');
+ done();
+ });
+ });
+
+ test('vertical offset overridden by param if it exists', () => {
+ assert.equal(element.$.suggestions.verticalOffset, 31);
+ element.verticalOffset = 30;
+ assert.equal(element.$.suggestions.verticalOffset, 30);
+ });
+
+ test('_focused flag shows/hides the suggestions', () => {
+ const openStub = sandbox.stub(element.$.suggestions, 'open');
+ const closedStub = sandbox.stub(element.$.suggestions, 'close');
+ element._suggestions = ['hello', 'its me'];
+ assert.isFalse(openStub.called);
+ assert.isTrue(closedStub.calledOnce);
+ element._focused = true;
+ assert.isTrue(openStub.calledOnce);
+ element._suggestions = [];
+ assert.isTrue(closedStub.calledTwice);
+ assert.isTrue(openStub.calledOnce);
+ });
+
+ test('_handleInputCommit with autocomplete hidden does nothing without' +
+ 'without allowNonSuggestedValues', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.$.suggestions.isHidden = true;
+ element._handleInputCommit();
+ assert.isFalse(commitStub.called);
+ });
+
+ test('_handleInputCommit with autocomplete hidden with' +
+ 'allowNonSuggestedValues', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.allowNonSuggestedValues = true;
+ element.$.suggestions.isHidden = true;
+ element._handleInputCommit();
+ assert.isTrue(commitStub.called);
+ });
+
+ test('_handleInputCommit with autocomplete open calls commit', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.$.suggestions.isHidden = false;
+ element._handleInputCommit();
+ assert.isTrue(commitStub.calledOnce);
+ });
+
+ test('_handleInputCommit with autocomplete open calls commit' +
+ 'with allowNonSuggestedValues', () => {
+ const commitStub = sandbox.stub(element, '_commit');
+ element.allowNonSuggestedValues = true;
+ element.$.suggestions.isHidden = false;
+ element._handleInputCommit();
+ assert.isTrue(commitStub.calledOnce);
+ });
+
+ test('issue 8655', () => {
+ function makeSuggestion(s) { return {name: s, text: s, value: s}; }
+ const keydownSpy = sandbox.spy(element, '_handleKeydown');
+ element.setText('file:');
+ element._suggestions =
+ [makeSuggestion('file:'), makeSuggestion('-file:')];
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 88, null, 'x');
+ // Must set the value, because the MockInteraction does not.
+ element.$.input.value = 'file:x';
+ assert.isTrue(keydownSpy.calledOnce);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.input,
+ 13,
+ null,
+ 'enter'
+ );
+ assert.isTrue(keydownSpy.calledTwice);
+ assert.equal(element.text, 'file:x');
+ });
+
+ suite('focus', () => {
+ let commitSpy;
+ let focusSpy;
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ commitSpy = sandbox.spy(element, '_commit');
});
- teardown(() => {
- sandbox.restore();
- });
+ test('enter does not call focus', () => {
+ element._suggestions = ['sugar bombs'];
+ focusSpy = sandbox.spy(element, 'focus');
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+ 'enter');
+ flushAsynchronousOperations();
- test('renders', () => {
- let promise;
- const queryStub = sandbox.spy(input => promise = Promise.resolve([
- {name: input + ' 0', value: 0},
- {name: input + ' 1', value: 1},
- {name: input + ' 2', value: 2},
- {name: input + ' 3', value: 3},
- {name: input + ' 4', value: 4},
- ]));
- element.query = queryStub;
- assert.isTrue(element.$.suggestions.isHidden);
- assert.equal(element.$.suggestions.$.cursor.index, -1);
-
- focusOnInput(element);
- element.text = 'blah';
-
- assert.isTrue(queryStub.called);
- element._focused = true;
-
- return promise.then(() => {
- assert.isFalse(element.$.suggestions.isHidden);
- const suggestions =
- Polymer.dom(element.$.suggestions.root).querySelectorAll('li');
- assert.equal(suggestions.length, 5);
-
- for (let i = 0; i < 5; i++) {
- assert.equal(suggestions[i].innerText.trim(), 'blah ' + i);
- }
-
- assert.notEqual(element.$.suggestions.$.cursor.index, -1);
- });
- });
-
- test('selectAll', done => {
- flush(() => {
- const nativeInput = element._nativeInput;
- const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange');
-
- element.selectAll();
- assert.isFalse(selectionStub.called);
-
- element.$.input.value = 'test';
- element.selectAll();
- assert.isTrue(selectionStub.called);
- done();
- });
- });
-
- test('esc key behavior', done => {
- let promise;
- const queryStub = sandbox.spy(() => promise = Promise.resolve([
- {name: 'blah', value: 123},
- ]));
- element.query = queryStub;
-
- assert.isTrue(element.$.suggestions.isHidden);
-
- element._focused = true;
- element.text = 'blah';
-
- promise.then(() => {
- assert.isFalse(element.$.suggestions.isHidden);
-
- const cancelHandler = sandbox.spy();
- element.addEventListener('cancel', cancelHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
- assert.isFalse(cancelHandler.called);
- assert.isTrue(element.$.suggestions.isHidden);
- assert.equal(element._suggestions.length, 0);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
- assert.isTrue(cancelHandler.called);
- done();
- });
- });
-
- test('emits commit and handles cursor movement', done => {
- let promise;
- const queryStub = sandbox.spy(input => promise = Promise.resolve([
- {name: input + ' 0', value: 0},
- {name: input + ' 1', value: 1},
- {name: input + ' 2', value: 2},
- {name: input + ' 3', value: 3},
- {name: input + ' 4', value: 4},
- ]));
- element.query = queryStub;
-
- assert.isTrue(element.$.suggestions.isHidden);
- assert.equal(element.$.suggestions.$.cursor.index, -1);
- element._focused = true;
- element.text = 'blah';
-
- promise.then(() => {
- assert.isFalse(element.$.suggestions.isHidden);
-
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- assert.equal(element.$.suggestions.$.cursor.index, 0);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
- 'down');
-
- assert.equal(element.$.suggestions.$.cursor.index, 1);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
- 'down');
-
- assert.equal(element.$.suggestions.$.cursor.index, 2);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up');
-
- assert.equal(element.$.suggestions.$.cursor.index, 1);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.equal(element.value, 1);
- assert.isTrue(commitHandler.called);
- assert.equal(commitHandler.getCall(0).args[0].detail.value, 1);
- assert.isTrue(element.$.suggestions.isHidden);
- assert.isTrue(element._focused);
- done();
- });
- });
-
- test('clear-on-commit behavior (off)', done => {
- let promise;
- const queryStub = sandbox.spy(() => {
- promise = Promise.resolve([{name: 'suggestion', value: 0}]);
- return promise;
- });
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'blah';
-
- promise.then(() => {
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.isTrue(commitHandler.called);
- assert.equal(element.text, 'suggestion');
- done();
- });
- });
-
- test('clear-on-commit behavior (on)', done => {
- let promise;
- const queryStub = sandbox.spy(() => {
- promise = Promise.resolve([{name: 'suggestion', value: 0}]);
- return promise;
- });
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'blah';
- element.clearOnCommit = true;
-
- promise.then(() => {
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.isTrue(commitHandler.called);
- assert.equal(element.text, '');
- done();
- });
- });
-
- test('threshold guards the query', () => {
- const queryStub = sandbox.spy(() => Promise.resolve([]));
- element.query = queryStub;
- element.threshold = 2;
- focusOnInput(element);
- element.text = 'a';
- assert.isFalse(queryStub.called);
- element.text = 'ab';
- assert.isTrue(queryStub.called);
- });
-
- test('noDebounce=false debounces the query', () => {
- const queryStub = sandbox.spy(() => Promise.resolve([]));
- let callback;
- const debounceStub = sandbox.stub(element, 'debounce',
- (name, cb) => { callback = cb; });
- element.query = queryStub;
- element.noDebounce = false;
- focusOnInput(element);
- element.text = 'a';
- assert.isFalse(queryStub.called);
- assert.isTrue(debounceStub.called);
- assert.equal(debounceStub.lastCall.args[2], 200);
- assert.isFunction(callback);
- callback();
- assert.isTrue(queryStub.called);
- });
-
- test('_computeClass respects border property', () => {
- assert.equal(element._computeClass(), '');
- assert.equal(element._computeClass(false), '');
- assert.equal(element._computeClass(true), 'borderless');
- });
-
- test('undefined or empty text results in no suggestions', () => {
- element._updateSuggestions(undefined, 0, null);
+ assert.isTrue(commitSpy.called);
+ assert.isFalse(focusSpy.called);
assert.equal(element._suggestions.length, 0);
});
- test('when focused', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- element.suggestOnlyWhenFocus = true;
- focusOnInput(element);
- element.text = 'bla';
- assert.equal(element._focused, true);
- flushAsynchronousOperations();
- promise.then(() => {
- assert.equal(element._suggestions.length, 1);
- assert.equal(queryStub.notCalled, false);
- done();
- });
- });
-
- test('when not focused', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- element.suggestOnlyWhenFocus = true;
- element.text = 'bla';
- assert.equal(element._focused, false);
- flushAsynchronousOperations();
- promise.then(() => {
- assert.equal(element._suggestions.length, 0);
- done();
- });
- });
-
- test('suggestions should not carry over', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'bla';
- flushAsynchronousOperations();
- promise.then(() => {
- assert.equal(element._suggestions.length, 1);
- element._updateSuggestions('', 0, false);
- assert.equal(element._suggestions.length, 0);
- done();
- });
- });
-
- test('multi completes only the last part of the query', done => {
- let promise;
- const queryStub = sandbox.stub()
- .returns(promise = Promise.resolve([
- {name: 'suggestion', value: 0},
- ]));
- element.query = queryStub;
- focusOnInput(element);
- element.text = 'blah blah';
- element.multi = true;
-
- promise.then(() => {
- const commitHandler = sandbox.spy();
- element.addEventListener('commit', commitHandler);
-
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
-
- assert.isTrue(commitHandler.called);
- assert.equal(element.text, 'blah 0');
- done();
- });
- });
-
- test('tabComplete flag functions', () => {
- // commitHandler checks for the commit event, whereas commitSpy checks for
- // the _commit function of the element.
- const commitHandler = sandbox.spy();
+ test('tab in input, tabComplete = true', () => {
+ focusSpy = sandbox.spy(element, 'focus');
+ const commitHandler = sandbox.stub();
element.addEventListener('commit', commitHandler);
- const commitSpy = sandbox.spy(element, '_commit');
- element._focused = true;
-
- element._suggestions = ['tunnel snakes rule!'];
- element.tabComplete = false;
+ element.tabComplete = true;
+ element._suggestions = ['tunnel snakes drool'];
MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ flushAsynchronousOperations();
+
+ assert.isTrue(commitSpy.called);
+ assert.isTrue(focusSpy.called);
assert.isFalse(commitHandler.called);
+ assert.equal(element._suggestions.length, 0);
+ });
+
+ test('tab in input, tabComplete = false', () => {
+ element._suggestions = ['sugar bombs'];
+ focusSpy = sandbox.spy(element, 'focus');
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ flushAsynchronousOperations();
+
+ assert.isFalse(commitSpy.called);
+ assert.isFalse(focusSpy.called);
+ assert.equal(element._suggestions.length, 1);
+ });
+
+ test('tab on suggestion, tabComplete = false', () => {
+ element._suggestions = [{name: 'sugar bombs'}];
+ element._focused = true;
+ // When tabComplete is false, do not focus.
+ element.tabComplete = false;
+ focusSpy = sandbox.spy(element, 'focus');
+ flush$0();
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.suggestions.shadowRoot
+ .querySelector('li:first-child'), 9, null, 'tab');
+ flushAsynchronousOperations();
assert.isFalse(commitSpy.called);
assert.isFalse(element._focused);
+ });
- element.tabComplete = true;
+ test('tab on suggestion, tabComplete = true', () => {
+ element._suggestions = [{name: 'sugar bombs'}];
element._focused = true;
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
- assert.isFalse(commitHandler.called);
+ // When tabComplete is true, focus.
+ element.tabComplete = true;
+ focusSpy = sandbox.spy(element, 'focus');
+ flush$0();
+ assert.isFalse(element.$.suggestions.isHidden);
+
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.suggestions.shadowRoot
+ .querySelector('li:first-child'), 9, null, 'tab');
+ flushAsynchronousOperations();
+
assert.isTrue(commitSpy.called);
assert.isTrue(element._focused);
});
- test('_focused flag properly triggered', done => {
- flush(() => {
- assert.isFalse(element._focused);
- const input = element.shadowRoot
- .querySelector('paper-input').inputElement;
- MockInteractions.focus(input);
- assert.isTrue(element._focused);
- done();
- });
- });
-
- test('search icon shows with showSearchIcon property', done => {
- flush(() => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('iron-icon')).display,
- 'none');
- element.showSearchIcon = true;
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('iron-icon')).display,
- 'none');
- done();
- });
- });
-
- test('vertical offset overridden by param if it exists', () => {
- assert.equal(element.$.suggestions.verticalOffset, 31);
- element.verticalOffset = 30;
- assert.equal(element.$.suggestions.verticalOffset, 30);
- });
-
- test('_focused flag shows/hides the suggestions', () => {
- const openStub = sandbox.stub(element.$.suggestions, 'open');
- const closedStub = sandbox.stub(element.$.suggestions, 'close');
- element._suggestions = ['hello', 'its me'];
- assert.isFalse(openStub.called);
- assert.isTrue(closedStub.calledOnce);
+ test('tap on suggestion commits, does not call focus', () => {
+ focusSpy = sandbox.spy(element, 'focus');
element._focused = true;
- assert.isTrue(openStub.calledOnce);
- element._suggestions = [];
- assert.isTrue(closedStub.calledTwice);
- assert.isTrue(openStub.calledOnce);
- });
-
- test('_handleInputCommit with autocomplete hidden does nothing without' +
- 'without allowNonSuggestedValues', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.$.suggestions.isHidden = true;
- element._handleInputCommit();
- assert.isFalse(commitStub.called);
- });
-
- test('_handleInputCommit with autocomplete hidden with' +
- 'allowNonSuggestedValues', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.allowNonSuggestedValues = true;
- element.$.suggestions.isHidden = true;
- element._handleInputCommit();
- assert.isTrue(commitStub.called);
- });
-
- test('_handleInputCommit with autocomplete open calls commit', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.$.suggestions.isHidden = false;
- element._handleInputCommit();
- assert.isTrue(commitStub.calledOnce);
- });
-
- test('_handleInputCommit with autocomplete open calls commit' +
- 'with allowNonSuggestedValues', () => {
- const commitStub = sandbox.stub(element, '_commit');
- element.allowNonSuggestedValues = true;
- element.$.suggestions.isHidden = false;
- element._handleInputCommit();
- assert.isTrue(commitStub.calledOnce);
- });
-
- test('issue 8655', () => {
- function makeSuggestion(s) { return {name: s, text: s, value: s}; }
- const keydownSpy = sandbox.spy(element, '_handleKeydown');
- element.setText('file:');
- element._suggestions =
- [makeSuggestion('file:'), makeSuggestion('-file:')];
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 88, null, 'x');
- // Must set the value, because the MockInteraction does not.
- element.$.input.value = 'file:x';
- assert.isTrue(keydownSpy.calledOnce);
- MockInteractions.pressAndReleaseKeyOn(
- element.$.input,
- 13,
- null,
- 'enter'
- );
- assert.isTrue(keydownSpy.calledTwice);
- assert.equal(element.text, 'file:x');
- });
-
- suite('focus', () => {
- let commitSpy;
- let focusSpy;
-
- setup(() => {
- commitSpy = sandbox.spy(element, '_commit');
- });
-
- test('enter does not call focus', () => {
- element._suggestions = ['sugar bombs'];
- focusSpy = sandbox.spy(element, 'focus');
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
- 'enter');
- flushAsynchronousOperations();
-
- assert.isTrue(commitSpy.called);
- assert.isFalse(focusSpy.called);
- assert.equal(element._suggestions.length, 0);
- });
-
- test('tab in input, tabComplete = true', () => {
- focusSpy = sandbox.spy(element, 'focus');
- const commitHandler = sandbox.stub();
- element.addEventListener('commit', commitHandler);
- element.tabComplete = true;
- element._suggestions = ['tunnel snakes drool'];
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
- flushAsynchronousOperations();
-
- assert.isTrue(commitSpy.called);
- assert.isTrue(focusSpy.called);
- assert.isFalse(commitHandler.called);
- assert.equal(element._suggestions.length, 0);
- });
-
- test('tab in input, tabComplete = false', () => {
- element._suggestions = ['sugar bombs'];
- focusSpy = sandbox.spy(element, 'focus');
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
- flushAsynchronousOperations();
-
- assert.isFalse(commitSpy.called);
- assert.isFalse(focusSpy.called);
- assert.equal(element._suggestions.length, 1);
- });
-
- test('tab on suggestion, tabComplete = false', () => {
- element._suggestions = [{name: 'sugar bombs'}];
- element._focused = true;
- // When tabComplete is false, do not focus.
- element.tabComplete = false;
- focusSpy = sandbox.spy(element, 'focus');
- Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.isHidden);
-
- MockInteractions.pressAndReleaseKeyOn(
- element.$.suggestions.shadowRoot
- .querySelector('li:first-child'), 9, null, 'tab');
- flushAsynchronousOperations();
- assert.isFalse(commitSpy.called);
- assert.isFalse(element._focused);
- });
-
- test('tab on suggestion, tabComplete = true', () => {
- element._suggestions = [{name: 'sugar bombs'}];
- element._focused = true;
- // When tabComplete is true, focus.
- element.tabComplete = true;
- focusSpy = sandbox.spy(element, 'focus');
- Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.isHidden);
-
- MockInteractions.pressAndReleaseKeyOn(
- element.$.suggestions.shadowRoot
- .querySelector('li:first-child'), 9, null, 'tab');
- flushAsynchronousOperations();
-
- assert.isTrue(commitSpy.called);
- assert.isTrue(element._focused);
- });
-
- test('tap on suggestion commits, does not call focus', () => {
- focusSpy = sandbox.spy(element, 'focus');
- element._focused = true;
- element._suggestions = [{name: 'first suggestion'}];
- Polymer.dom.flush();
- assert.isFalse(element.$.suggestions.isHidden);
- MockInteractions.tap(element.$.suggestions.shadowRoot
- .querySelector('li:first-child'));
- flushAsynchronousOperations();
-
- assert.isFalse(focusSpy.called);
- assert.isTrue(commitSpy.called);
- assert.isTrue(element.$.suggestions.isHidden);
- });
- });
-
- test('input-keydown event fired', () => {
- const listener = sandbox.spy();
- element.addEventListener('input-keydown', listener);
- MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ element._suggestions = [{name: 'first suggestion'}];
+ flush$0();
+ assert.isFalse(element.$.suggestions.isHidden);
+ MockInteractions.tap(element.$.suggestions.shadowRoot
+ .querySelector('li:first-child'));
flushAsynchronousOperations();
- assert.isTrue(listener.called);
- });
- test('enter with modifier does not complete', () => {
- const handleSpy = sandbox.spy(element, '_handleKeydown');
- const commitStub = sandbox.stub(element, '_handleInputCommit');
- MockInteractions.pressAndReleaseKeyOn(
- element.$.input, 13, 'ctrl', 'enter');
- assert.isTrue(handleSpy.called);
- assert.isFalse(commitStub.called);
- MockInteractions.pressAndReleaseKeyOn(
- element.$.input, 13, null, 'enter');
- assert.isTrue(commitStub.called);
- });
-
- suite('warnUncommitted', () => {
- let inputClassList;
- setup(() => {
- inputClassList = element.$.input.classList;
- });
-
- test('enabled', () => {
- element.warnUncommitted = true;
- element.text = 'blah blah blah';
- MockInteractions.blur(element.$.input);
- assert.isTrue(inputClassList.contains('warnUncommitted'));
- MockInteractions.focus(element.$.input);
- assert.isFalse(inputClassList.contains('warnUncommitted'));
- });
-
- test('disabled', () => {
- element.warnUncommitted = false;
- element.text = 'blah blah blah';
- MockInteractions.blur(element.$.input);
- assert.isFalse(inputClassList.contains('warnUncommitted'));
- });
-
- test('no text', () => {
- element.warnUncommitted = true;
- element.text = '';
- MockInteractions.blur(element.$.input);
- assert.isFalse(inputClassList.contains('warnUncommitted'));
- });
+ assert.isFalse(focusSpy.called);
+ assert.isTrue(commitSpy.called);
+ assert.isTrue(element.$.suggestions.isHidden);
});
});
+
+ test('input-keydown event fired', () => {
+ const listener = sandbox.spy();
+ element.addEventListener('input-keydown', listener);
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
+ flushAsynchronousOperations();
+ assert.isTrue(listener.called);
+ });
+
+ test('enter with modifier does not complete', () => {
+ const handleSpy = sandbox.spy(element, '_handleKeydown');
+ const commitStub = sandbox.stub(element, '_handleInputCommit');
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.input, 13, 'ctrl', 'enter');
+ assert.isTrue(handleSpy.called);
+ assert.isFalse(commitStub.called);
+ MockInteractions.pressAndReleaseKeyOn(
+ element.$.input, 13, null, 'enter');
+ assert.isTrue(commitStub.called);
+ });
+
+ suite('warnUncommitted', () => {
+ let inputClassList;
+ setup(() => {
+ inputClassList = element.$.input.classList;
+ });
+
+ test('enabled', () => {
+ element.warnUncommitted = true;
+ element.text = 'blah blah blah';
+ MockInteractions.blur(element.$.input);
+ assert.isTrue(inputClassList.contains('warnUncommitted'));
+ MockInteractions.focus(element.$.input);
+ assert.isFalse(inputClassList.contains('warnUncommitted'));
+ });
+
+ test('disabled', () => {
+ element.warnUncommitted = false;
+ element.text = 'blah blah blah';
+ MockInteractions.blur(element.$.input);
+ assert.isFalse(inputClassList.contains('warnUncommitted'));
+ });
+
+ test('no text', () => {
+ element.warnUncommitted = true;
+ element.text = '';
+ MockInteractions.blur(element.$.input);
+ assert.isFalse(inputClassList.contains('warnUncommitted'));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index efa97cf..857f1b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.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,89 +14,99 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @extends Polymer.Element
- */
- class GrAvatar extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-avatar'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-js-api-interface/gr-js-api-interface.js';
+import '../gr-rest-api-interface/gr-rest-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-avatar_html.js';
- static get properties() {
- return {
- account: {
- type: Object,
- observer: '_accountChanged',
- },
- imageSize: {
- type: Number,
- value: 16,
- },
- _hasAvatars: {
- type: Boolean,
- value: false,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @extends Polymer.Element
+ */
+class GrAvatar extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- Promise.all([
- this._getConfig(),
- Gerrit.awaitPluginsLoaded(),
- ]).then(([cfg]) => {
- this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+ static get is() { return 'gr-avatar'; }
- this._updateAvatarURL();
- });
- }
+ static get properties() {
+ return {
+ account: {
+ type: Object,
+ observer: '_accountChanged',
+ },
+ imageSize: {
+ type: Number,
+ value: 16,
+ },
+ _hasAvatars: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- _getConfig() {
- return this.$.restAPI.getConfig();
- }
+ /** @override */
+ attached() {
+ super.attached();
+ Promise.all([
+ this._getConfig(),
+ Gerrit.awaitPluginsLoaded(),
+ ]).then(([cfg]) => {
+ this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
- _accountChanged(account) {
this._updateAvatarURL();
+ });
+ }
+
+ _getConfig() {
+ return this.$.restAPI.getConfig();
+ }
+
+ _accountChanged(account) {
+ this._updateAvatarURL();
+ }
+
+ _updateAvatarURL() {
+ if (!this._hasAvatars || !this.account) {
+ this.hidden = true;
+ return;
}
+ this.hidden = false;
- _updateAvatarURL() {
- if (!this._hasAvatars || !this.account) {
- this.hidden = true;
- return;
- }
- this.hidden = false;
-
- const url = this._buildAvatarURL(this.account);
- if (url) {
- this.style.backgroundImage = 'url("' + url + '")';
- }
- }
-
- _getAccounts(account) {
- return account._account_id || account.email || account.username ||
- account.name;
- }
-
- _buildAvatarURL(account) {
- if (!account) { return ''; }
- const avatars = account.avatars || [];
- for (let i = 0; i < avatars.length; i++) {
- if (avatars[i].height === this.imageSize) {
- return avatars[i].url;
- }
- }
- return this.getBaseUrl() + '/accounts/' +
- encodeURIComponent(this._getAccounts(account)) +
- '/avatar?s=' + this.imageSize;
+ const url = this._buildAvatarURL(this.account);
+ if (url) {
+ this.style.backgroundImage = 'url("' + url + '")';
}
}
- customElements.define(GrAvatar.is, GrAvatar);
-})();
+ _getAccounts(account) {
+ return account._account_id || account.email || account.username ||
+ account.name;
+ }
+
+ _buildAvatarURL(account) {
+ if (!account) { return ''; }
+ const avatars = account.avatars || [];
+ for (let i = 0; i < avatars.length; i++) {
+ if (avatars[i].height === this.imageSize) {
+ return avatars[i].url;
+ }
+ }
+ return this.getBaseUrl() + '/accounts/' +
+ encodeURIComponent(this._getAccounts(account)) +
+ '/avatar?s=' + this.imageSize;
+ }
+}
+
+customElements.define(GrAvatar.is, GrAvatar);
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
index 1daffa2..be4c350 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
@@ -1,28 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-avatar">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: inline-block;
@@ -32,6 +26,4 @@
}
</style>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-avatar.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index bd3f805..2cec20e 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_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-avatar</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-avatar.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-avatar.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-avatar.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,14 +40,117 @@
</template>
</test-fixture>
-<script>
- suite('gr-avatar tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-avatar.js';
+suite('gr-avatar tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('methods', () => {
+ assert.equal(
+ element._buildAvatarURL({
+ _account_id: 123,
+ }),
+ '/accounts/123/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ email: 'test@example.com',
+ }),
+ '/accounts/test%40example.com/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ name: 'John Doe',
+ }),
+ '/accounts/John%20Doe/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ username: 'John_Doe',
+ }),
+ '/accounts/John_Doe/avatar?s=16');
+ assert.equal(
+ element._buildAvatarURL({
+ _account_id: 123,
+ avatars: [
+ {
+ url: 'https://cdn.example.com/s12-p/photo.jpg',
+ height: 12,
+ },
+ {
+ url: 'https://cdn.example.com/s16-p/photo.jpg',
+ height: 16,
+ },
+ {
+ url: 'https://cdn.example.com/s100-p/photo.jpg',
+ height: 100,
+ },
+ ],
+ }),
+ 'https://cdn.example.com/s16-p/photo.jpg');
+ assert.equal(
+ element._buildAvatarURL({
+ _account_id: 123,
+ avatars: [
+ {
+ url: 'https://cdn.example.com/s95-p/photo.jpg',
+ height: 95,
+ },
+ ],
+ }),
+ '/accounts/123/avatar?s=16');
+ assert.equal(element._buildAvatarURL(undefined), '');
+ });
+
+ test('dom for existing account', () => {
+ assert.isFalse(element.hasAttribute('hidden'));
+
+ sandbox.stub(
+ element,
+ '_getConfig',
+ () => Promise.resolve({plugin: {has_avatars: true}}));
+
+ element.imageSize = 64;
+ element.account = {
+ _account_id: 123,
+ };
+
+ assert.strictEqual(element.style.backgroundImage, '');
+
+ // Emulate plugins loaded.
+ Gerrit._loadPlugins([]);
+
+ Promise.all([
+ element.$.restAPI.getConfig(),
+ Gerrit.awaitPluginsLoaded(),
+ ]).then(() => {
+ assert.isFalse(element.hasAttribute('hidden'));
+
+ assert.isTrue(
+ element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
+ });
+ });
+
+ suite('plugin has avatars', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
+
+ stub('gr-avatar', {
+ _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}),
+ });
+
element = fixture('basic');
});
@@ -50,110 +158,49 @@
sandbox.restore();
});
- test('methods', () => {
- assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- }),
- '/accounts/123/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- email: 'test@example.com',
- }),
- '/accounts/test%40example.com/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- name: 'John Doe',
- }),
- '/accounts/John%20Doe/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- username: 'John_Doe',
- }),
- '/accounts/John_Doe/avatar?s=16');
- assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- avatars: [
- {
- url: 'https://cdn.example.com/s12-p/photo.jpg',
- height: 12,
- },
- {
- url: 'https://cdn.example.com/s16-p/photo.jpg',
- height: 16,
- },
- {
- url: 'https://cdn.example.com/s100-p/photo.jpg',
- height: 100,
- },
- ],
- }),
- 'https://cdn.example.com/s16-p/photo.jpg');
- assert.equal(
- element._buildAvatarURL({
- _account_id: 123,
- avatars: [
- {
- url: 'https://cdn.example.com/s95-p/photo.jpg',
- height: 95,
- },
- ],
- }),
- '/accounts/123/avatar?s=16');
- assert.equal(element._buildAvatarURL(undefined), '');
- });
-
- test('dom for existing account', () => {
+ test('dom for non available account', () => {
assert.isFalse(element.hasAttribute('hidden'));
- sandbox.stub(
- element,
- '_getConfig',
- () => Promise.resolve({plugin: {has_avatars: true}}));
-
- element.imageSize = 64;
- element.account = {
- _account_id: 123,
- };
-
- assert.strictEqual(element.style.backgroundImage, '');
-
// Emulate plugins loaded.
Gerrit._loadPlugins([]);
- Promise.all([
+ return Promise.all([
element.$.restAPI.getConfig(),
Gerrit.awaitPluginsLoaded(),
]).then(() => {
- assert.isFalse(element.hasAttribute('hidden'));
+ assert.isTrue(element.hasAttribute('hidden'));
- assert.isTrue(
- element.style.backgroundImage.includes('/accounts/123/avatar?s=64'));
+ assert.strictEqual(element.style.backgroundImage, '');
});
});
+ });
- suite('plugin has avatars', () => {
- let element;
- let sandbox;
+ suite('config not set', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-avatar', {
- _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}),
- });
-
- element = fixture('basic');
+ stub('gr-avatar', {
+ _getConfig: () => Promise.resolve({}),
});
- teardown(() => {
- sandbox.restore();
- });
+ element = fixture('basic');
+ });
- test('dom for non available account', () => {
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('avatar hidden when account set', () => {
+ flush(() => {
assert.isFalse(element.hasAttribute('hidden'));
+ element.imageSize = 64;
+ element.account = {
+ _account_id: 123,
+ };
// Emulate plugins loaded.
Gerrit._loadPlugins([]);
@@ -162,49 +209,9 @@
Gerrit.awaitPluginsLoaded(),
]).then(() => {
assert.isTrue(element.hasAttribute('hidden'));
-
- assert.strictEqual(element.style.backgroundImage, '');
- });
- });
- });
-
- suite('config not set', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
-
- stub('gr-avatar', {
- _getConfig: () => Promise.resolve({}),
- });
-
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('avatar hidden when account set', () => {
- flush(() => {
- assert.isFalse(element.hasAttribute('hidden'));
-
- element.imageSize = 64;
- element.account = {
- _account_id: 123,
- };
- // Emulate plugins loaded.
- Gerrit._loadPlugins([]);
-
- return Promise.all([
- element.$.restAPI.getConfig(),
- Gerrit.awaitPluginsLoaded(),
- ]).then(() => {
- assert.isTrue(element.hasAttribute('hidden'));
- });
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 9d96038..cde56df 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -14,120 +14,131 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrButton extends Polymer.mixinBehaviors( [
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-button'; }
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '@polymer/paper-button/paper-button.js';
+import '../../../styles/shared-styles.js';
+import '../../core/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 {htmlTemplate} from './gr-button_html.js';
- static get properties() {
- return {
- tooltip: String,
- downArrow: {
- type: Boolean,
- reflectToAttribute: true,
- },
- link: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- disabled: {
- type: Boolean,
- observer: '_disabledChanged',
- reflectToAttribute: true,
- },
- noUppercase: {
- type: Boolean,
- value: false,
- },
- loading: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
+/**
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrButton extends mixinBehaviors( [
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- _disabled: {
- type: Boolean,
- computed: '_computeDisabled(disabled, loading)',
- },
+ static get is() { return 'gr-button'; }
- _initialTabindex: {
- type: String,
- value: '0',
- },
- };
- }
+ static get properties() {
+ return {
+ tooltip: String,
+ downArrow: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ link: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ disabled: {
+ type: Boolean,
+ observer: '_disabledChanged',
+ reflectToAttribute: true,
+ },
+ noUppercase: {
+ type: Boolean,
+ value: false,
+ },
+ loading: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
- /** @override */
- created() {
- super.created();
- this._initialTabindex = this.getAttribute('tabindex') || '0';
- this.addEventListener('click', e => this._handleAction(e));
- this.addEventListener('keydown',
- e => this._handleKeydown(e));
- }
+ _disabled: {
+ type: Boolean,
+ computed: '_computeDisabled(disabled, loading)',
+ },
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'button');
- this._ensureAttribute('tabindex', '0');
- }
-
- _handleAction(e) {
- if (this._disabled) {
- e.preventDefault();
- e.stopPropagation();
- e.stopImmediatePropagation();
- return;
- }
-
- let el = this.root;
- let path = '';
- while (el = el.parentNode || el.host) {
- if (el.tagName && el.tagName.startsWith('GR-APP')) {
- break;
- }
- if (el.tagName) {
- const idString = el.id ? '#' + el.id : '';
- path = el.tagName + idString + ' ' + path;
- }
- }
- this.$.reporting.reportInteraction('button-click',
- {path: path.trim().toLowerCase()});
- }
-
- _disabledChanged(disabled) {
- this.setAttribute('tabindex', disabled ? '-1' : this._initialTabindex);
- this.updateStyles();
- }
-
- _computeDisabled(disabled, loading) {
- return disabled || loading;
- }
-
- _handleKeydown(e) {
- if (this.modifierPressed(e)) { return; }
- e = this.getKeyboardEvent(e);
- // Handle `enter`, `space`.
- if (e.keyCode === 13 || e.keyCode === 32) {
- e.preventDefault();
- e.stopPropagation();
- this.click();
- }
- }
+ _initialTabindex: {
+ type: String,
+ value: '0',
+ },
+ };
}
- customElements.define(GrButton.is, GrButton);
-})();
+ /** @override */
+ created() {
+ super.created();
+ this._initialTabindex = this.getAttribute('tabindex') || '0';
+ this.addEventListener('click', e => this._handleAction(e));
+ this.addEventListener('keydown',
+ e => this._handleKeydown(e));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'button');
+ this._ensureAttribute('tabindex', '0');
+ }
+
+ _handleAction(e) {
+ if (this._disabled) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ return;
+ }
+
+ let el = this.root;
+ let path = '';
+ while (el = el.parentNode || el.host) {
+ if (el.tagName && el.tagName.startsWith('GR-APP')) {
+ break;
+ }
+ if (el.tagName) {
+ const idString = el.id ? '#' + el.id : '';
+ path = el.tagName + idString + ' ' + path;
+ }
+ }
+ this.$.reporting.reportInteraction('button-click',
+ {path: path.trim().toLowerCase()});
+ }
+
+ _disabledChanged(disabled) {
+ this.setAttribute('tabindex', disabled ? '-1' : this._initialTabindex);
+ this.updateStyles();
+ }
+
+ _computeDisabled(disabled, loading) {
+ return disabled || loading;
+ }
+
+ _handleKeydown(e) {
+ if (this.modifierPressed(e)) { return; }
+ e = this.getKeyboardEvent(e);
+ // Handle `enter`, `space`.
+ if (e.keyCode === 13 || e.keyCode === 32) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.click();
+ }
+ }
+}
+
+customElements.define(GrButton.is, GrButton);
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
index b94359f..ad5c00d 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
@@ -1,30 +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/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/paper-button/paper-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-
-<dom-module id="gr-button">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* general styles for all buttons */
:host {
@@ -172,10 +164,7 @@
border-top-color: var(--deemphasized-text-color);
}
</style>
- <paper-button
- raised="[[!link]]"
- disabled="[[_disabled]]"
- tabindex="-1">
+ <paper-button raised="[[!link]]" disabled="[[_disabled]]" tabindex="-1">
<template is="dom-if" if="[[loading]]">
<span class="loadingSpin"></span>
</template>
@@ -183,6 +172,4 @@
<i class="downArrow"></i>
</paper-button>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-button.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index cfac37f..42f9a5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_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-button</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-button.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-button.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-button.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -41,146 +46,149 @@
</template>
</test-fixture>
-<script>
- suite('gr-button 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-button.js';
+import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+suite('gr-button tests', () => {
+ let element;
+ let sandbox;
- const addSpyOn = function(eventName) {
- const spy = sandbox.spy();
- if (eventName == 'tap') {
- Polymer.Gestures.addListener(element, eventName, spy);
- } else {
- element.addEventListener(eventName, spy);
- }
- return spy;
- };
+ const addSpyOn = function(eventName) {
+ const spy = sandbox.spy();
+ if (eventName == 'tap') {
+ addListener(element, eventName, spy);
+ } else {
+ element.addEventListener(eventName, spy);
+ }
+ return spy;
+ };
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('disabled is set by disabled', () => {
+ const paperBtn = element.shadowRoot.querySelector('paper-button');
+ assert.isFalse(paperBtn.disabled);
+ element.disabled = true;
+ assert.isTrue(paperBtn.disabled);
+ element.disabled = false;
+ assert.isFalse(paperBtn.disabled);
+ });
+
+ test('loading set from listener', done => {
+ let resolve;
+ element.addEventListener('click', e => {
+ e.target.loading = true;
+ resolve = () => e.target.loading = false;
+ });
+ const paperBtn = element.shadowRoot.querySelector('paper-button');
+ assert.isFalse(paperBtn.disabled);
+ MockInteractions.tap(element);
+ assert.isTrue(paperBtn.disabled);
+ assert.isTrue(element.hasAttribute('loading'));
+ resolve();
+ flush(() => {
+ assert.isFalse(paperBtn.disabled);
+ assert.isFalse(element.hasAttribute('loading'));
+ done();
+ });
+ });
+
+ test('tabindex should be -1 if disabled', () => {
+ element.disabled = true;
+ assert.isTrue(element.getAttribute('tabindex') === '-1');
+ });
+
+ // Regression tests for Issue: 11969
+ test('tabindex should be reset to 0 if enabled', () => {
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '0');
+ element.disabled = true;
+ assert.equal(element.getAttribute('tabindex'), '-1');
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '0');
+ });
+
+ test('tabindex should be preserved', () => {
+ element = fixture('tabindex');
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '3');
+ element.disabled = true;
+ assert.equal(element.getAttribute('tabindex'), '-1');
+ element.disabled = false;
+ assert.equal(element.getAttribute('tabindex'), '3');
+ });
+
+ // 'tap' event is tested so we don't loose backward compatibility with older
+ // plugins who didn't move to on-click which is faster and well supported.
+ test('dispatches click event', () => {
+ const spy = addSpyOn('click');
+ MockInteractions.click(element);
+ assert.isTrue(spy.calledOnce);
+ });
+
+ test('dispatches tap event', () => {
+ const spy = addSpyOn('tap');
+ MockInteractions.tap(element);
+ assert.isTrue(spy.calledOnce);
+ });
+
+ test('dispatches click from tap event', () => {
+ const spy = addSpyOn('click');
+ MockInteractions.tap(element);
+ assert.isTrue(spy.calledOnce);
+ });
+
+ // Keycodes: 32 for Space, 13 for Enter.
+ for (const key of [32, 13]) {
+ test('dispatches click event on keycode ' + key, () => {
+ const tapSpy = sandbox.spy();
+ element.addEventListener('click', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key);
+ assert.isTrue(tapSpy.calledOnce);
+ });
+
+ test('dispatches no click event with modifier on keycode ' + key, () => {
+ const tapSpy = sandbox.spy();
+ element.addEventListener('click', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'shift');
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl');
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'meta');
+ MockInteractions.pressAndReleaseKeyOn(element, key, 'alt');
+ assert.isFalse(tapSpy.calledOnce);
+ });
+ }
+
+ suite('disabled', () => {
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('disabled is set by disabled', () => {
- const paperBtn = element.shadowRoot.querySelector('paper-button');
- assert.isFalse(paperBtn.disabled);
element.disabled = true;
- assert.isTrue(paperBtn.disabled);
- element.disabled = false;
- assert.isFalse(paperBtn.disabled);
});
- test('loading set from listener', done => {
- let resolve;
- element.addEventListener('click', e => {
- e.target.loading = true;
- resolve = () => e.target.loading = false;
- });
- const paperBtn = element.shadowRoot.querySelector('paper-button');
- assert.isFalse(paperBtn.disabled);
- MockInteractions.tap(element);
- assert.isTrue(paperBtn.disabled);
- assert.isTrue(element.hasAttribute('loading'));
- resolve();
- flush(() => {
- assert.isFalse(paperBtn.disabled);
- assert.isFalse(element.hasAttribute('loading'));
- done();
- });
- });
-
- test('tabindex should be -1 if disabled', () => {
- element.disabled = true;
- assert.isTrue(element.getAttribute('tabindex') === '-1');
- });
-
- // Regression tests for Issue: 11969
- test('tabindex should be reset to 0 if enabled', () => {
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '0');
- element.disabled = true;
- assert.equal(element.getAttribute('tabindex'), '-1');
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '0');
- });
-
- test('tabindex should be preserved', () => {
- element = fixture('tabindex');
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '3');
- element.disabled = true;
- assert.equal(element.getAttribute('tabindex'), '-1');
- element.disabled = false;
- assert.equal(element.getAttribute('tabindex'), '3');
- });
-
- // 'tap' event is tested so we don't loose backward compatibility with older
- // plugins who didn't move to on-click which is faster and well supported.
- test('dispatches click event', () => {
- const spy = addSpyOn('click');
- MockInteractions.click(element);
- assert.isTrue(spy.calledOnce);
- });
-
- test('dispatches tap event', () => {
- const spy = addSpyOn('tap');
- MockInteractions.tap(element);
- assert.isTrue(spy.calledOnce);
- });
-
- test('dispatches click from tap event', () => {
- const spy = addSpyOn('click');
- MockInteractions.tap(element);
- assert.isTrue(spy.calledOnce);
- });
-
- // Keycodes: 32 for Space, 13 for Enter.
- for (const key of [32, 13]) {
- test('dispatches click event on keycode ' + key, () => {
- const tapSpy = sandbox.spy();
- element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key);
- assert.isTrue(tapSpy.calledOnce);
- });
-
- test('dispatches no click event with modifier on keycode ' + key, () => {
- const tapSpy = sandbox.spy();
- element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key, 'shift');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'meta');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'alt');
- assert.isFalse(tapSpy.calledOnce);
+ for (const eventName of ['tap', 'click']) {
+ test('stops ' + eventName + ' event', () => {
+ const spy = addSpyOn(eventName);
+ MockInteractions.tap(element);
+ assert.isFalse(spy.called);
});
}
- suite('disabled', () => {
- setup(() => {
- element.disabled = true;
+ // Keycodes: 32 for Space, 13 for Enter.
+ for (const key of [32, 13]) {
+ test('stops click event on keycode ' + key, () => {
+ const tapSpy = sandbox.spy();
+ element.addEventListener('click', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key);
+ assert.isFalse(tapSpy.called);
});
-
- for (const eventName of ['tap', 'click']) {
- test('stops ' + eventName + ' event', () => {
- const spy = addSpyOn(eventName);
- MockInteractions.tap(element);
- assert.isFalse(spy.called);
- });
- }
-
- // Keycodes: 32 for Space, 13 for Enter.
- for (const key of [32, 13]) {
- test('stops click event on keycode ' + key, () => {
- const tapSpy = sandbox.spy();
- element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key);
- assert.isFalse(tapSpy.called);
- });
- }
- });
+ }
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 001632f..10e06dd 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.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,49 +14,56 @@
* 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 GrChangeStar extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-change-star'; }
- /**
- * Fired when star state is toggled.
- *
- * @event toggle-star
- */
+import '../gr-icons/gr-icons.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-change-star_html.js';
- static get properties() {
- return {
- /** @type {?} */
- change: {
- type: Object,
- notify: true,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrChangeStar extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _computeStarClass(starred) {
- return starred ? 'active' : '';
- }
+ static get is() { return 'gr-change-star'; }
+ /**
+ * Fired when star state is toggled.
+ *
+ * @event toggle-star
+ */
- _computeStarIcon(starred) {
- // Hollow star is used to indicate inactive state.
- return `gr-icons:star${starred ? '' : '-border'}`;
- }
-
- toggleStar() {
- const newVal = !this.change.starred;
- this.set('change.starred', newVal);
- this.dispatchEvent(new CustomEvent('toggle-star', {
- bubbles: true,
- composed: true,
- detail: {change: this.change, starred: newVal},
- }));
- }
+ static get properties() {
+ return {
+ /** @type {?} */
+ change: {
+ type: Object,
+ notify: true,
+ },
+ };
}
- customElements.define(GrChangeStar.is, GrChangeStar);
-})();
+ _computeStarClass(starred) {
+ return starred ? 'active' : '';
+ }
+
+ _computeStarIcon(starred) {
+ // Hollow star is used to indicate inactive state.
+ return `gr-icons:star${starred ? '' : '-border'}`;
+ }
+
+ toggleStar() {
+ const newVal = !this.change.starred;
+ this.set('change.starred', newVal);
+ this.dispatchEvent(new CustomEvent('toggle-star', {
+ bubbles: true,
+ composed: true,
+ detail: {change: this.change, starred: newVal},
+ }));
+ }
+}
+
+customElements.define(GrChangeStar.is, GrChangeStar);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
index dc8ba34..a4925aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
@@ -1,26 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-star">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
button {
background-color: transparent;
@@ -36,10 +32,6 @@
}
</style>
<button aria-label="Change star" on-click="toggleStar">
- <iron-icon
- class$="[[_computeStarClass(change.starred)]]"
- icon$="[[_computeStarIcon(change.starred)]]"></iron-icon>
+ <iron-icon class\$="[[_computeStarClass(change.starred)]]" icon\$="[[_computeStarIcon(change.starred)]]"></iron-icon>
</button>
- </template>
- <script src="gr-change-star.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index b76ce4d..aa138d3 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_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-change-star</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-change-star.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-change-star.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-star.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,51 +40,53 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-star tests', async () => {
- await readyToTest();
- let element;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-star.js';
+suite('gr-change-star tests', () => {
+ let element;
- setup(() => {
- element = fixture('basic');
- element.change = {
- _number: 2,
- starred: true,
- };
- });
-
- test('star visibility states', () => {
- element.set('change.starred', true);
- let icon = element.shadowRoot
- .querySelector('iron-icon');
- assert.isTrue(icon.classList.contains('active'));
- assert.equal(icon.icon, 'gr-icons:star');
-
- element.set('change.starred', false);
- icon = element.shadowRoot
- .querySelector('iron-icon');
- assert.isFalse(icon.classList.contains('active'));
- assert.equal(icon.icon, 'gr-icons:star-border');
- });
-
- test('starring', done => {
- element.addEventListener('toggle-star', () => {
- assert.equal(element.change.starred, true);
- done();
- });
- element.set('change.starred', false);
- MockInteractions.tap(element.shadowRoot
- .querySelector('button'));
- });
-
- test('unstarring', done => {
- element.addEventListener('toggle-star', () => {
- assert.equal(element.change.starred, false);
- done();
- });
- element.set('change.starred', true);
- MockInteractions.tap(element.shadowRoot
- .querySelector('button'));
- });
+ setup(() => {
+ element = fixture('basic');
+ element.change = {
+ _number: 2,
+ starred: true,
+ };
});
+
+ test('star visibility states', () => {
+ element.set('change.starred', true);
+ let icon = element.shadowRoot
+ .querySelector('iron-icon');
+ assert.isTrue(icon.classList.contains('active'));
+ assert.equal(icon.icon, 'gr-icons:star');
+
+ element.set('change.starred', false);
+ icon = element.shadowRoot
+ .querySelector('iron-icon');
+ assert.isFalse(icon.classList.contains('active'));
+ assert.equal(icon.icon, 'gr-icons:star-border');
+ });
+
+ test('starring', done => {
+ element.addEventListener('toggle-star', () => {
+ assert.equal(element.change.starred, true);
+ done();
+ });
+ element.set('change.starred', false);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('button'));
+ });
+
+ test('unstarring', done => {
+ element.addEventListener('toggle-star', () => {
+ assert.equal(element.change.starred, false);
+ done();
+ });
+ element.set('change.starred', true);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('button'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 7052a6a..b99612e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -14,85 +14,93 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const ChangeStates = {
- MERGED: 'Merged',
- ABANDONED: 'Abandoned',
- MERGE_CONFLICT: 'Merge Conflict',
- WIP: 'WIP',
- PRIVATE: 'Private',
- };
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-tooltip-content/gr-tooltip-content.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-change-status_html.js';
- const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
- 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
- 'and email notifications will be silenced until the review is started.';
+const ChangeStates = {
+ MERGED: 'Merged',
+ ABANDONED: 'Abandoned',
+ MERGE_CONFLICT: 'Merge Conflict',
+ WIP: 'WIP',
+ PRIVATE: 'Private',
+};
- const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
- 'Download the patch and run "git rebase master". ' +
- 'Upload a new patchset after resolving all merge conflicts.';
+const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
+ 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+ 'and email notifications will be silenced until the review is started.';
- const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
- 'current reviewers (or anyone with "View Private Changes" permission).';
+const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
+ 'Download the patch and run "git rebase master". ' +
+ 'Upload a new patchset after resolving all merge conflicts.';
- /** @extends Polymer.Element */
- class GrChangeStatus extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-change-status'; }
+const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
+ 'current reviewers (or anyone with "View Private Changes" permission).';
- static get properties() {
- return {
- flat: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- status: {
- type: String,
- observer: '_updateChipDetails',
- },
- tooltipText: {
- type: String,
- value: '',
- },
- };
- }
+/** @extends Polymer.Element */
+class GrChangeStatus extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _computeStatusString(status) {
- if (status === ChangeStates.WIP && !this.flat) {
- return 'Work in Progress';
- }
- return status;
- }
+ static get is() { return 'gr-change-status'; }
- _toClassName(str) {
- return str.toLowerCase().replace(/\s/g, '-');
- }
-
- _updateChipDetails(status, previousStatus) {
- if (previousStatus) {
- this.classList.remove(this._toClassName(previousStatus));
- }
- this.classList.add(this._toClassName(status));
-
- switch (status) {
- case ChangeStates.WIP:
- this.tooltipText = WIP_TOOLTIP;
- break;
- case ChangeStates.PRIVATE:
- this.tooltipText = PRIVATE_TOOLTIP;
- break;
- case ChangeStates.MERGE_CONFLICT:
- this.tooltipText = MERGE_CONFLICT_TOOLTIP;
- break;
- default:
- this.tooltipText = '';
- break;
- }
- }
+ static get properties() {
+ return {
+ flat: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ status: {
+ type: String,
+ observer: '_updateChipDetails',
+ },
+ tooltipText: {
+ type: String,
+ value: '',
+ },
+ };
}
- customElements.define(GrChangeStatus.is, GrChangeStatus);
-})();
+ _computeStatusString(status) {
+ if (status === ChangeStates.WIP && !this.flat) {
+ return 'Work in Progress';
+ }
+ return status;
+ }
+
+ _toClassName(str) {
+ return str.toLowerCase().replace(/\s/g, '-');
+ }
+
+ _updateChipDetails(status, previousStatus) {
+ if (previousStatus) {
+ this.classList.remove(this._toClassName(previousStatus));
+ }
+ this.classList.add(this._toClassName(status));
+
+ switch (status) {
+ case ChangeStates.WIP:
+ this.tooltipText = WIP_TOOLTIP;
+ break;
+ case ChangeStates.PRIVATE:
+ this.tooltipText = PRIVATE_TOOLTIP;
+ break;
+ case ChangeStates.MERGE_CONFLICT:
+ this.tooltipText = MERGE_CONFLICT_TOOLTIP;
+ break;
+ default:
+ this.tooltipText = '';
+ break;
+ }
+ }
+}
+
+customElements.define(GrChangeStatus.is, GrChangeStatus);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
index eaca593..1a1bc1b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
@@ -1,28 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-change-status">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.chip {
border-radius: var(--border-radius);
@@ -70,17 +64,9 @@
color: white;
}
</style>
- <gr-tooltip-content
- has-tooltip
- position-below
- title="[[tooltipText]]"
- max-width="40em">
- <div
- class="chip"
- aria-label$="Label: [[status]]">
+ <gr-tooltip-content has-tooltip="" position-below="" title="[[tooltipText]]" max-width="40em">
+ <div class="chip" aria-label\$="Label: [[status]]">
[[_computeStatusString(status)]]
</div>
</gr-tooltip-content>
- </template>
- <script src="gr-change-status.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
index d78cc3a..819411f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_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-change-status</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-change-status.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-change-status.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-status.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,106 +40,108 @@
</template>
</test-fixture>
-<script>
- const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
- 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
- 'and email notifications will be silenced until the review is started.';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-change-status.js';
+const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
+ 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+ 'and email notifications will be silenced until the review is started.';
- const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
- 'Download the patch and run "git rebase master". ' +
- 'Upload a new patchset after resolving all merge conflicts.';
+const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' +
+ 'Download the patch and run "git rebase master". ' +
+ 'Upload a new patchset after resolving all merge conflicts.';
- const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
- 'current reviewers (or anyone with "View Private Changes" permission).';
+const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
+ 'current reviewers (or anyone with "View Private Changes" permission).';
- suite('gr-change-status tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+suite('gr-change-status tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('WIP', () => {
- element.status = 'WIP';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, 'Work in Progress');
- assert.equal(element.tooltipText, WIP_TOOLTIP);
- assert.isTrue(element.classList.contains('wip'));
- });
-
- test('WIP flat', () => {
- element.flat = true;
- element.status = 'WIP';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, 'WIP');
- assert.isDefined(element.tooltipText);
- assert.isTrue(element.classList.contains('wip'));
- assert.isTrue(element.hasAttribute('flat'));
- });
-
- test('merged', () => {
- element.status = 'Merged';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('merged'));
- });
-
- test('abandoned', () => {
- element.status = 'Abandoned';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('abandoned'));
- });
-
- test('merge conflict', () => {
- element.status = 'Merge Conflict';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
- assert.isTrue(element.classList.contains('merge-conflict'));
- });
-
- test('private', () => {
- element.status = 'Private';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
- assert.isTrue(element.classList.contains('private'));
- });
-
- test('active', () => {
- element.status = 'Active';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('active'));
- });
-
- test('ready to submit', () => {
- element.status = 'Ready to submit';
- assert.equal(element.shadowRoot
- .querySelector('.chip').innerText, element.status);
- assert.equal(element.tooltipText, '');
- assert.isTrue(element.classList.contains('ready-to-submit'));
- });
-
- test('updating status removes the previous class', () => {
- element.status = 'Private';
- assert.isTrue(element.classList.contains('private'));
- assert.isFalse(element.classList.contains('wip'));
-
- element.status = 'WIP';
- assert.isFalse(element.classList.contains('private'));
- assert.isTrue(element.classList.contains('wip'));
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('WIP', () => {
+ element.status = 'WIP';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, 'Work in Progress');
+ assert.equal(element.tooltipText, WIP_TOOLTIP);
+ assert.isTrue(element.classList.contains('wip'));
+ });
+
+ test('WIP flat', () => {
+ element.flat = true;
+ element.status = 'WIP';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, 'WIP');
+ assert.isDefined(element.tooltipText);
+ assert.isTrue(element.classList.contains('wip'));
+ assert.isTrue(element.hasAttribute('flat'));
+ });
+
+ test('merged', () => {
+ element.status = 'Merged';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('merged'));
+ });
+
+ test('abandoned', () => {
+ element.status = 'Abandoned';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('abandoned'));
+ });
+
+ test('merge conflict', () => {
+ element.status = 'Merge Conflict';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP);
+ assert.isTrue(element.classList.contains('merge-conflict'));
+ });
+
+ test('private', () => {
+ element.status = 'Private';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, PRIVATE_TOOLTIP);
+ assert.isTrue(element.classList.contains('private'));
+ });
+
+ test('active', () => {
+ element.status = 'Active';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('active'));
+ });
+
+ test('ready to submit', () => {
+ element.status = 'Ready to submit';
+ assert.equal(element.shadowRoot
+ .querySelector('.chip').innerText, element.status);
+ assert.equal(element.tooltipText, '');
+ assert.isTrue(element.classList.contains('ready-to-submit'));
+ });
+
+ test('updating status removes the previous class', () => {
+ element.status = 'Private';
+ assert.isTrue(element.classList.contains('private'));
+ assert.isFalse(element.classList.contains('wip'));
+
+ element.status = 'WIP';
+ assert.isFalse(element.classList.contains('private'));
+ assert.isTrue(element.classList.contains('wip'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index d8a56f8..765c5cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.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,517 +14,532 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const UNRESOLVED_EXPAND_COUNT = 5;
- const NEWLINE_PATTERN = /\n/g;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-storage/gr-storage.js';
+import '../gr-comment/gr-comment.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-comment-thread_html.js';
+
+const UNRESOLVED_EXPAND_COUNT = 5;
+const NEWLINE_PATTERN = /\n/g;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @extends Polymer.Element
+ */
+class GrCommentThread extends mixinBehaviors( [
+ /**
+ * Not used in this element rather other elements tests
+ */
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+ Gerrit.PathListBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-comment-thread'; }
+ /**
+ * Fired when the thread should be discarded.
+ *
+ * @event thread-discard
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @appliesMixin Gerrit.PathListMixin
- * @extends Polymer.Element
+ * Fired when a comment in the thread is permanently modified.
+ *
+ * @event thread-changed
*/
- class GrCommentThread extends Polymer.mixinBehaviors( [
- /**
- * Not used in this element rather other elements tests
- */
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- Gerrit.PathListBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment-thread'; }
- /**
- * Fired when the thread should be discarded.
- *
- * @event thread-discard
- */
- /**
- * Fired when a comment in the thread is permanently modified.
- *
- * @event thread-changed
- */
+ /**
+ * gr-comment-thread exposes the following attributes that allow a
+ * diff widget like gr-diff to show the thread in the right location:
+ *
+ * line-num:
+ * 1-based line number or undefined if it refers to the entire file.
+ *
+ * comment-side:
+ * "left" or "right". These indicate which of the two diffed versions
+ * the comment relates to. In the case of unified diff, the left
+ * version is the one whose line number column is further to the left.
+ *
+ * range:
+ * The range of text that the comment refers to (start_line,
+ * start_character, end_line, end_character), serialized as JSON. If
+ * set, range's end_line will have the same value as line-num. Line
+ * numbers are 1-based, char numbers are 0-based. The start position
+ * (start_line, start_character) is inclusive, and the end position
+ * (end_line, end_character) is exclusive.
+ */
+ static get properties() {
+ return {
+ changeNum: String,
+ comments: {
+ type: Array,
+ value() { return []; },
+ },
+ /**
+ * @type {?{start_line: number, start_character: number, end_line: number,
+ * end_character: number}}
+ */
+ range: {
+ type: Object,
+ reflectToAttribute: true,
+ },
+ keyEventTarget: {
+ type: Object,
+ value() { return document.body; },
+ },
+ commentSide: {
+ type: String,
+ reflectToAttribute: true,
+ },
+ patchNum: String,
+ path: String,
+ projectName: {
+ type: String,
+ observer: '_projectNameChanged',
+ },
+ hasDraft: {
+ type: Boolean,
+ notify: true,
+ reflectToAttribute: true,
+ },
+ isOnParent: {
+ type: Boolean,
+ value: false,
+ },
+ parentIndex: {
+ type: Number,
+ value: null,
+ },
+ rootId: {
+ type: String,
+ notify: true,
+ computed: '_computeRootId(comments.*)',
+ },
+ /**
+ * If this is true, the comment thread also needs to have the change and
+ * line properties property set
+ */
+ showFilePath: {
+ type: Boolean,
+ value: false,
+ },
+ /** Necessary only if showFilePath is true or when used with gr-diff */
+ lineNum: {
+ type: Number,
+ reflectToAttribute: true,
+ },
+ unresolved: {
+ type: Boolean,
+ notify: true,
+ reflectToAttribute: true,
+ },
+ _showActions: Boolean,
+ _lastComment: Object,
+ _orderedComments: Array,
+ _projectConfig: Object,
+ isRobotComment: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ };
+ }
- /**
- * gr-comment-thread exposes the following attributes that allow a
- * diff widget like gr-diff to show the thread in the right location:
- *
- * line-num:
- * 1-based line number or undefined if it refers to the entire file.
- *
- * comment-side:
- * "left" or "right". These indicate which of the two diffed versions
- * the comment relates to. In the case of unified diff, the left
- * version is the one whose line number column is further to the left.
- *
- * range:
- * The range of text that the comment refers to (start_line,
- * start_character, end_line, end_character), serialized as JSON. If
- * set, range's end_line will have the same value as line-num. Line
- * numbers are 1-based, char numbers are 0-based. The start position
- * (start_line, start_character) is inclusive, and the end position
- * (end_line, end_character) is exclusive.
- */
- static get properties() {
- return {
- changeNum: String,
- comments: {
- type: Array,
- value() { return []; },
- },
- /**
- * @type {?{start_line: number, start_character: number, end_line: number,
- * end_character: number}}
- */
- range: {
- type: Object,
- reflectToAttribute: true,
- },
- keyEventTarget: {
- type: Object,
- value() { return document.body; },
- },
- commentSide: {
- type: String,
- reflectToAttribute: true,
- },
- patchNum: String,
- path: String,
- projectName: {
- type: String,
- observer: '_projectNameChanged',
- },
- hasDraft: {
- type: Boolean,
- notify: true,
- reflectToAttribute: true,
- },
- isOnParent: {
- type: Boolean,
- value: false,
- },
- parentIndex: {
- type: Number,
- value: null,
- },
- rootId: {
- type: String,
- notify: true,
- computed: '_computeRootId(comments.*)',
- },
- /**
- * If this is true, the comment thread also needs to have the change and
- * line properties property set
- */
- showFilePath: {
- type: Boolean,
- value: false,
- },
- /** Necessary only if showFilePath is true or when used with gr-diff */
- lineNum: {
- type: Number,
- reflectToAttribute: true,
- },
- unresolved: {
- type: Boolean,
- notify: true,
- reflectToAttribute: true,
- },
- _showActions: Boolean,
- _lastComment: Object,
- _orderedComments: Array,
- _projectConfig: Object,
- isRobotComment: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- };
- }
+ static get observers() {
+ return [
+ '_commentsChanged(comments.*)',
+ ];
+ }
- static get observers() {
- return [
- '_commentsChanged(comments.*)',
- ];
- }
+ get keyBindings() {
+ return {
+ 'e shift+e': '_handleEKey',
+ };
+ }
- get keyBindings() {
- return {
- 'e shift+e': '_handleEKey',
- };
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('comment-update',
+ e => this._handleCommentUpdate(e));
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('comment-update',
- e => this._handleCommentUpdate(e));
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._showActions = loggedIn;
+ });
+ this._setInitialExpandedState();
+ }
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._showActions = loggedIn;
- });
- this._setInitialExpandedState();
- }
+ addOrEditDraft(opt_lineNum, opt_range) {
+ const lastComment = this.comments[this.comments.length - 1] || {};
+ if (lastComment.__draft) {
+ const commentEl = this._commentElWithDraftID(
+ lastComment.id || lastComment.__draftID);
+ commentEl.editing = true;
- addOrEditDraft(opt_lineNum, opt_range) {
- const lastComment = this.comments[this.comments.length - 1] || {};
- if (lastComment.__draft) {
- const commentEl = this._commentElWithDraftID(
- lastComment.id || lastComment.__draftID);
- commentEl.editing = true;
-
- // If the comment was collapsed, re-open it to make it clear which
- // actions are available.
- commentEl.collapsed = false;
- } else {
- const range = opt_range ? opt_range :
- lastComment ? lastComment.range : undefined;
- const unresolved = lastComment ? lastComment.unresolved : undefined;
- this.addDraft(opt_lineNum, range, unresolved);
- }
- }
-
- addDraft(opt_lineNum, opt_range, opt_unresolved) {
- const draft = this._newDraft(opt_lineNum, opt_range);
- draft.__editing = true;
- draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
- this.push('comments', draft);
- }
-
- fireRemoveSelf() {
- this.dispatchEvent(new CustomEvent('thread-discard',
- {detail: {rootId: this.rootId}, bubbles: false}));
- }
-
- _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
- return Gerrit.Nav.getUrlForDiffById(changeNum,
- projectName, path, patchNum,
- null, this.lineNum);
- }
-
- _computeDisplayPath(path) {
- const lineString = this.lineNum ? `#${this.lineNum}` : '';
- return this.computeDisplayPath(path) + lineString;
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _commentsChanged() {
- this._orderedComments = this._sortedComments(this.comments);
- this.updateThreadProperties();
- }
-
- updateThreadProperties() {
- if (this._orderedComments.length) {
- this._lastComment = this._getLastComment();
- this.unresolved = this._lastComment.unresolved;
- this.hasDraft = this._lastComment.__draft;
- this.isRobotComment = !!(this._lastComment.robot_id);
- }
- }
-
- _shouldDisableAction(_showActions, _lastComment) {
- return !_showActions || !_lastComment || !!_lastComment.__draft;
- }
-
- _hideActions(_showActions, _lastComment) {
- return this._shouldDisableAction(_showActions, _lastComment) ||
- !!_lastComment.robot_id;
- }
-
- _getLastComment() {
- return this._orderedComments[this._orderedComments.length - 1] || {};
- }
-
- _handleEKey(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
- // Don’t preventDefault in this case because it will render the event
- // useless for other handlers (other gr-comment-thread elements).
- if (e.detail.keyboardEvent.shiftKey) {
- this._expandCollapseComments(true);
- } else {
- if (this.modifierPressed(e)) { return; }
- this._expandCollapseComments(false);
- }
- }
-
- _expandCollapseComments(actionIsCollapse) {
- const comments =
- Polymer.dom(this.root).querySelectorAll('gr-comment');
- for (const comment of comments) {
- comment.collapsed = actionIsCollapse;
- }
- }
-
- /**
- * Sets the initial state of the comment thread.
- * Expands the thread if one of the following is true:
- * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
- * thread is unresolved,
- * - it's a robot comment.
- */
- _setInitialExpandedState() {
- if (this._orderedComments) {
- for (let i = 0; i < this._orderedComments.length; i++) {
- const comment = this._orderedComments[i];
- const isRobotComment = !!comment.robot_id;
- // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
- const resolvedThread = !this.unresolved ||
- this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
- comment.collapsed = !isRobotComment && resolvedThread;
- }
- }
- }
-
- _sortedComments(comments) {
- return comments.slice().sort((c1, c2) => {
- const c1Date = c1.__date || util.parseDate(c1.updated);
- const c2Date = c2.__date || util.parseDate(c2.updated);
- const dateCompare = c1Date - c2Date;
- // Ensure drafts are at the end. There should only be one but in edge
- // cases could be more. In the unlikely event two drafts are being
- // compared, use the typical date compare.
- if (c2.__draft && !c1.__draft ) { return -1; }
- if (c1.__draft && !c2.__draft ) { return 1; }
- if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; }
- // If same date, fall back to sorting by id.
- return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
- });
- }
-
- _createReplyComment(parent, content, opt_isEditing,
- opt_unresolved) {
- this.$.reporting.recordDraftInteraction();
- const reply = this._newReply(
- this._orderedComments[this._orderedComments.length - 1].id,
- parent.line,
- content,
- opt_unresolved,
- parent.range);
-
- // If there is currently a comment in an editing state, add an attribute
- // so that the gr-comment knows not to populate the draft text.
- for (let i = 0; i < this.comments.length; i++) {
- if (this.comments[i].__editing) {
- reply.__otherEditing = true;
- break;
- }
- }
-
- if (opt_isEditing) {
- reply.__editing = true;
- }
-
- this.push('comments', reply);
-
- if (!opt_isEditing) {
- // Allow the reply to render in the dom-repeat.
- this.async(() => {
- const commentEl = this._commentElWithDraftID(reply.__draftID);
- commentEl.save();
- }, 1);
- }
- }
-
- _isDraft(comment) {
- return !!comment.__draft;
- }
-
- /**
- * @param {boolean=} opt_quote
- */
- _processCommentReply(opt_quote) {
- const comment = this._lastComment;
- let quoteStr;
- if (opt_quote) {
- const msg = comment.message;
- quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
- }
- this._createReplyComment(comment, quoteStr, true, comment.unresolved);
- }
-
- _handleCommentReply(e) {
- this._processCommentReply();
- }
-
- _handleCommentQuote(e) {
- this._processCommentReply(true);
- }
-
- _handleCommentAck(e) {
- const comment = this._lastComment;
- this._createReplyComment(comment, 'Ack', false, false);
- }
-
- _handleCommentDone(e) {
- const comment = this._lastComment;
- this._createReplyComment(comment, 'Done', false, false);
- }
-
- _handleCommentFix(e) {
- const comment = e.detail.comment;
- const msg = comment.message;
- const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
- const response = quoteStr + 'Please fix.';
- this._createReplyComment(comment, response, false, true);
- }
-
- _commentElWithDraftID(id) {
- const els = Polymer.dom(this.root).querySelectorAll('gr-comment');
- for (const el of els) {
- if (el.comment.id === id || el.comment.__draftID === id) {
- return el;
- }
- }
- return null;
- }
-
- _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
- opt_range) {
- const d = this._newDraft(opt_lineNum);
- d.in_reply_to = inReplyTo;
- d.range = opt_range;
- if (opt_message != null) {
- d.message = opt_message;
- }
- if (opt_unresolved !== undefined) {
- d.unresolved = opt_unresolved;
- }
- return d;
- }
-
- /**
- * @param {number=} opt_lineNum
- * @param {!Object=} opt_range
- */
- _newDraft(opt_lineNum, opt_range) {
- const d = {
- __draft: true,
- __draftID: Math.random().toString(36),
- __date: new Date(),
- path: this.path,
- patchNum: this.patchNum,
- side: this._getSide(this.isOnParent),
- __commentSide: this.commentSide,
- };
- if (opt_lineNum) {
- d.line = opt_lineNum;
- }
- if (opt_range) {
- d.range = opt_range;
- }
- if (this.parentIndex) {
- d.parent = this.parentIndex;
- }
- return d;
- }
-
- _getSide(isOnParent) {
- if (isOnParent) { return 'PARENT'; }
- return 'REVISION';
- }
-
- _computeRootId(comments) {
- // Keep the root ID even if the comment was removed, so that notification
- // to sync will know which thread to remove.
- if (!comments.base.length) { return this.rootId; }
- const rootComment = comments.base[0];
- return rootComment.id || rootComment.__draftID;
- }
-
- _handleCommentDiscard(e) {
- const diffCommentEl = Polymer.dom(e).rootTarget;
- const comment = diffCommentEl.comment;
- const idx = this._indexOf(comment, this.comments);
- if (idx == -1) {
- throw Error('Cannot find comment ' +
- JSON.stringify(diffCommentEl.comment));
- }
- this.splice('comments', idx, 1);
- if (this.comments.length === 0) {
- this.fireRemoveSelf();
- }
- this._handleCommentSavedOrDiscarded(e);
-
- // Check to see if there are any other open comments getting edited and
- // set the local storage value to its message value.
- for (const changeComment of this.comments) {
- if (changeComment.__editing) {
- const commentLocation = {
- changeNum: this.changeNum,
- patchNum: this.patchNum,
- path: changeComment.path,
- line: changeComment.line,
- };
- return this.$.storage.setDraftComment(commentLocation,
- changeComment.message);
- }
- }
- }
-
- _handleCommentSavedOrDiscarded(e) {
- this.dispatchEvent(new CustomEvent('thread-changed',
- {detail: {rootId: this.rootId, path: this.path},
- bubbles: false}));
- }
-
- _handleCommentUpdate(e) {
- const comment = e.detail.comment;
- const index = this._indexOf(comment, this.comments);
- if (index === -1) {
- // This should never happen: comment belongs to another thread.
- console.warn('Comment update for another comment thread.');
- return;
- }
- this.set(['comments', index], comment);
- // Because of the way we pass these comment objects around by-ref, in
- // combination with the fact that Polymer does dirty checking in
- // observers, the this.set() call above will not cause a thread update in
- // some situations.
- this.updateThreadProperties();
- }
-
- _indexOf(comment, arr) {
- for (let i = 0; i < arr.length; i++) {
- const c = arr[i];
- if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
- (c.id != null && c.id == comment.id)) {
- return i;
- }
- }
- return -1;
- }
-
- _computeHostClass(unresolved) {
- if (this.isRobotComment) {
- return 'robotComment';
- }
- return unresolved ? 'unresolved' : '';
- }
-
- /**
- * Load the project config when a project name has been provided.
- *
- * @param {string} name The project name.
- */
- _projectNameChanged(name) {
- if (!name) { return; }
- this.$.restAPI.getProjectConfig(name).then(config => {
- this._projectConfig = config;
- });
+ // If the comment was collapsed, re-open it to make it clear which
+ // actions are available.
+ commentEl.collapsed = false;
+ } else {
+ const range = opt_range ? opt_range :
+ lastComment ? lastComment.range : undefined;
+ const unresolved = lastComment ? lastComment.unresolved : undefined;
+ this.addDraft(opt_lineNum, range, unresolved);
}
}
- customElements.define(GrCommentThread.is, GrCommentThread);
-})();
+ addDraft(opt_lineNum, opt_range, opt_unresolved) {
+ const draft = this._newDraft(opt_lineNum, opt_range);
+ draft.__editing = true;
+ draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
+ this.push('comments', draft);
+ }
+
+ fireRemoveSelf() {
+ this.dispatchEvent(new CustomEvent('thread-discard',
+ {detail: {rootId: this.rootId}, bubbles: false}));
+ }
+
+ _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
+ return Gerrit.Nav.getUrlForDiffById(changeNum,
+ projectName, path, patchNum,
+ null, this.lineNum);
+ }
+
+ _computeDisplayPath(path) {
+ const lineString = this.lineNum ? `#${this.lineNum}` : '';
+ return this.computeDisplayPath(path) + lineString;
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _commentsChanged() {
+ this._orderedComments = this._sortedComments(this.comments);
+ this.updateThreadProperties();
+ }
+
+ updateThreadProperties() {
+ if (this._orderedComments.length) {
+ this._lastComment = this._getLastComment();
+ this.unresolved = this._lastComment.unresolved;
+ this.hasDraft = this._lastComment.__draft;
+ this.isRobotComment = !!(this._lastComment.robot_id);
+ }
+ }
+
+ _shouldDisableAction(_showActions, _lastComment) {
+ return !_showActions || !_lastComment || !!_lastComment.__draft;
+ }
+
+ _hideActions(_showActions, _lastComment) {
+ return this._shouldDisableAction(_showActions, _lastComment) ||
+ !!_lastComment.robot_id;
+ }
+
+ _getLastComment() {
+ return this._orderedComments[this._orderedComments.length - 1] || {};
+ }
+
+ _handleEKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+ // Don’t preventDefault in this case because it will render the event
+ // useless for other handlers (other gr-comment-thread elements).
+ if (e.detail.keyboardEvent.shiftKey) {
+ this._expandCollapseComments(true);
+ } else {
+ if (this.modifierPressed(e)) { return; }
+ this._expandCollapseComments(false);
+ }
+ }
+
+ _expandCollapseComments(actionIsCollapse) {
+ const comments =
+ dom(this.root).querySelectorAll('gr-comment');
+ for (const comment of comments) {
+ comment.collapsed = actionIsCollapse;
+ }
+ }
+
+ /**
+ * Sets the initial state of the comment thread.
+ * Expands the thread if one of the following is true:
+ * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
+ * thread is unresolved,
+ * - it's a robot comment.
+ */
+ _setInitialExpandedState() {
+ if (this._orderedComments) {
+ for (let i = 0; i < this._orderedComments.length; i++) {
+ const comment = this._orderedComments[i];
+ const isRobotComment = !!comment.robot_id;
+ // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
+ const resolvedThread = !this.unresolved ||
+ this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
+ comment.collapsed = !isRobotComment && resolvedThread;
+ }
+ }
+ }
+
+ _sortedComments(comments) {
+ return comments.slice().sort((c1, c2) => {
+ const c1Date = c1.__date || util.parseDate(c1.updated);
+ const c2Date = c2.__date || util.parseDate(c2.updated);
+ const dateCompare = c1Date - c2Date;
+ // Ensure drafts are at the end. There should only be one but in edge
+ // cases could be more. In the unlikely event two drafts are being
+ // compared, use the typical date compare.
+ if (c2.__draft && !c1.__draft ) { return -1; }
+ if (c1.__draft && !c2.__draft ) { return 1; }
+ if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; }
+ // If same date, fall back to sorting by id.
+ return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
+ });
+ }
+
+ _createReplyComment(parent, content, opt_isEditing,
+ opt_unresolved) {
+ this.$.reporting.recordDraftInteraction();
+ const reply = this._newReply(
+ this._orderedComments[this._orderedComments.length - 1].id,
+ parent.line,
+ content,
+ opt_unresolved,
+ parent.range);
+
+ // If there is currently a comment in an editing state, add an attribute
+ // so that the gr-comment knows not to populate the draft text.
+ for (let i = 0; i < this.comments.length; i++) {
+ if (this.comments[i].__editing) {
+ reply.__otherEditing = true;
+ break;
+ }
+ }
+
+ if (opt_isEditing) {
+ reply.__editing = true;
+ }
+
+ this.push('comments', reply);
+
+ if (!opt_isEditing) {
+ // Allow the reply to render in the dom-repeat.
+ this.async(() => {
+ const commentEl = this._commentElWithDraftID(reply.__draftID);
+ commentEl.save();
+ }, 1);
+ }
+ }
+
+ _isDraft(comment) {
+ return !!comment.__draft;
+ }
+
+ /**
+ * @param {boolean=} opt_quote
+ */
+ _processCommentReply(opt_quote) {
+ const comment = this._lastComment;
+ let quoteStr;
+ if (opt_quote) {
+ const msg = comment.message;
+ quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
+ }
+ this._createReplyComment(comment, quoteStr, true, comment.unresolved);
+ }
+
+ _handleCommentReply(e) {
+ this._processCommentReply();
+ }
+
+ _handleCommentQuote(e) {
+ this._processCommentReply(true);
+ }
+
+ _handleCommentAck(e) {
+ const comment = this._lastComment;
+ this._createReplyComment(comment, 'Ack', false, false);
+ }
+
+ _handleCommentDone(e) {
+ const comment = this._lastComment;
+ this._createReplyComment(comment, 'Done', false, false);
+ }
+
+ _handleCommentFix(e) {
+ const comment = e.detail.comment;
+ const msg = comment.message;
+ const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
+ const response = quoteStr + 'Please fix.';
+ this._createReplyComment(comment, response, false, true);
+ }
+
+ _commentElWithDraftID(id) {
+ const els = dom(this.root).querySelectorAll('gr-comment');
+ for (const el of els) {
+ if (el.comment.id === id || el.comment.__draftID === id) {
+ return el;
+ }
+ }
+ return null;
+ }
+
+ _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
+ opt_range) {
+ const d = this._newDraft(opt_lineNum);
+ d.in_reply_to = inReplyTo;
+ d.range = opt_range;
+ if (opt_message != null) {
+ d.message = opt_message;
+ }
+ if (opt_unresolved !== undefined) {
+ d.unresolved = opt_unresolved;
+ }
+ return d;
+ }
+
+ /**
+ * @param {number=} opt_lineNum
+ * @param {!Object=} opt_range
+ */
+ _newDraft(opt_lineNum, opt_range) {
+ const d = {
+ __draft: true,
+ __draftID: Math.random().toString(36),
+ __date: new Date(),
+ path: this.path,
+ patchNum: this.patchNum,
+ side: this._getSide(this.isOnParent),
+ __commentSide: this.commentSide,
+ };
+ if (opt_lineNum) {
+ d.line = opt_lineNum;
+ }
+ if (opt_range) {
+ d.range = opt_range;
+ }
+ if (this.parentIndex) {
+ d.parent = this.parentIndex;
+ }
+ return d;
+ }
+
+ _getSide(isOnParent) {
+ if (isOnParent) { return 'PARENT'; }
+ return 'REVISION';
+ }
+
+ _computeRootId(comments) {
+ // Keep the root ID even if the comment was removed, so that notification
+ // to sync will know which thread to remove.
+ if (!comments.base.length) { return this.rootId; }
+ const rootComment = comments.base[0];
+ return rootComment.id || rootComment.__draftID;
+ }
+
+ _handleCommentDiscard(e) {
+ const diffCommentEl = dom(e).rootTarget;
+ const comment = diffCommentEl.comment;
+ const idx = this._indexOf(comment, this.comments);
+ if (idx == -1) {
+ throw Error('Cannot find comment ' +
+ JSON.stringify(diffCommentEl.comment));
+ }
+ this.splice('comments', idx, 1);
+ if (this.comments.length === 0) {
+ this.fireRemoveSelf();
+ }
+ this._handleCommentSavedOrDiscarded(e);
+
+ // Check to see if there are any other open comments getting edited and
+ // set the local storage value to its message value.
+ for (const changeComment of this.comments) {
+ if (changeComment.__editing) {
+ const commentLocation = {
+ changeNum: this.changeNum,
+ patchNum: this.patchNum,
+ path: changeComment.path,
+ line: changeComment.line,
+ };
+ return this.$.storage.setDraftComment(commentLocation,
+ changeComment.message);
+ }
+ }
+ }
+
+ _handleCommentSavedOrDiscarded(e) {
+ this.dispatchEvent(new CustomEvent('thread-changed',
+ {detail: {rootId: this.rootId, path: this.path},
+ bubbles: false}));
+ }
+
+ _handleCommentUpdate(e) {
+ const comment = e.detail.comment;
+ const index = this._indexOf(comment, this.comments);
+ if (index === -1) {
+ // This should never happen: comment belongs to another thread.
+ console.warn('Comment update for another comment thread.');
+ return;
+ }
+ this.set(['comments', index], comment);
+ // Because of the way we pass these comment objects around by-ref, in
+ // combination with the fact that Polymer does dirty checking in
+ // observers, the this.set() call above will not cause a thread update in
+ // some situations.
+ this.updateThreadProperties();
+ }
+
+ _indexOf(comment, arr) {
+ for (let i = 0; i < arr.length; i++) {
+ const c = arr[i];
+ if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
+ (c.id != null && c.id == comment.id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ _computeHostClass(unresolved) {
+ if (this.isRobotComment) {
+ return 'robotComment';
+ }
+ return unresolved ? 'unresolved' : '';
+ }
+
+ /**
+ * Load the project config when a project name has been provided.
+ *
+ * @param {string} name The project name.
+ */
+ _projectNameChanged(name) {
+ if (!name) { return; }
+ this.$.restAPI.getProjectConfig(name).then(config => {
+ this._projectConfig = config;
+ });
+ }
+}
+
+customElements.define(GrCommentThread.is, GrCommentThread);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
index d615a7f..1d991cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -1,32 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../gr-comment/gr-comment.html">
-
-<dom-module id="gr-comment-thread">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
font-family: var(--font-family);
@@ -83,58 +73,25 @@
</style>
<template is="dom-if" if="[[showFilePath]]">
<div class="pathInfo">
- <a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
+ <a href\$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
<span class="descriptionText">Patchset [[patchNum]]</span>
</div>
</template>
- <div id="container" class$="[[_computeHostClass(unresolved, isRobotComment)]]">
- <template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
- as="comment">
- <gr-comment
- comment="{{comment}}"
- comments="{{comments}}"
- robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]"
- change-num="[[changeNum]]"
- patch-num="[[patchNum]]"
- draft="[[_isDraft(comment)]]"
- show-actions="[[_showActions]]"
- comment-side="[[comment.__commentSide]]"
- side="[[comment.side]]"
- project-config="[[_projectConfig]]"
- on-create-fix-comment="_handleCommentFix"
- on-comment-discard="_handleCommentDiscard"
- on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment>
+ <div id="container" class\$="[[_computeHostClass(unresolved, isRobotComment)]]">
+ <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment">
+ <gr-comment comment="{{comment}}" comments="{{comments}}" robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" draft="[[_isDraft(comment)]]" show-actions="[[_showActions]]" comment-side="[[comment.__commentSide]]" side="[[comment.side]]" project-config="[[_projectConfig]]" on-create-fix-comment="_handleCommentFix" on-comment-discard="_handleCommentDiscard" on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment>
</template>
- <div id="commentInfoContainer"
- hidden$="[[_hideActions(_showActions, _lastComment)]]">
- <span id="unresolvedLabel" hidden$="[[!unresolved]]">Unresolved</span>
+ <div id="commentInfoContainer" hidden\$="[[_hideActions(_showActions, _lastComment)]]">
+ <span id="unresolvedLabel" hidden\$="[[!unresolved]]">Unresolved</span>
<div id="actions">
- <gr-button
- id="replyBtn"
- link
- class="action reply"
- on-click="_handleCommentReply">Reply</gr-button>
- <gr-button
- id="quoteBtn"
- link
- class="action quote"
- on-click="_handleCommentQuote">Quote</gr-button>
- <gr-button
- id="ackBtn"
- link
- class="action ack"
- on-click="_handleCommentAck">Ack</gr-button>
- <gr-button
- id="doneBtn"
- link
- class="action done"
- on-click="_handleCommentDone">Done</gr-button>
+ <gr-button id="replyBtn" link="" class="action reply" on-click="_handleCommentReply">Reply</gr-button>
+ <gr-button id="quoteBtn" link="" class="action quote" on-click="_handleCommentQuote">Quote</gr-button>
+ <gr-button id="ackBtn" link="" class="action ack" on-click="_handleCommentAck">Ack</gr-button>
+ <gr-button id="doneBtn" link="" class="action done" on-click="_handleCommentDone">Done</gr-button>
</div>
</div>
</div>
<gr-reporting id="reporting"></gr-reporting>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-storage id="storage"></gr-storage>
- </template>
- <script src="gr-comment-thread.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
index a17a174..bb71869 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-comment-thread</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-comment-thread.html">
+<script type="module" src="./gr-comment-thread.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment-thread.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -43,197 +49,14 @@
</template>
</test-fixture>
-<script>
- suite('gr-comment-thread tests', async () => {
- await readyToTest();
-
- suite('basic test', () => {
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(false); },
- });
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('comments are sorted correctly', () => {
- const comments = [
- {
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- __date: new Date('2015-12-25'),
- }, {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- id: 'sally_to_dr_finklestein',
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000',
- }, {
- id: 'sallys_defiance',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000',
- }, {
- id: 'dr_finklesteins_response',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'no i will pull a thread and your arm will fall off',
- updated: '2015-10-31 11:00:20.396000000',
- }, {
- id: 'sallys_mission',
- message: 'i have to find santa',
- updated: '2015-12-24 15:00:20.396000000',
- },
- ];
- const results = element._sortedComments(comments);
- assert.deepEqual(results, [
- {
- id: 'sally_to_dr_finklestein',
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000',
- }, {
- id: 'dr_finklesteins_response',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'no i will pull a thread and your arm will fall off',
- updated: '2015-10-31 11:00:20.396000000',
- }, {
- id: 'sallys_defiance',
- in_reply_to: 'sally_to_dr_finklestein',
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000',
- }, {
- id: 'sallys_confession',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- id: 'sallys_mission',
- message: 'i have to find santa',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- __date: new Date('2015-12-25'),
- },
- ]);
- });
-
- test('addOrEditDraft w/ edit draft', () => {
- element.comments = [{
- id: 'jacks_reply',
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- updated: '2015-12-25 15:00:20.396000000',
- __draft: true,
- }];
- const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
- () => { return {}; });
- const addDraftStub = sandbox.stub(element, 'addDraft');
-
- element.addOrEditDraft(123);
-
- assert.isTrue(commentElStub.called);
- assert.isFalse(addDraftStub.called);
- });
-
- test('addOrEditDraft w/o edit draft', () => {
- element.comments = [];
- const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
- () => { return {}; });
- const addDraftStub = sandbox.stub(element, 'addDraft');
-
- element.addOrEditDraft(123);
-
- assert.isFalse(commentElStub.called);
- assert.isTrue(addDraftStub.called);
- });
-
- test('_shouldDisableAction', () => {
- let showActions = true;
- const lastComment = {};
- assert.equal(
- element._shouldDisableAction(showActions, lastComment), false);
- showActions = false;
- assert.equal(
- element._shouldDisableAction(showActions, lastComment), true);
- showActions = true;
- lastComment.__draft = true;
- assert.equal(
- element._shouldDisableAction(showActions, lastComment), true);
- const robotComment = {};
- robotComment.robot_id = true;
- assert.equal(
- element._shouldDisableAction(showActions, robotComment), false);
- });
-
- test('_hideActions', () => {
- let showActions = true;
- const lastComment = {};
- assert.equal(element._hideActions(showActions, lastComment), false);
- showActions = false;
- assert.equal(element._hideActions(showActions, lastComment), true);
- showActions = true;
- lastComment.__draft = true;
- assert.equal(element._hideActions(showActions, lastComment), true);
- const robotComment = {};
- robotComment.robot_id = true;
- assert.equal(element._hideActions(showActions, robotComment), true);
- });
-
- test('setting project name loads the project config', done => {
- const projectName = 'foo/bar/baz';
- const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
- .returns(Promise.resolve({}));
- element.projectName = projectName;
- flush(() => {
- assert.isTrue(getProjectStub.calledWithExactly(projectName));
- done();
- });
- });
-
- test('optionally show file path', () => {
- // Path info doesn't exist when showFilePath is false. Because it's in a
- // dom-if it is not yet in the dom.
- assert.isNotOk(element.shadowRoot
- .querySelector('.pathInfo'));
-
- sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
- element.changeNum = 123;
- element.projectName = 'test project';
- element.path = 'path/to/file';
- element.patchNum = 3;
- element.lineNum = 5;
- element.showFilePath = true;
- flushAsynchronousOperations();
- assert.isOk(element.shadowRoot
- .querySelector('.pathInfo'));
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.pathInfo')).display,
- 'none');
- assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
- element.changeNum, element.projectName, element.path,
- element.patchNum, null, element.lineNum));
- });
-
- test('_computeDisplayPath', () => {
- const path = 'path/to/file';
- assert.equal(element._computeDisplayPath(path), 'path/to/file');
-
- element.lineNum = 5;
- assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
- });
- });
- });
-
- suite('comment action tests', () => {
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment-thread.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-comment-thread tests', () => {
+ suite('basic test', () => {
let element;
let sandbox;
@@ -241,535 +64,721 @@
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(false); },
- saveDiffDraft() {
- return Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(')]}\'\n' +
- JSON.stringify({
- id: '7afa4931_de3d65bd',
- path: '/path/to/file.txt',
- line: 5,
- in_reply_to: 'baf0414d_60047215',
- updated: '2015-12-21 02:01:10.850000000',
- message: 'Done',
- }));
- },
- });
- },
- deleteDiffDraft() { return Promise.resolve({ok: true}); },
});
- element = fixture('withComment');
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000',
- path: '/path/to/file.txt',
- }];
- flushAsynchronousOperations();
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
- test('reply', () => {
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- assert.ok(commentEl);
-
- const replyBtn = element.$.replyBtn;
- MockInteractions.tap(replyBtn);
- flushAsynchronousOperations();
-
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.notOk(drafts[0].message, 'message should be empty');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('quote reply', () => {
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- assert.ok(commentEl);
-
- const quoteBtn = element.$.quoteBtn;
- MockInteractions.tap(quoteBtn);
- flushAsynchronousOperations();
-
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('quote reply multiline', () => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
+ test('comments are sorted correctly', () => {
+ const comments = [
+ {
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ __date: new Date('2015-12-25'),
+ }, {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ id: 'sally_to_dr_finklestein',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ }, {
+ id: 'sallys_defiance',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ }, {
+ id: 'dr_finklesteins_response',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000',
+ }, {
+ id: 'sallys_mission',
+ message: 'i have to find santa',
+ updated: '2015-12-24 15:00:20.396000000',
},
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?\nIt might be!',
- updated: '2015-12-08 19:48:33.843000000',
- }];
- flushAsynchronousOperations();
-
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const quoteBtn = element.$.quoteBtn;
- MockInteractions.tap(quoteBtn);
- flushAsynchronousOperations();
-
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message,
- '> is this a crossover episode!?\n> It might be!\n\n');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('ack', done => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.changeNum = '42';
- element.patchNum = '1';
-
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const ackBtn = element.$.ackBtn;
- MockInteractions.tap(ackBtn);
- flush(() => {
- const drafts = element.comments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message, 'Ack');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.equal(drafts[0].unresolved, false);
- assert.isTrue(reportStub.calledOnce);
- done();
- });
- });
-
- test('done', done => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.changeNum = '42';
- element.patchNum = '1';
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const doneBtn = element.$.doneBtn;
- MockInteractions.tap(doneBtn);
- flush(() => {
- const drafts = element.comments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(drafts[0].message, 'Done');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isFalse(drafts[0].unresolved);
- assert.isTrue(reportStub.calledOnce);
- done();
- });
- });
-
- test('save', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.path = '/path/to/file.txt';
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
-
- const saveOrDiscardStub = sandbox.stub();
- element.addEventListener('thread-changed', saveOrDiscardStub);
- element.shadowRoot
- .querySelector('gr-comment')._fireSave();
-
- flush(() => {
- assert.isTrue(saveOrDiscardStub.called);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
- 'baf0414d_60047215');
- assert.equal(element.rootId, 'baf0414d_60047215');
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
- '/path/to/file.txt');
- done();
- });
- });
-
- test('please fix', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- assert.ok(commentEl);
- commentEl.addEventListener('create-fix-comment', () => {
- const drafts = element._orderedComments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 1);
- assert.equal(
- drafts[0].message, '> is this a crossover episode!?\n\nPlease fix.');
- assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
- assert.isTrue(drafts[0].unresolved);
- done();
- });
- commentEl.fire('create-fix-comment', {comment: commentEl.comment},
- {bubbles: false});
- });
-
- test('discard', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.path = '/path/to/file.txt';
- element.push('comments', element._newReply(
- element.comments[0].id,
- element.comments[0].line,
- element.comments[0].path,
- 'it’s pronouced jiff, not giff'));
- flushAsynchronousOperations();
-
- const saveOrDiscardStub = sandbox.stub();
- element.addEventListener('thread-changed', saveOrDiscardStub);
- const draftEl =
- Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
- assert.ok(draftEl);
- draftEl.addEventListener('comment-discard', () => {
- const drafts = element.comments.filter(c => c.__draft == true);
- assert.equal(drafts.length, 0);
- assert.isTrue(saveOrDiscardStub.called);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
- element.rootId);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
- element.path);
- done();
- });
- draftEl.fire('comment-discard', {comment: draftEl.comment},
- {bubbles: false});
- });
-
- test('discard with a single comment still fires event with previous rootId',
- done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.path = '/path/to/file.txt';
- element.comments = [];
- element.addOrEditDraft('1');
- flushAsynchronousOperations();
- const rootId = element.rootId;
- assert.isOk(rootId);
-
- const saveOrDiscardStub = sandbox.stub();
- element.addEventListener('thread-changed', saveOrDiscardStub);
- const draftEl =
- Polymer.dom(element.root).querySelectorAll('gr-comment')[0];
- assert.ok(draftEl);
- draftEl.addEventListener('comment-discard', () => {
- assert.equal(element.comments.length, 0);
- assert.isTrue(saveOrDiscardStub.called);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
- rootId);
- assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
- element.path);
- done();
- });
- draftEl.fire('comment-discard', {comment: draftEl.comment},
- {bubbles: false});
- });
-
- test('first editing comment does not add __otherEditing attribute', () => {
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
+ ];
+ const results = element._sortedComments(comments);
+ assert.deepEqual(results, [
+ {
+ id: 'sally_to_dr_finklestein',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ }, {
+ id: 'dr_finklesteins_response',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000',
+ }, {
+ id: 'sallys_defiance',
+ in_reply_to: 'sally_to_dr_finklestein',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ }, {
+ id: 'sallys_confession',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ id: 'sallys_mission',
+ message: 'i have to find santa',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ __date: new Date('2015-12-25'),
},
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000',
+ ]);
+ });
+
+ test('addOrEditDraft w/ edit draft', () => {
+ element.comments = [{
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
__draft: true,
}];
+ const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
+ () => { return {}; });
+ const addDraftStub = sandbox.stub(element, 'addDraft');
- const replyBtn = element.$.replyBtn;
- MockInteractions.tap(replyBtn);
- flushAsynchronousOperations();
+ element.addOrEditDraft(123);
- const editing = element._orderedComments.filter(c => c.__editing == true);
- assert.equal(editing.length, 1);
- assert.equal(!!editing[0].__otherEditing, false);
+ assert.isTrue(commentElStub.called);
+ assert.isFalse(addDraftStub.called);
});
- test('When not editing other comments, local storage not set' +
- ' after discard', done => {
- element.changeNum = '42';
- element.patchNum = '1';
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:31.843000000',
- },
- {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- __draftID: '1',
- in_reply_to: 'baf0414d_60047215',
- line: 5,
- message: 'yes',
- updated: '2015-12-08 19:48:32.843000000',
- __draft: true,
- __editing: true,
- },
- {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- __draftID: '2',
- in_reply_to: 'baf0414d_60047215',
- line: 5,
- message: 'no',
- updated: '2015-12-08 19:48:33.843000000',
- __draft: true,
- }];
- const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
- flushAsynchronousOperations();
+ test('addOrEditDraft w/o edit draft', () => {
+ element.comments = [];
+ const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
+ () => { return {}; });
+ const addDraftStub = sandbox.stub(element, 'addDraft');
- const draftEl =
- Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
- assert.ok(draftEl);
- draftEl.addEventListener('comment-discard', () => {
- assert.isFalse(storageStub.called);
- storageStub.restore();
+ element.addOrEditDraft(123);
+
+ assert.isFalse(commentElStub.called);
+ assert.isTrue(addDraftStub.called);
+ });
+
+ test('_shouldDisableAction', () => {
+ let showActions = true;
+ const lastComment = {};
+ assert.equal(
+ element._shouldDisableAction(showActions, lastComment), false);
+ showActions = false;
+ assert.equal(
+ element._shouldDisableAction(showActions, lastComment), true);
+ showActions = true;
+ lastComment.__draft = true;
+ assert.equal(
+ element._shouldDisableAction(showActions, lastComment), true);
+ const robotComment = {};
+ robotComment.robot_id = true;
+ assert.equal(
+ element._shouldDisableAction(showActions, robotComment), false);
+ });
+
+ test('_hideActions', () => {
+ let showActions = true;
+ const lastComment = {};
+ assert.equal(element._hideActions(showActions, lastComment), false);
+ showActions = false;
+ assert.equal(element._hideActions(showActions, lastComment), true);
+ showActions = true;
+ lastComment.__draft = true;
+ assert.equal(element._hideActions(showActions, lastComment), true);
+ const robotComment = {};
+ robotComment.robot_id = true;
+ assert.equal(element._hideActions(showActions, robotComment), true);
+ });
+
+ test('setting project name loads the project config', done => {
+ const projectName = 'foo/bar/baz';
+ const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
+ .returns(Promise.resolve({}));
+ element.projectName = projectName;
+ flush(() => {
+ assert.isTrue(getProjectStub.calledWithExactly(projectName));
done();
});
- draftEl.fire('comment-discard', {comment: draftEl.comment},
- {bubbles: false});
});
- test('comment-update', () => {
- const commentEl = element.shadowRoot
- .querySelector('gr-comment');
- const updatedComment = {
- id: element.comments[0].id,
- foo: 'bar',
- };
- commentEl.fire('comment-update', {comment: updatedComment});
- assert.strictEqual(element.comments[0], updatedComment);
- });
+ test('optionally show file path', () => {
+ // Path info doesn't exist when showFilePath is false. Because it's in a
+ // dom-if it is not yet in the dom.
+ assert.isNotOk(element.shadowRoot
+ .querySelector('.pathInfo'));
- suite('jack and sally comment data test consolidation', () => {
- setup(() => {
- element.comments = [
- {
- id: 'jacks_reply',
- message: 'i like you, too',
- in_reply_to: 'sallys_confession',
- updated: '2015-12-25 15:00:20.396000000',
- unresolved: false,
- }, {
- id: 'sallys_confession',
- in_reply_to: 'nonexistent_comment',
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000',
- }, {
- id: 'sally_to_dr_finklestein',
- in_reply_to: 'nonexistent_comment',
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000',
- }, {
- id: 'sallys_defiance',
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000',
- }];
- });
-
- test('orphan replies', () => {
- assert.equal(4, element._orderedComments.length);
- });
-
- test('keyboard shortcuts', () => {
- const expandCollapseStub =
- sinon.stub(element, '_expandCollapseComments');
- MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
- assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
-
- MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
- assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
- });
-
- test('comment in_reply_to is either null or most recent comment', () => {
- element._createReplyComment(element.comments[3], 'dummy', true);
- flushAsynchronousOperations();
- assert.equal(element._orderedComments.length, 5);
- assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
- });
-
- test('resolvable comments', () => {
- assert.isFalse(element.unresolved);
- element._createReplyComment(element.comments[3], 'dummy', true, true);
- flushAsynchronousOperations();
- assert.isTrue(element.unresolved);
- });
-
- test('_setInitialExpandedState', () => {
- element.unresolved = true;
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isFalse(element.comments[i].collapsed);
- }
- element.unresolved = false;
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isTrue(element.comments[i].collapsed);
- }
- for (let i = 0; i < element.comments.length; i++) {
- element.comments[i].robot_id = 123;
- }
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isFalse(element.comments[i].collapsed);
- }
- });
- });
-
- test('_computeHostClass', () => {
- assert.equal(element._computeHostClass(true), 'unresolved');
- assert.equal(element._computeHostClass(false), '');
- });
-
- test('addDraft sets unresolved state correctly', () => {
- let unresolved = true;
- element.comments = [];
- element.addDraft(null, null, unresolved);
- assert.equal(element.comments[0].unresolved, true);
-
- unresolved = false; // comment should get added as actually resolved.
- element.comments = [];
- element.addDraft(null, null, unresolved);
- assert.equal(element.comments[0].unresolved, false);
-
- element.comments = [];
- element.addDraft();
- assert.equal(element.comments[0].unresolved, true);
- });
-
- test('_newDraft', () => {
- element.commentSide = 'left';
+ sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
+ element.changeNum = 123;
+ element.projectName = 'test project';
+ element.path = 'path/to/file';
element.patchNum = 3;
- const draft = element._newDraft();
- assert.equal(draft.__commentSide, 'left');
- assert.equal(draft.patchNum, 3);
+ element.lineNum = 5;
+ element.showFilePath = true;
+ flushAsynchronousOperations();
+ assert.isOk(element.shadowRoot
+ .querySelector('.pathInfo'));
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.pathInfo')).display,
+ 'none');
+ assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
+ element.changeNum, element.projectName, element.path,
+ element.patchNum, null, element.lineNum));
});
- test('new comment gets created', () => {
- element.comments = [];
- element.addOrEditDraft(1);
- assert.equal(element.comments.length, 1);
- // Mock a submitted comment.
- element.comments[0].id = element.comments[0].__draftID;
- element.comments[0].__draft = false;
- element.addOrEditDraft(1);
- assert.equal(element.comments.length, 2);
- });
+ test('_computeDisplayPath', () => {
+ const path = 'path/to/file';
+ assert.equal(element._computeDisplayPath(path), 'path/to/file');
- test('unresolved label', () => {
- element.unresolved = false;
- assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
- element.unresolved = true;
- assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
- });
-
- test('draft comments are at the end of orderedComments', () => {
- element.comments = [{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 2,
- line: 5,
- message: 'Earlier draft',
- updated: '2015-12-08 19:48:33.843000000',
- __draft: true,
- },
- {
- author: {
- name: 'Mr. Peanutbutter2',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 1,
- line: 5,
- message: 'This comment was left last but is not a draft',
- updated: '2015-12-10 19:48:33.843000000',
- },
- {
- author: {
- name: 'Mr. Peanutbutter2',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 3,
- line: 5,
- message: 'Later draft',
- updated: '2015-12-09 19:48:33.843000000',
- __draft: true,
- }];
- assert.equal(element._orderedComments[0].id, '1');
- assert.equal(element._orderedComments[1].id, '2');
- assert.equal(element._orderedComments[2].id, '3');
- });
-
- test('reflects lineNum and commentSide to attributes', () => {
- element.lineNum = 7;
- element.commentSide = 'left';
-
- assert.equal(element.getAttribute('line-num'), '7');
- assert.equal(element.getAttribute('comment-side'), 'left');
- });
-
- test('reflects range to JSON serialized attribute if set', () => {
- element.range = {
- start_line: 4,
- end_line: 5,
- start_character: 6,
- end_character: 7,
- };
-
- assert.deepEqual(
- JSON.parse(element.getAttribute('range')),
- {start_line: 4, end_line: 5, start_character: 6, end_character: 7});
- });
-
- test('removes range attribute if range is unset', () => {
- element.range = {
- start_line: 4,
- end_line: 5,
- start_character: 6,
- end_character: 7,
- };
- element.range = undefined;
-
- assert.notOk(element.hasAttribute('range'));
+ element.lineNum = 5;
+ assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
});
});
+});
+
+suite('comment action tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ saveDiffDraft() {
+ return Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(')]}\'\n' +
+ JSON.stringify({
+ id: '7afa4931_de3d65bd',
+ path: '/path/to/file.txt',
+ line: 5,
+ in_reply_to: 'baf0414d_60047215',
+ updated: '2015-12-21 02:01:10.850000000',
+ message: 'Done',
+ }));
+ },
+ });
+ },
+ deleteDiffDraft() { return Promise.resolve({ok: true}); },
+ });
+ element = fixture('withComment');
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ path: '/path/to/file.txt',
+ }];
+ flushAsynchronousOperations();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('reply', () => {
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ assert.ok(commentEl);
+
+ const replyBtn = element.$.replyBtn;
+ MockInteractions.tap(replyBtn);
+ flushAsynchronousOperations();
+
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.notOk(drafts[0].message, 'message should be empty');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('quote reply', () => {
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ assert.ok(commentEl);
+
+ const quoteBtn = element.$.quoteBtn;
+ MockInteractions.tap(quoteBtn);
+ flushAsynchronousOperations();
+
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('quote reply multiline', () => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?\nIt might be!',
+ updated: '2015-12-08 19:48:33.843000000',
+ }];
+ flushAsynchronousOperations();
+
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const quoteBtn = element.$.quoteBtn;
+ MockInteractions.tap(quoteBtn);
+ flushAsynchronousOperations();
+
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message,
+ '> is this a crossover episode!?\n> It might be!\n\n');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('ack', done => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.changeNum = '42';
+ element.patchNum = '1';
+
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const ackBtn = element.$.ackBtn;
+ MockInteractions.tap(ackBtn);
+ flush(() => {
+ const drafts = element.comments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, 'Ack');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.equal(drafts[0].unresolved, false);
+ assert.isTrue(reportStub.calledOnce);
+ done();
+ });
+ });
+
+ test('done', done => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.changeNum = '42';
+ element.patchNum = '1';
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const doneBtn = element.$.doneBtn;
+ MockInteractions.tap(doneBtn);
+ flush(() => {
+ const drafts = element.comments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(drafts[0].message, 'Done');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isFalse(drafts[0].unresolved);
+ assert.isTrue(reportStub.calledOnce);
+ done();
+ });
+ });
+
+ test('save', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.path = '/path/to/file.txt';
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+
+ const saveOrDiscardStub = sandbox.stub();
+ element.addEventListener('thread-changed', saveOrDiscardStub);
+ element.shadowRoot
+ .querySelector('gr-comment')._fireSave();
+
+ flush(() => {
+ assert.isTrue(saveOrDiscardStub.called);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+ 'baf0414d_60047215');
+ assert.equal(element.rootId, 'baf0414d_60047215');
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+ '/path/to/file.txt');
+ done();
+ });
+ });
+
+ test('please fix', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ assert.ok(commentEl);
+ commentEl.addEventListener('create-fix-comment', () => {
+ const drafts = element._orderedComments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 1);
+ assert.equal(
+ drafts[0].message, '> is this a crossover episode!?\n\nPlease fix.');
+ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+ assert.isTrue(drafts[0].unresolved);
+ done();
+ });
+ commentEl.fire('create-fix-comment', {comment: commentEl.comment},
+ {bubbles: false});
+ });
+
+ test('discard', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.path = '/path/to/file.txt';
+ element.push('comments', element._newReply(
+ element.comments[0].id,
+ element.comments[0].line,
+ element.comments[0].path,
+ 'it’s pronouced jiff, not giff'));
+ flushAsynchronousOperations();
+
+ const saveOrDiscardStub = sandbox.stub();
+ element.addEventListener('thread-changed', saveOrDiscardStub);
+ const draftEl =
+ dom(element.root).querySelectorAll('gr-comment')[1];
+ assert.ok(draftEl);
+ draftEl.addEventListener('comment-discard', () => {
+ const drafts = element.comments.filter(c => c.__draft == true);
+ assert.equal(drafts.length, 0);
+ assert.isTrue(saveOrDiscardStub.called);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+ element.rootId);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+ element.path);
+ done();
+ });
+ draftEl.fire('comment-discard', {comment: draftEl.comment},
+ {bubbles: false});
+ });
+
+ test('discard with a single comment still fires event with previous rootId',
+ done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.path = '/path/to/file.txt';
+ element.comments = [];
+ element.addOrEditDraft('1');
+ flushAsynchronousOperations();
+ const rootId = element.rootId;
+ assert.isOk(rootId);
+
+ const saveOrDiscardStub = sandbox.stub();
+ element.addEventListener('thread-changed', saveOrDiscardStub);
+ const draftEl =
+ dom(element.root).querySelectorAll('gr-comment')[0];
+ assert.ok(draftEl);
+ draftEl.addEventListener('comment-discard', () => {
+ assert.equal(element.comments.length, 0);
+ assert.isTrue(saveOrDiscardStub.called);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
+ rootId);
+ assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
+ element.path);
+ done();
+ });
+ draftEl.fire('comment-discard', {comment: draftEl.comment},
+ {bubbles: false});
+ });
+
+ test('first editing comment does not add __otherEditing attribute', () => {
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ __draft: true,
+ }];
+
+ const replyBtn = element.$.replyBtn;
+ MockInteractions.tap(replyBtn);
+ flushAsynchronousOperations();
+
+ const editing = element._orderedComments.filter(c => c.__editing == true);
+ assert.equal(editing.length, 1);
+ assert.equal(!!editing[0].__otherEditing, false);
+ });
+
+ test('When not editing other comments, local storage not set' +
+ ' after discard', done => {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:31.843000000',
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ __draftID: '1',
+ in_reply_to: 'baf0414d_60047215',
+ line: 5,
+ message: 'yes',
+ updated: '2015-12-08 19:48:32.843000000',
+ __draft: true,
+ __editing: true,
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ __draftID: '2',
+ in_reply_to: 'baf0414d_60047215',
+ line: 5,
+ message: 'no',
+ updated: '2015-12-08 19:48:33.843000000',
+ __draft: true,
+ }];
+ const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
+ flushAsynchronousOperations();
+
+ const draftEl =
+ dom(element.root).querySelectorAll('gr-comment')[1];
+ assert.ok(draftEl);
+ draftEl.addEventListener('comment-discard', () => {
+ assert.isFalse(storageStub.called);
+ storageStub.restore();
+ done();
+ });
+ draftEl.fire('comment-discard', {comment: draftEl.comment},
+ {bubbles: false});
+ });
+
+ test('comment-update', () => {
+ const commentEl = element.shadowRoot
+ .querySelector('gr-comment');
+ const updatedComment = {
+ id: element.comments[0].id,
+ foo: 'bar',
+ };
+ commentEl.fire('comment-update', {comment: updatedComment});
+ assert.strictEqual(element.comments[0], updatedComment);
+ });
+
+ suite('jack and sally comment data test consolidation', () => {
+ setup(() => {
+ element.comments = [
+ {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
+ unresolved: false,
+ }, {
+ id: 'sallys_confession',
+ in_reply_to: 'nonexistent_comment',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ id: 'sally_to_dr_finklestein',
+ in_reply_to: 'nonexistent_comment',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ }, {
+ id: 'sallys_defiance',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ }];
+ });
+
+ test('orphan replies', () => {
+ assert.equal(4, element._orderedComments.length);
+ });
+
+ test('keyboard shortcuts', () => {
+ const expandCollapseStub =
+ sinon.stub(element, '_expandCollapseComments');
+ MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
+ assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
+ assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
+ });
+
+ test('comment in_reply_to is either null or most recent comment', () => {
+ element._createReplyComment(element.comments[3], 'dummy', true);
+ flushAsynchronousOperations();
+ assert.equal(element._orderedComments.length, 5);
+ assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
+ });
+
+ test('resolvable comments', () => {
+ assert.isFalse(element.unresolved);
+ element._createReplyComment(element.comments[3], 'dummy', true, true);
+ flushAsynchronousOperations();
+ assert.isTrue(element.unresolved);
+ });
+
+ test('_setInitialExpandedState', () => {
+ element.unresolved = true;
+ element._setInitialExpandedState();
+ for (let i = 0; i < element.comments.length; i++) {
+ assert.isFalse(element.comments[i].collapsed);
+ }
+ element.unresolved = false;
+ element._setInitialExpandedState();
+ for (let i = 0; i < element.comments.length; i++) {
+ assert.isTrue(element.comments[i].collapsed);
+ }
+ for (let i = 0; i < element.comments.length; i++) {
+ element.comments[i].robot_id = 123;
+ }
+ element._setInitialExpandedState();
+ for (let i = 0; i < element.comments.length; i++) {
+ assert.isFalse(element.comments[i].collapsed);
+ }
+ });
+ });
+
+ test('_computeHostClass', () => {
+ assert.equal(element._computeHostClass(true), 'unresolved');
+ assert.equal(element._computeHostClass(false), '');
+ });
+
+ test('addDraft sets unresolved state correctly', () => {
+ let unresolved = true;
+ element.comments = [];
+ element.addDraft(null, null, unresolved);
+ assert.equal(element.comments[0].unresolved, true);
+
+ unresolved = false; // comment should get added as actually resolved.
+ element.comments = [];
+ element.addDraft(null, null, unresolved);
+ assert.equal(element.comments[0].unresolved, false);
+
+ element.comments = [];
+ element.addDraft();
+ assert.equal(element.comments[0].unresolved, true);
+ });
+
+ test('_newDraft', () => {
+ element.commentSide = 'left';
+ element.patchNum = 3;
+ const draft = element._newDraft();
+ assert.equal(draft.__commentSide, 'left');
+ assert.equal(draft.patchNum, 3);
+ });
+
+ test('new comment gets created', () => {
+ element.comments = [];
+ element.addOrEditDraft(1);
+ assert.equal(element.comments.length, 1);
+ // Mock a submitted comment.
+ element.comments[0].id = element.comments[0].__draftID;
+ element.comments[0].__draft = false;
+ element.addOrEditDraft(1);
+ assert.equal(element.comments.length, 2);
+ });
+
+ test('unresolved label', () => {
+ element.unresolved = false;
+ assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
+ element.unresolved = true;
+ assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
+ });
+
+ test('draft comments are at the end of orderedComments', () => {
+ element.comments = [{
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 2,
+ line: 5,
+ message: 'Earlier draft',
+ updated: '2015-12-08 19:48:33.843000000',
+ __draft: true,
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter2',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 1,
+ line: 5,
+ message: 'This comment was left last but is not a draft',
+ updated: '2015-12-10 19:48:33.843000000',
+ },
+ {
+ author: {
+ name: 'Mr. Peanutbutter2',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 3,
+ line: 5,
+ message: 'Later draft',
+ updated: '2015-12-09 19:48:33.843000000',
+ __draft: true,
+ }];
+ assert.equal(element._orderedComments[0].id, '1');
+ assert.equal(element._orderedComments[1].id, '2');
+ assert.equal(element._orderedComments[2].id, '3');
+ });
+
+ test('reflects lineNum and commentSide to attributes', () => {
+ element.lineNum = 7;
+ element.commentSide = 'left';
+
+ assert.equal(element.getAttribute('line-num'), '7');
+ assert.equal(element.getAttribute('comment-side'), 'left');
+ });
+
+ test('reflects range to JSON serialized attribute if set', () => {
+ element.range = {
+ start_line: 4,
+ end_line: 5,
+ start_character: 6,
+ end_character: 7,
+ };
+
+ assert.deepEqual(
+ JSON.parse(element.getAttribute('range')),
+ {start_line: 4, end_line: 5, start_character: 6, end_character: 7});
+ });
+
+ test('removes range attribute if range is unset', () => {
+ element.range = {
+ start_line: 4,
+ end_line: 5,
+ start_character: 6,
+ end_character: 7,
+ };
+ element.range = undefined;
+
+ assert.notOk(element.hasAttribute('range'));
+ });
+});
</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 9880e88..6f1eaa8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.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,797 +14,823 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const STORAGE_DEBOUNCE_INTERVAL = 400;
- const TOAST_DEBOUNCE_INTERVAL = 200;
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../gr-button/gr-button.js';
+import '../gr-dialog/gr-dialog.js';
+import '../gr-date-formatter/gr-date-formatter.js';
+import '../gr-formatted-text/gr-formatted-text.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-overlay/gr-overlay.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-storage/gr-storage.js';
+import '../gr-textarea/gr-textarea.js';
+import '../gr-tooltip-content/gr-tooltip-content.js';
+import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js';
+import '../../../scripts/rootElement.js';
+import {flush, 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-comment_html.js';
- const SAVING_MESSAGE = 'Saving';
- const DRAFT_SINGULAR = 'draft...';
- const DRAFT_PLURAL = 'drafts...';
- const SAVED_MESSAGE = 'All changes saved';
+const STORAGE_DEBOUNCE_INTERVAL = 400;
+const TOAST_DEBOUNCE_INTERVAL = 200;
- const REPORT_CREATE_DRAFT = 'CreateDraftComment';
- const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
- const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+const SAVING_MESSAGE = 'Saving';
+const DRAFT_SINGULAR = 'draft...';
+const DRAFT_PLURAL = 'drafts...';
+const SAVED_MESSAGE = 'All changes saved';
- const FILE = 'FILE';
+const REPORT_CREATE_DRAFT = 'CreateDraftComment';
+const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
+const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+
+const FILE = 'FILE';
+
+/**
+ * All candidates tips to show, will pick randomly.
+ */
+const RESPECTFUL_REVIEW_TIPS= [
+ 'DO: Assume competence.',
+ 'DO: Provide rationale or context.',
+ 'DO: Consider how comments may be interpreted.',
+ 'DON’T: Criticize the person.',
+ 'DON’T: Use harsh language.',
+];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrComment extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-comment'; }
+ /**
+ * Fired when the create fix comment action is triggered.
+ *
+ * @event create-fix-comment
+ */
/**
- * All candidates tips to show, will pick randomly.
+ * Fired when the show fix preview action is triggered.
+ *
+ * @event open-fix-preview
*/
- const RESPECTFUL_REVIEW_TIPS= [
- 'DO: Assume competence.',
- 'DO: Provide rationale or context.',
- 'DO: Consider how comments may be interpreted.',
- 'DON’T: Criticize the person.',
- 'DON’T: Use harsh language.',
- ];
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when this comment is discarded.
+ *
+ * @event comment-discard
*/
- class GrComment extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-comment'; }
- /**
- * Fired when the create fix comment action is triggered.
- *
- * @event create-fix-comment
- */
- /**
- * Fired when the show fix preview action is triggered.
- *
- * @event open-fix-preview
- */
+ /**
+ * Fired when this comment is saved.
+ *
+ * @event comment-save
+ */
- /**
- * Fired when this comment is discarded.
- *
- * @event comment-discard
- */
+ /**
+ * Fired when this comment is updated.
+ *
+ * @event comment-update
+ */
- /**
- * Fired when this comment is saved.
- *
- * @event comment-save
- */
+ /**
+ * Fired when the comment's timestamp is tapped.
+ *
+ * @event comment-anchor-tap
+ */
- /**
- * Fired when this comment is updated.
- *
- * @event comment-update
- */
+ static get properties() {
+ return {
+ changeNum: String,
+ /** @type {!Gerrit.Comment} */
+ comment: {
+ type: Object,
+ notify: true,
+ observer: '_commentChanged',
+ },
+ comments: {
+ type: Array,
+ },
+ isRobotComment: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ draft: {
+ type: Boolean,
+ value: false,
+ observer: '_draftChanged',
+ },
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_editingChanged',
+ },
+ discarding: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ hasChildren: Boolean,
+ patchNum: String,
+ showActions: Boolean,
+ _showHumanActions: Boolean,
+ _showRobotActions: Boolean,
+ collapsed: {
+ type: Boolean,
+ value: true,
+ observer: '_toggleCollapseClass',
+ },
+ /** @type {?} */
+ projectConfig: Object,
+ robotButtonDisabled: Boolean,
+ _hasHumanReply: Boolean,
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * Fired when the comment's timestamp is tapped.
- *
- * @event comment-anchor-tap
- */
+ _xhrPromise: Object, // Used for testing.
+ _messageText: {
+ type: String,
+ value: '',
+ observer: '_messageTextChanged',
+ },
+ commentSide: String,
+ side: String,
- static get properties() {
- return {
- changeNum: String,
- /** @type {!Gerrit.Comment} */
- comment: {
- type: Object,
- notify: true,
- observer: '_commentChanged',
- },
- comments: {
- type: Array,
- },
- isRobotComment: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- draft: {
- type: Boolean,
- value: false,
- observer: '_draftChanged',
- },
- editing: {
- type: Boolean,
- value: false,
- observer: '_editingChanged',
- },
- discarding: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- hasChildren: Boolean,
- patchNum: String,
- showActions: Boolean,
- _showHumanActions: Boolean,
- _showRobotActions: Boolean,
- collapsed: {
- type: Boolean,
- value: true,
- observer: '_toggleCollapseClass',
- },
- /** @type {?} */
- projectConfig: Object,
- robotButtonDisabled: Boolean,
- _hasHumanReply: Boolean,
- _isAdmin: {
- type: Boolean,
- value: false,
- },
+ resolved: Boolean,
- _xhrPromise: Object, // Used for testing.
- _messageText: {
- type: String,
- value: '',
- observer: '_messageTextChanged',
- },
- commentSide: String,
- side: String,
+ _numPendingDraftRequests: {
+ type: Object,
+ value:
+ {number: 0}, // Intentional to share the object across instances.
+ },
- resolved: Boolean,
+ _enableOverlay: {
+ type: Boolean,
+ value: false,
+ },
- _numPendingDraftRequests: {
- type: Object,
- value:
- {number: 0}, // Intentional to share the object across instances.
- },
+ /**
+ * Property for storing references to overlay elements. When the overlays
+ * are moved to Gerrit.getRootElement() to be shown they are no-longer
+ * children, so they can't be queried along the tree, so they are stored
+ * here.
+ */
+ _overlays: {
+ type: Object,
+ value: () => { return {}; },
+ },
- _enableOverlay: {
- type: Boolean,
- value: false,
- },
+ _showRespectfulTip: {
+ type: Boolean,
+ value: false,
+ },
+ _respectfulReviewTip: String,
+ _respectfulTipDismissed: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- /**
- * Property for storing references to overlay elements. When the overlays
- * are moved to Gerrit.getRootElement() to be shown they are no-longer
- * children, so they can't be queried along the tree, so they are stored
- * here.
- */
- _overlays: {
- type: Object,
- value: () => { return {}; },
- },
+ static get observers() {
+ return [
+ '_commentMessageChanged(comment.message)',
+ '_loadLocalDraft(changeNum, patchNum, comment)',
+ '_isRobotComment(comment)',
+ '_calculateActionstoShow(showActions, isRobotComment)',
+ '_computeHasHumanReply(comment, comments.*)',
+ '_onEditingChange(editing)',
+ ];
+ }
- _showRespectfulTip: {
- type: Boolean,
- value: false,
- },
- _respectfulReviewTip: String,
- _respectfulTipDismissed: {
- type: Boolean,
- value: false,
- },
- };
+ get keyBindings() {
+ return {
+ 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
+ 'esc': '_handleEsc',
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ if (this.editing) {
+ this.collapsed = false;
+ } else if (this.comment) {
+ this.collapsed = this.comment.collapsed;
}
+ this._getIsAdmin().then(isAdmin => {
+ this._isAdmin = isAdmin;
+ });
+ }
- static get observers() {
- return [
- '_commentMessageChanged(comment.message)',
- '_loadLocalDraft(changeNum, patchNum, comment)',
- '_isRobotComment(comment)',
- '_calculateActionstoShow(showActions, isRobotComment)',
- '_computeHasHumanReply(comment, comments.*)',
- '_onEditingChange(editing)',
- ];
+ /** @override */
+ detached() {
+ super.detached();
+ this.cancelDebouncer('fire-update');
+ if (this.textarea) {
+ this.textarea.closeDropdown();
}
+ }
- get keyBindings() {
- return {
- 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
- 'esc': '_handleEsc',
- };
- }
-
- /** @override */
- attached() {
- super.attached();
- if (this.editing) {
- this.collapsed = false;
- } else if (this.comment) {
- this.collapsed = this.comment.collapsed;
- }
- this._getIsAdmin().then(isAdmin => {
- this._isAdmin = isAdmin;
- });
- }
-
- /** @override */
- detached() {
- super.detached();
- this.cancelDebouncer('fire-update');
- if (this.textarea) {
- this.textarea.closeDropdown();
- }
- }
-
- _onEditingChange(editing) {
- if (!editing) return;
- // visibility based on cache this will make sure we only and always show
- // a tip once every Math.max(a day, period between creating comments)
- const cachedVisibilityOfRespectfulTip =
- this.$.storage.getRespectfulTipVisibility();
- if (!cachedVisibilityOfRespectfulTip) {
- // we still want to show the tip with a probability of 30%
- if (this.getRandomNum(0, 3) >= 1) return;
- this._showRespectfulTip = true;
- const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
- this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
- this.$.reporting.reportInteraction(
- 'respectful-tip-appeared',
- {tip: this._respectfulReviewTip}
- );
- // update cache
- this.$.storage.setRespectfulTipVisibility();
- }
- }
-
- /** Set as a separate method so easy to stub. */
- getRandomNum(min, max) {
- return Math.floor(Math.random() * (max - min) + min);
- }
-
- _computeVisibilityOfTip(showTip, tipDismissed) {
- return showTip && !tipDismissed;
- }
-
- _dismissRespectfulTip() {
- this._respectfulTipDismissed = true;
+ _onEditingChange(editing) {
+ if (!editing) return;
+ // visibility based on cache this will make sure we only and always show
+ // a tip once every Math.max(a day, period between creating comments)
+ const cachedVisibilityOfRespectfulTip =
+ this.$.storage.getRespectfulTipVisibility();
+ if (!cachedVisibilityOfRespectfulTip) {
+ // we still want to show the tip with a probability of 30%
+ if (this.getRandomNum(0, 3) >= 1) return;
+ this._showRespectfulTip = true;
+ const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
+ this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
this.$.reporting.reportInteraction(
- 'respectful-tip-dismissed',
+ 'respectful-tip-appeared',
{tip: this._respectfulReviewTip}
);
- // add a 3 day delay to the tip cache
- this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3);
+ // update cache
+ this.$.storage.setRespectfulTipVisibility();
+ }
+ }
+
+ /** Set as a separate method so easy to stub. */
+ getRandomNum(min, max) {
+ return Math.floor(Math.random() * (max - min) + min);
+ }
+
+ _computeVisibilityOfTip(showTip, tipDismissed) {
+ return showTip && !tipDismissed;
+ }
+
+ _dismissRespectfulTip() {
+ this._respectfulTipDismissed = true;
+ this.$.reporting.reportInteraction(
+ 'respectful-tip-dismissed',
+ {tip: this._respectfulReviewTip}
+ );
+ // add a 3 day delay to the tip cache
+ this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3);
+ }
+
+ _onRespectfulReadMoreClick() {
+ this.$.reporting.reportInteraction('respectful-read-more-clicked');
+ }
+
+ get textarea() {
+ return this.shadowRoot.querySelector('#editTextarea');
+ }
+
+ get confirmDeleteOverlay() {
+ if (!this._overlays.confirmDelete) {
+ this._enableOverlay = true;
+ flush();
+ this._overlays.confirmDelete = this.shadowRoot
+ .querySelector('#confirmDeleteOverlay');
+ }
+ return this._overlays.confirmDelete;
+ }
+
+ get confirmDiscardOverlay() {
+ if (!this._overlays.confirmDiscard) {
+ this._enableOverlay = true;
+ flush();
+ this._overlays.confirmDiscard = this.shadowRoot
+ .querySelector('#confirmDiscardOverlay');
+ }
+ return this._overlays.confirmDiscard;
+ }
+
+ _computeShowHideIcon(collapsed) {
+ return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
+ }
+
+ _calculateActionstoShow(showActions, isRobotComment) {
+ // Polymer 2: check for undefined
+ if ([showActions, isRobotComment].some(arg => arg === undefined)) {
+ return;
}
- _onRespectfulReadMoreClick() {
- this.$.reporting.reportInteraction('respectful-read-more-clicked');
+ this._showHumanActions = showActions && !isRobotComment;
+ this._showRobotActions = showActions && isRobotComment;
+ }
+
+ _isRobotComment(comment) {
+ this.isRobotComment = !!comment.robot_id;
+ }
+
+ isOnParent() {
+ return this.side === 'PARENT';
+ }
+
+ _getIsAdmin() {
+ return this.$.restAPI.getIsAdmin();
+ }
+
+ /**
+ * @param {*=} opt_comment
+ */
+ save(opt_comment) {
+ let comment = opt_comment;
+ if (!comment) {
+ comment = this.comment;
}
- get textarea() {
- return this.shadowRoot.querySelector('#editTextarea');
+ this.set('comment.message', this._messageText);
+ this.editing = false;
+ this.disabled = true;
+
+ if (!this._messageText) {
+ return this._discardDraft();
}
- get confirmDeleteOverlay() {
- if (!this._overlays.confirmDelete) {
- this._enableOverlay = true;
- Polymer.dom.flush();
- this._overlays.confirmDelete = this.shadowRoot
- .querySelector('#confirmDeleteOverlay');
- }
- return this._overlays.confirmDelete;
- }
+ this._xhrPromise = this._saveDraft(comment).then(response => {
+ this.disabled = false;
+ if (!response.ok) { return response; }
- get confirmDiscardOverlay() {
- if (!this._overlays.confirmDiscard) {
- this._enableOverlay = true;
- Polymer.dom.flush();
- this._overlays.confirmDiscard = this.shadowRoot
- .querySelector('#confirmDiscardOverlay');
- }
- return this._overlays.confirmDiscard;
- }
-
- _computeShowHideIcon(collapsed) {
- return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
- }
-
- _calculateActionstoShow(showActions, isRobotComment) {
- // Polymer 2: check for undefined
- if ([showActions, isRobotComment].some(arg => arg === undefined)) {
- return;
- }
-
- this._showHumanActions = showActions && !isRobotComment;
- this._showRobotActions = showActions && isRobotComment;
- }
-
- _isRobotComment(comment) {
- this.isRobotComment = !!comment.robot_id;
- }
-
- isOnParent() {
- return this.side === 'PARENT';
- }
-
- _getIsAdmin() {
- return this.$.restAPI.getIsAdmin();
- }
-
- /**
- * @param {*=} opt_comment
- */
- save(opt_comment) {
- let comment = opt_comment;
- if (!comment) {
- comment = this.comment;
- }
-
- this.set('comment.message', this._messageText);
- this.editing = false;
- this.disabled = true;
-
- if (!this._messageText) {
- return this._discardDraft();
- }
-
- this._xhrPromise = this._saveDraft(comment).then(response => {
- this.disabled = false;
- if (!response.ok) { return response; }
-
- this._eraseDraftComment();
- return this.$.restAPI.getResponseObject(response).then(obj => {
- const resComment = obj;
- resComment.__draft = true;
- // Maintain the ephemeral draft ID for identification by other
- // elements.
- if (this.comment.__draftID) {
- resComment.__draftID = this.comment.__draftID;
- }
- resComment.__commentSide = this.commentSide;
- this.comment = resComment;
- this._fireSave();
- return obj;
+ this._eraseDraftComment();
+ return this.$.restAPI.getResponseObject(response).then(obj => {
+ const resComment = obj;
+ resComment.__draft = true;
+ // Maintain the ephemeral draft ID for identification by other
+ // elements.
+ if (this.comment.__draftID) {
+ resComment.__draftID = this.comment.__draftID;
+ }
+ resComment.__commentSide = this.commentSide;
+ this.comment = resComment;
+ this._fireSave();
+ return obj;
+ });
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
});
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
- return this._xhrPromise;
+ return this._xhrPromise;
+ }
+
+ _eraseDraftComment() {
+ // Prevents a race condition in which removing the draft comment occurs
+ // prior to it being saved.
+ this.cancelDebouncer('store');
+
+ this.$.storage.eraseDraftComment({
+ changeNum: this.changeNum,
+ patchNum: this._getPatchNum(),
+ path: this.comment.path,
+ line: this.comment.line,
+ range: this.comment.range,
+ });
+ }
+
+ _commentChanged(comment) {
+ this.editing = !!comment.__editing;
+ this.resolved = !comment.unresolved;
+ if (this.editing) { // It's a new draft/reply, notify.
+ this._fireUpdate();
+ }
+ }
+
+ _computeHasHumanReply() {
+ if (!this.comment || !this.comments) return;
+ // hide please fix button for robot comment that has human reply
+ this._hasHumanReply = this.comments
+ .some(c => c.in_reply_to && c.in_reply_to === this.comment.id &&
+ !c.robot_id);
+ }
+
+ /**
+ * @param {!Object=} opt_mixin
+ *
+ * @return {!Object}
+ */
+ _getEventPayload(opt_mixin) {
+ return Object.assign({}, opt_mixin, {
+ comment: this.comment,
+ patchNum: this.patchNum,
+ });
+ }
+
+ _fireSave() {
+ this.fire('comment-save', this._getEventPayload());
+ }
+
+ _fireUpdate() {
+ this.debounce('fire-update', () => {
+ this.fire('comment-update', this._getEventPayload());
+ });
+ }
+
+ _draftChanged(draft) {
+ this.$.container.classList.toggle('draft', draft);
+ }
+
+ _editingChanged(editing, previousValue) {
+ // Polymer 2: observer fires when at least one property is defined.
+ // Do nothing to prevent comment.__editing being overwritten
+ // if previousValue is undefined
+ if (previousValue === undefined) return;
+
+ this.$.container.classList.toggle('editing', editing);
+ if (this.comment && this.comment.id) {
+ this.shadowRoot.querySelector('.cancel').hidden = !editing;
+ }
+ if (this.comment) {
+ this.comment.__editing = this.editing;
+ }
+ if (editing != !!previousValue) {
+ // To prevent event firing on comment creation.
+ this._fireUpdate();
+ }
+ if (editing) {
+ this.async(() => {
+ flush();
+ this.textarea && this.textarea.putCursorAtEnd();
+ }, 1);
+ }
+ }
+
+ _computeDeleteButtonClass(isAdmin, draft) {
+ return isAdmin && !draft ? 'showDeleteButtons' : '';
+ }
+
+ _computeSaveDisabled(draft, comment, resolved) {
+ // If resolved state has changed and a msg exists, save should be enabled.
+ if (!comment || comment.unresolved === resolved && draft) {
+ return false;
+ }
+ return !draft || draft.trim() === '';
+ }
+
+ _handleSaveKey(e) {
+ if (!this._computeSaveDisabled(this._messageText, this.comment,
+ this.resolved)) {
+ e.preventDefault();
+ this._handleSave(e);
+ }
+ }
+
+ _handleEsc(e) {
+ if (!this._messageText.length) {
+ e.preventDefault();
+ this._handleCancel(e);
+ }
+ }
+
+ _handleToggleCollapsed() {
+ this.collapsed = !this.collapsed;
+ }
+
+ _toggleCollapseClass(collapsed) {
+ if (collapsed) {
+ this.$.container.classList.add('collapsed');
+ } else {
+ this.$.container.classList.remove('collapsed');
+ }
+ }
+
+ _commentMessageChanged(message) {
+ this._messageText = message || '';
+ }
+
+ _messageTextChanged(newValue, oldValue) {
+ if (!this.comment || (this.comment && this.comment.id)) {
+ return;
}
- _eraseDraftComment() {
- // Prevents a race condition in which removing the draft comment occurs
- // prior to it being saved.
- this.cancelDebouncer('store');
-
- this.$.storage.eraseDraftComment({
+ this.debounce('store', () => {
+ const message = this._messageText;
+ const commentLocation = {
changeNum: this.changeNum,
patchNum: this._getPatchNum(),
path: this.comment.path,
line: this.comment.line,
range: this.comment.range,
- });
- }
+ };
- _commentChanged(comment) {
- this.editing = !!comment.__editing;
- this.resolved = !comment.unresolved;
- if (this.editing) { // It's a new draft/reply, notify.
- this._fireUpdate();
- }
- }
-
- _computeHasHumanReply() {
- if (!this.comment || !this.comments) return;
- // hide please fix button for robot comment that has human reply
- this._hasHumanReply = this.comments
- .some(c => c.in_reply_to && c.in_reply_to === this.comment.id &&
- !c.robot_id);
- }
-
- /**
- * @param {!Object=} opt_mixin
- *
- * @return {!Object}
- */
- _getEventPayload(opt_mixin) {
- return Object.assign({}, opt_mixin, {
- comment: this.comment,
- patchNum: this.patchNum,
- });
- }
-
- _fireSave() {
- this.fire('comment-save', this._getEventPayload());
- }
-
- _fireUpdate() {
- this.debounce('fire-update', () => {
- this.fire('comment-update', this._getEventPayload());
- });
- }
-
- _draftChanged(draft) {
- this.$.container.classList.toggle('draft', draft);
- }
-
- _editingChanged(editing, previousValue) {
- // Polymer 2: observer fires when at least one property is defined.
- // Do nothing to prevent comment.__editing being overwritten
- // if previousValue is undefined
- if (previousValue === undefined) return;
-
- this.$.container.classList.toggle('editing', editing);
- if (this.comment && this.comment.id) {
- this.shadowRoot.querySelector('.cancel').hidden = !editing;
- }
- if (this.comment) {
- this.comment.__editing = this.editing;
- }
- if (editing != !!previousValue) {
- // To prevent event firing on comment creation.
- this._fireUpdate();
- }
- if (editing) {
- this.async(() => {
- Polymer.dom.flush();
- this.textarea && this.textarea.putCursorAtEnd();
- }, 1);
- }
- }
-
- _computeDeleteButtonClass(isAdmin, draft) {
- return isAdmin && !draft ? 'showDeleteButtons' : '';
- }
-
- _computeSaveDisabled(draft, comment, resolved) {
- // If resolved state has changed and a msg exists, save should be enabled.
- if (!comment || comment.unresolved === resolved && draft) {
- return false;
- }
- return !draft || draft.trim() === '';
- }
-
- _handleSaveKey(e) {
- if (!this._computeSaveDisabled(this._messageText, this.comment,
- this.resolved)) {
- e.preventDefault();
- this._handleSave(e);
- }
- }
-
- _handleEsc(e) {
- if (!this._messageText.length) {
- e.preventDefault();
- this._handleCancel(e);
- }
- }
-
- _handleToggleCollapsed() {
- this.collapsed = !this.collapsed;
- }
-
- _toggleCollapseClass(collapsed) {
- if (collapsed) {
- this.$.container.classList.add('collapsed');
+ if ((!this._messageText || !this._messageText.length) && oldValue) {
+ // If the draft has been modified to be empty, then erase the storage
+ // entry.
+ this.$.storage.eraseDraftComment(commentLocation);
} else {
- this.$.container.classList.remove('collapsed');
+ this.$.storage.setDraftComment(commentLocation, message);
}
+ }, STORAGE_DEBOUNCE_INTERVAL);
+ }
+
+ _handleAnchorClick(e) {
+ e.preventDefault();
+ if (!this.comment.line) {
+ return;
+ }
+ this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ number: this.comment.line || FILE,
+ side: this.side,
+ },
+ }));
+ }
+
+ _handleEdit(e) {
+ e.preventDefault();
+ this._messageText = this.comment.message;
+ this.editing = true;
+ this.$.reporting.recordDraftInteraction();
+ }
+
+ _handleSave(e) {
+ e.preventDefault();
+
+ // Ignore saves started while already saving.
+ if (this.disabled) {
+ return;
+ }
+ const timingLabel = this.comment.id ?
+ REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
+ const timer = this.$.reporting.getTimer(timingLabel);
+ this.set('comment.__editing', false);
+ return this.save().then(() => { timer.end(); });
+ }
+
+ _handleCancel(e) {
+ e.preventDefault();
+
+ if (!this.comment.message ||
+ this.comment.message.trim().length === 0 ||
+ !this.comment.id) {
+ this._fireDiscard();
+ return;
+ }
+ this._messageText = this.comment.message;
+ this.editing = false;
+ }
+
+ _fireDiscard() {
+ this.cancelDebouncer('fire-update');
+ this.fire('comment-discard', this._getEventPayload());
+ }
+
+ _handleFix() {
+ this.dispatchEvent(new CustomEvent('create-fix-comment', {
+ bubbles: true,
+ composed: true,
+ detail: this._getEventPayload(),
+ }));
+ }
+
+ _handleShowFix() {
+ this.dispatchEvent(new CustomEvent('open-fix-preview', {
+ bubbles: true,
+ composed: true,
+ detail: this._getEventPayload(),
+ }));
+ }
+
+ _hasNoFix(comment) {
+ return !comment || !comment.fix_suggestions;
+ }
+
+ _handleDiscard(e) {
+ e.preventDefault();
+ this.$.reporting.recordDraftInteraction();
+
+ if (!this._messageText) {
+ this._discardDraft();
+ return;
}
- _commentMessageChanged(message) {
- this._messageText = message || '';
+ this._openOverlay(this.confirmDiscardOverlay).then(() => {
+ this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
+ .resetFocus();
+ });
+ }
+
+ _handleConfirmDiscard(e) {
+ e.preventDefault();
+ const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
+ this._closeConfirmDiscardOverlay();
+ return this._discardDraft().then(() => { timer.end(); });
+ }
+
+ _discardDraft() {
+ if (!this.comment.__draft) {
+ throw Error('Cannot discard a non-draft comment.');
+ }
+ this.discarding = true;
+ this.editing = false;
+ this.disabled = true;
+ this._eraseDraftComment();
+
+ if (!this.comment.id) {
+ this.disabled = false;
+ this._fireDiscard();
+ return;
}
- _messageTextChanged(newValue, oldValue) {
- if (!this.comment || (this.comment && this.comment.id)) {
- return;
+ this._xhrPromise = this._deleteDraft(this.comment).then(response => {
+ this.disabled = false;
+ if (!response.ok) {
+ this.discarding = false;
+ return response;
}
- this.debounce('store', () => {
- const message = this._messageText;
- const commentLocation = {
- changeNum: this.changeNum,
- patchNum: this._getPatchNum(),
- path: this.comment.path,
- line: this.comment.line,
- range: this.comment.range,
- };
+ this._fireDiscard();
+ })
+ .catch(err => {
+ this.disabled = false;
+ throw err;
+ });
- if ((!this._messageText || !this._messageText.length) && oldValue) {
- // If the draft has been modified to be empty, then erase the storage
- // entry.
- this.$.storage.eraseDraftComment(commentLocation);
- } else {
- this.$.storage.setDraftComment(commentLocation, message);
- }
- }, STORAGE_DEBOUNCE_INTERVAL);
+ return this._xhrPromise;
+ }
+
+ _closeConfirmDiscardOverlay() {
+ this._closeOverlay(this.confirmDiscardOverlay);
+ }
+
+ _getSavingMessage(numPending) {
+ if (numPending === 0) {
+ return SAVED_MESSAGE;
}
+ return [
+ SAVING_MESSAGE,
+ numPending,
+ numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
+ ].join(' ');
+ }
- _handleAnchorClick(e) {
- e.preventDefault();
- if (!this.comment.line) {
- return;
+ _showStartRequest() {
+ const numPending = ++this._numPendingDraftRequests.number;
+ this._updateRequestToast(numPending);
+ }
+
+ _showEndRequest() {
+ const numPending = --this._numPendingDraftRequests.number;
+ this._updateRequestToast(numPending);
+ }
+
+ _handleFailedDraftRequest() {
+ this._numPendingDraftRequests.number--;
+
+ // Cancel the debouncer so that error toasts from the error-manager will
+ // not be overridden.
+ this.cancelDebouncer('draft-toast');
+ }
+
+ _updateRequestToast(numPending) {
+ const message = this._getSavingMessage(numPending);
+ this.debounce('draft-toast', () => {
+ // Note: the event is fired on the body rather than this element because
+ // this element may not be attached by the time this executes, in which
+ // case the event would not bubble.
+ document.body.dispatchEvent(new CustomEvent(
+ 'show-alert', {detail: {message}, bubbles: true, composed: true}));
+ }, TOAST_DEBOUNCE_INTERVAL);
+ }
+
+ _saveDraft(draft) {
+ this._showStartRequest();
+ return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
+ .then(result => {
+ if (result.ok) {
+ this._showEndRequest();
+ } else {
+ this._handleFailedDraftRequest();
+ }
+ return result;
+ });
+ }
+
+ _deleteDraft(draft) {
+ this._showStartRequest();
+ return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
+ draft).then(result => {
+ if (result.ok) {
+ this._showEndRequest();
+ } else {
+ this._handleFailedDraftRequest();
}
- this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {
- number: this.comment.line || FILE,
- side: this.side,
- },
- }));
+ return result;
+ });
+ }
+
+ _getPatchNum() {
+ return this.isOnParent() ? 'PARENT' : this.patchNum;
+ }
+
+ _loadLocalDraft(changeNum, patchNum, comment) {
+ // Polymer 2: check for undefined
+ if ([changeNum, patchNum, comment].some(arg => arg === undefined)) {
+ return;
}
- _handleEdit(e) {
- e.preventDefault();
- this._messageText = this.comment.message;
- this.editing = true;
- this.$.reporting.recordDraftInteraction();
+ // Only apply local drafts to comments that haven't been saved
+ // remotely, and haven't been given a default message already.
+ //
+ // Don't get local draft if there is another comment that is currently
+ // in an editing state.
+ if (!comment || comment.id || comment.message || comment.__otherEditing) {
+ delete comment.__otherEditing;
+ return;
}
- _handleSave(e) {
- e.preventDefault();
+ const draft = this.$.storage.getDraftComment({
+ changeNum,
+ patchNum: this._getPatchNum(),
+ path: comment.path,
+ line: comment.line,
+ range: comment.range,
+ });
- // Ignore saves started while already saving.
- if (this.disabled) {
- return;
- }
- const timingLabel = this.comment.id ?
- REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
- const timer = this.$.reporting.getTimer(timingLabel);
- this.set('comment.__editing', false);
- return this.save().then(() => { timer.end(); });
- }
-
- _handleCancel(e) {
- e.preventDefault();
-
- if (!this.comment.message ||
- this.comment.message.trim().length === 0 ||
- !this.comment.id) {
- this._fireDiscard();
- return;
- }
- this._messageText = this.comment.message;
- this.editing = false;
- }
-
- _fireDiscard() {
- this.cancelDebouncer('fire-update');
- this.fire('comment-discard', this._getEventPayload());
- }
-
- _handleFix() {
- this.dispatchEvent(new CustomEvent('create-fix-comment', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- }));
- }
-
- _handleShowFix() {
- this.dispatchEvent(new CustomEvent('open-fix-preview', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- }));
- }
-
- _hasNoFix(comment) {
- return !comment || !comment.fix_suggestions;
- }
-
- _handleDiscard(e) {
- e.preventDefault();
- this.$.reporting.recordDraftInteraction();
-
- if (!this._messageText) {
- this._discardDraft();
- return;
- }
-
- this._openOverlay(this.confirmDiscardOverlay).then(() => {
- this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
- .resetFocus();
- });
- }
-
- _handleConfirmDiscard(e) {
- e.preventDefault();
- const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
- this._closeConfirmDiscardOverlay();
- return this._discardDraft().then(() => { timer.end(); });
- }
-
- _discardDraft() {
- if (!this.comment.__draft) {
- throw Error('Cannot discard a non-draft comment.');
- }
- this.discarding = true;
- this.editing = false;
- this.disabled = true;
- this._eraseDraftComment();
-
- if (!this.comment.id) {
- this.disabled = false;
- this._fireDiscard();
- return;
- }
-
- this._xhrPromise = this._deleteDraft(this.comment).then(response => {
- this.disabled = false;
- if (!response.ok) {
- this.discarding = false;
- return response;
- }
-
- this._fireDiscard();
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
-
- return this._xhrPromise;
- }
-
- _closeConfirmDiscardOverlay() {
- this._closeOverlay(this.confirmDiscardOverlay);
- }
-
- _getSavingMessage(numPending) {
- if (numPending === 0) {
- return SAVED_MESSAGE;
- }
- return [
- SAVING_MESSAGE,
- numPending,
- numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
- ].join(' ');
- }
-
- _showStartRequest() {
- const numPending = ++this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _showEndRequest() {
- const numPending = --this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _handleFailedDraftRequest() {
- this._numPendingDraftRequests.number--;
-
- // Cancel the debouncer so that error toasts from the error-manager will
- // not be overridden.
- this.cancelDebouncer('draft-toast');
- }
-
- _updateRequestToast(numPending) {
- const message = this._getSavingMessage(numPending);
- this.debounce('draft-toast', () => {
- // Note: the event is fired on the body rather than this element because
- // this element may not be attached by the time this executes, in which
- // case the event would not bubble.
- document.body.dispatchEvent(new CustomEvent(
- 'show-alert', {detail: {message}, bubbles: true, composed: true}));
- }, TOAST_DEBOUNCE_INTERVAL);
- }
-
- _saveDraft(draft) {
- this._showStartRequest();
- return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
- .then(result => {
- if (result.ok) {
- this._showEndRequest();
- } else {
- this._handleFailedDraftRequest();
- }
- return result;
- });
- }
-
- _deleteDraft(draft) {
- this._showStartRequest();
- return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
- draft).then(result => {
- if (result.ok) {
- this._showEndRequest();
- } else {
- this._handleFailedDraftRequest();
- }
- return result;
- });
- }
-
- _getPatchNum() {
- return this.isOnParent() ? 'PARENT' : this.patchNum;
- }
-
- _loadLocalDraft(changeNum, patchNum, comment) {
- // Polymer 2: check for undefined
- if ([changeNum, patchNum, comment].some(arg => arg === undefined)) {
- return;
- }
-
- // Only apply local drafts to comments that haven't been saved
- // remotely, and haven't been given a default message already.
- //
- // Don't get local draft if there is another comment that is currently
- // in an editing state.
- if (!comment || comment.id || comment.message || comment.__otherEditing) {
- delete comment.__otherEditing;
- return;
- }
-
- const draft = this.$.storage.getDraftComment({
- changeNum,
- patchNum: this._getPatchNum(),
- path: comment.path,
- line: comment.line,
- range: comment.range,
- });
-
- if (draft) {
- this.set('comment.message', draft.message);
- }
- }
-
- _handleToggleResolved() {
- this.$.reporting.recordDraftInteraction();
- this.resolved = !this.resolved;
- // Modify payload instead of this.comment, as this.comment is passed from
- // the parent by ref.
- const payload = this._getEventPayload();
- payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
- this.fire('comment-update', payload);
- if (!this.editing) {
- // Save the resolved state immediately.
- this.save(payload.comment);
- }
- }
-
- _handleCommentDelete() {
- this._openOverlay(this.confirmDeleteOverlay);
- }
-
- _handleCancelDeleteComment() {
- this._closeOverlay(this.confirmDeleteOverlay);
- }
-
- _openOverlay(overlay) {
- Polymer.dom(Gerrit.getRootElement()).appendChild(overlay);
- return overlay.open();
- }
-
- _computeAuthorName(comment) {
- if (!comment) return '';
- if (comment.robot_id) {
- return comment.robot_id;
- }
- return comment.author && comment.author.name;
- }
-
- _computeHideRunDetails(comment, collapsed) {
- if (!comment) return true;
- return !(comment.robot_id && comment.url && !collapsed);
- }
-
- _closeOverlay(overlay) {
- Polymer.dom(Gerrit.getRootElement()).removeChild(overlay);
- overlay.close();
- }
-
- _handleConfirmDeleteComment() {
- const dialog =
- this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
- this.$.restAPI.deleteComment(
- this.changeNum, this.patchNum, this.comment.id, dialog.message)
- .then(newComment => {
- this._handleCancelDeleteComment();
- this.comment = newComment;
- });
+ if (draft) {
+ this.set('comment.message', draft.message);
}
}
- customElements.define(GrComment.is, GrComment);
-})();
+ _handleToggleResolved() {
+ this.$.reporting.recordDraftInteraction();
+ this.resolved = !this.resolved;
+ // Modify payload instead of this.comment, as this.comment is passed from
+ // the parent by ref.
+ const payload = this._getEventPayload();
+ payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
+ this.fire('comment-update', payload);
+ if (!this.editing) {
+ // Save the resolved state immediately.
+ this.save(payload.comment);
+ }
+ }
+
+ _handleCommentDelete() {
+ this._openOverlay(this.confirmDeleteOverlay);
+ }
+
+ _handleCancelDeleteComment() {
+ this._closeOverlay(this.confirmDeleteOverlay);
+ }
+
+ _openOverlay(overlay) {
+ dom(Gerrit.getRootElement()).appendChild(overlay);
+ return overlay.open();
+ }
+
+ _computeAuthorName(comment) {
+ if (!comment) return '';
+ if (comment.robot_id) {
+ return comment.robot_id;
+ }
+ return comment.author && comment.author.name;
+ }
+
+ _computeHideRunDetails(comment, collapsed) {
+ if (!comment) return true;
+ return !(comment.robot_id && comment.url && !collapsed);
+ }
+
+ _closeOverlay(overlay) {
+ dom(Gerrit.getRootElement()).removeChild(overlay);
+ overlay.close();
+ }
+
+ _handleConfirmDeleteComment() {
+ const dialog =
+ this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
+ this.$.restAPI.deleteComment(
+ this.changeNum, this.patchNum, this.comment.id, dialog.message)
+ .then(newComment => {
+ this._handleCancelDeleteComment();
+ this.comment = newComment;
+ });
+ }
+}
+
+customElements.define(GrComment.is, GrComment);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
index 18ffc0e..4a0f388 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -1,43 +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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html">
-<script src="../../../scripts/rootElement.js"></script>
-
-<dom-module id="gr-comment">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -257,144 +236,85 @@
<div class="headerLeft">
<span class="authorName">[[_computeAuthorName(comment)]]</span>
<span class="draftLabel">DRAFT</span>
- <gr-tooltip-content class="draftTooltip"
- has-tooltip
- title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key."
- max-width="20em"
- show-icon></gr-tooltip-content>
+ <gr-tooltip-content class="draftTooltip" has-tooltip="" title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key." max-width="20em" show-icon=""></gr-tooltip-content>
</div>
<div class="headerMiddle">
<span class="collapsedContent">[[comment.message]]</span>
</div>
- <div hidden$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message">
+ <div hidden\$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message">
<div class="runIdInformation">
- <a class="robotRunLink" href$="[[comment.url]]">
+ <a class="robotRunLink" href\$="[[comment.url]]">
<span class="robotRun link">Run Details</span>
</a>
</div>
</div>
- <gr-button
- id="deleteBtn"
- link
- class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
- hidden$="[[isRobotComment]]"
- on-click="_handleCommentDelete">
+ <gr-button id="deleteBtn" link="" class\$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]" hidden\$="[[isRobotComment]]" on-click="_handleCommentDelete">
<iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
</gr-button>
<span class="date" on-click="_handleAnchorClick">
- <gr-date-formatter
- has-tooltip
- date-str="[[comment.updated]]"></gr-date-formatter>
+ <gr-date-formatter has-tooltip="" date-str="[[comment.updated]]"></gr-date-formatter>
</span>
<div class="show-hide">
<label class="show-hide">
- <input type="checkbox" class="show-hide"
- checked$="[[collapsed]]"
- on-change="_handleToggleCollapsed">
- <iron-icon
- id="icon"
- icon="[[_computeShowHideIcon(collapsed)]]">
+ <input type="checkbox" class="show-hide" checked\$="[[collapsed]]" on-change="_handleToggleCollapsed">
+ <iron-icon id="icon" icon="[[_computeShowHideIcon(collapsed)]]">
</iron-icon>
</label>
</div>
</div>
<div class="body">
<template is="dom-if" if="[[isRobotComment]]">
- <div class="robotId" hidden$="[[collapsed]]">
+ <div class="robotId" hidden\$="[[collapsed]]">
[[comment.author.name]]
</div>
</template>
<template is="dom-if" if="[[editing]]">
- <gr-textarea
- id="editTextarea"
- class="editMessage"
- autocomplete="on"
- code
- disabled="{{disabled}}"
- rows="4"
- text="{{_messageText}}"></gr-textarea>
+ <gr-textarea id="editTextarea" class="editMessage" autocomplete="on" code="" disabled="{{disabled}}" rows="4" text="{{_messageText}}"></gr-textarea>
<template is="dom-if" if="[[_computeVisibilityOfTip(_showRespectfulTip, _respectfulTipDismissed)]]">
<div class="respectfulReviewTip">
<div>
- <gr-tooltip-content
- has-tooltip
- title="Tips for respectful code reviews.">
+ <gr-tooltip-content has-tooltip="" title="Tips for respectful code reviews.">
<iron-icon class="pointer" icon="gr-icons:lightbulb-outline"></iron-icon>
</gr-tooltip-content>
[[_respectfulReviewTip]]
</div>
<div>
- <a
- tabIndex="-1"
- on-click="_onRespectfulReadMoreClick"
- href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html"
- target="_blank">
+ <a tabindex="-1" on-click="_onRespectfulReadMoreClick" href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html" target="_blank">
Read more
</a>
- <iron-icon
- class="close pointer"
- on-click="_dismissRespectfulTip"
- icon="gr-icons:close"></iron-icon>
+ <iron-icon class="close pointer" on-click="_dismissRespectfulTip" icon="gr-icons:close"></iron-icon>
</div>
</div>
</template>
</template>
<!--The message class is needed to ensure selectability from
gr-diff-selection.-->
- <gr-formatted-text class="message"
- content="[[comment.message]]"
- no-trailing-margin="[[!comment.__draft]]"
- config="[[projectConfig.commentlinks]]"></gr-formatted-text>
- <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
+ <gr-formatted-text class="message" content="[[comment.message]]" no-trailing-margin="[[!comment.__draft]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+ <div class="actions humanActions" hidden\$="[[!_showHumanActions]]">
<div class="action resolve hideOnPublished">
<label>
- <input type="checkbox"
- id="resolvedCheckbox"
- checked="[[resolved]]"
- on-change="_handleToggleResolved">
+ <input type="checkbox" id="resolvedCheckbox" checked="[[resolved]]" on-change="_handleToggleResolved">
Resolved
</label>
</div>
<div class="rightActions">
- <gr-button
- link
- class="action cancel hideOnPublished"
- on-click="_handleCancel">Cancel</gr-button>
- <gr-button
- link
- class="action discard hideOnPublished"
- on-click="_handleDiscard">Discard</gr-button>
- <gr-button
- link
- class="action edit hideOnPublished"
- on-click="_handleEdit">Edit</gr-button>
- <gr-button
- link
- disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
- class="action save hideOnPublished"
- on-click="_handleSave">Save</gr-button>
+ <gr-button link="" class="action cancel hideOnPublished" on-click="_handleCancel">Cancel</gr-button>
+ <gr-button link="" class="action discard hideOnPublished" on-click="_handleDiscard">Discard</gr-button>
+ <gr-button link="" class="action edit hideOnPublished" on-click="_handleEdit">Edit</gr-button>
+ <gr-button link="" disabled\$="[[_computeSaveDisabled(_messageText, comment, resolved)]]" class="action save hideOnPublished" on-click="_handleSave">Save</gr-button>
</div>
</div>
- <div class="robotActions" hidden$="[[!_showRobotActions]]">
+ <div class="robotActions" hidden\$="[[!_showRobotActions]]">
<template is="dom-if" if="[[isRobotComment]]">
<gr-endpoint-decorator name="robot-comment-controls">
<gr-endpoint-param name="comment" value="[[comment]]">
</gr-endpoint-param>
</gr-endpoint-decorator>
- <gr-button
- link
- secondary
- class="action show-fix"
- hidden$="[[_hasNoFix(comment)]]"
- on-click="_handleShowFix">
+ <gr-button link="" secondary="" class="action show-fix" hidden\$="[[_hasNoFix(comment)]]" on-click="_handleShowFix">
Show Fix
</gr-button>
<template is="dom-if" if="[[!_hasHumanReply]]">
- <gr-button
- link
- class="action fix"
- on-click="_handleFix"
- disabled="[[robotButtonDisabled]]">
+ <gr-button link="" class="action fix" on-click="_handleFix" disabled="[[robotButtonDisabled]]">
Please Fix
</gr-button>
</template>
@@ -403,19 +323,12 @@
</div>
</div>
<template is="dom-if" if="[[_enableOverlay]]">
- <gr-overlay id="confirmDeleteOverlay" with-backdrop>
- <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
- on-confirm="_handleConfirmDeleteComment"
- on-cancel="_handleCancelDeleteComment">
+ <gr-overlay id="confirmDeleteOverlay" with-backdrop="">
+ <gr-confirm-delete-comment-dialog id="confirmDeleteComment" on-confirm="_handleConfirmDeleteComment" on-cancel="_handleCancelDeleteComment">
</gr-confirm-delete-comment-dialog>
</gr-overlay>
- <gr-overlay id="confirmDiscardOverlay" with-backdrop>
- <gr-dialog
- id="confirmDiscardDialog"
- confirm-label="Discard"
- confirm-on-enter
- on-confirm="_handleConfirmDiscard"
- on-cancel="_closeConfirmDiscardOverlay">
+ <gr-overlay id="confirmDiscardOverlay" with-backdrop="">
+ <gr-dialog id="confirmDiscardDialog" confirm-label="Discard" confirm-on-enter="" on-confirm="_handleConfirmDiscard" on-cancel="_closeConfirmDiscardOverlay">
<div class="header" slot="header">
Discard comment
</div>
@@ -428,6 +341,4 @@
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-storage id="storage"></gr-storage>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-comment.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 5e9d37a..96d497e 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_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-comment</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="../../../scripts/util.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>
+<script type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-comment.html">
+<script type="module" src="./gr-comment.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -44,1119 +50,1204 @@
</template>
</test-fixture>
-<script>
- function isVisible(el) {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') !== 'none';
- }
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment.js';
+function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+}
- suite('gr-comment tests', async () => {
- await readyToTest();
+suite('gr-comment tests', () => {
+ suite('basic tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ });
+ element = fixture('basic');
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ id: 'baf0414d_60047215',
+ line: 5,
+ message: 'is this a crossover episode!?',
+ updated: '2015-12-08 19:48:33.843000000',
+ };
+ sandbox = sinon.sandbox.create();
+ });
- suite('basic tests', () => {
- let element;
- let sandbox;
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('collapsible comments', () => {
+ // When a comment (not draft) is loaded, it should be collapsed
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+
+ // The header middle content is only visible when comments are collapsed.
+ // It shows the message in a condensed way, and limits to a single line.
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When the header row is clicked, the comment should expand
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
+ test('clicking on date link fires event', () => {
+ element.side = 'PARENT';
+ const stub = sinon.stub();
+ element.addEventListener('comment-anchor-tap', stub);
+ const dateEl = element.shadowRoot
+ .querySelector('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+
+ assert.isTrue(stub.called);
+ assert.deepEqual(stub.lastCall.args[0].detail,
+ {side: element.side, number: element.comment.line});
+ });
+
+ test('message is not retrieved from storage when other edits', done => {
+ const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+ const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+ element.changeNum = 1;
+ element.patchNum = 1;
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ line: 5,
+ __otherEditing: true,
+ };
+ flush(() => {
+ assert.isTrue(loadSpy.called);
+ assert.isFalse(storageStub.called);
+ done();
+ });
+ });
+
+ test('message is retrieved from storage when no other edits', done => {
+ const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+ const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+ element.changeNum = 1;
+ element.patchNum = 1;
+ element.comment = {
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com',
+ },
+ line: 5,
+ };
+ flush(() => {
+ assert.isTrue(loadSpy.called);
+ assert.isTrue(storageStub.called);
+ done();
+ });
+ });
+
+ test('_getPatchNum', () => {
+ element.side = 'PARENT';
+ element.patchNum = 1;
+ assert.equal(element._getPatchNum(), 'PARENT');
+ element.side = 'REVISION';
+ assert.equal(element._getPatchNum(), 1);
+ });
+
+ test('comment expand and collapse', () => {
+ element.collapsed = true;
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ element.collapsed = false;
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is is not visible');
+ });
+
+ suite('while editing', () => {
setup(() => {
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- });
- element = fixture('basic');
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- id: 'baf0414d_60047215',
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000',
- };
- sandbox = sinon.sandbox.create();
+ element.editing = true;
+ element._messageText = 'test';
+ sandbox.stub(element, '_handleCancel');
+ sandbox.stub(element, '_handleSave');
+ flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
- });
-
- test('collapsible comments', () => {
- // When a comment (not draft) is loaded, it should be collapsed
- assert.isTrue(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
-
- // The header middle content is only visible when comments are collapsed.
- // It shows the message in a condensed way, and limits to a single line.
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- // When the header row is clicked, the comment should expand
- MockInteractions.tap(element.$.header);
- assert.isFalse(element.collapsed);
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is not visible');
- });
-
- test('clicking on date link fires event', () => {
- element.side = 'PARENT';
- const stub = sinon.stub();
- element.addEventListener('comment-anchor-tap', stub);
- const dateEl = element.shadowRoot
- .querySelector('.date');
- assert.ok(dateEl);
- MockInteractions.tap(dateEl);
-
- assert.isTrue(stub.called);
- assert.deepEqual(stub.lastCall.args[0].detail,
- {side: element.side, number: element.comment.line});
- });
-
- test('message is not retrieved from storage when other edits', done => {
- const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
- const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
- element.changeNum = 1;
- element.patchNum = 1;
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- line: 5,
- __otherEditing: true,
- };
- flush(() => {
- assert.isTrue(loadSpy.called);
- assert.isFalse(storageStub.called);
- done();
- });
- });
-
- test('message is retrieved from storage when no other edits', done => {
- const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
- const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
- element.changeNum = 1;
- element.patchNum = 1;
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com',
- },
- line: 5,
- };
- flush(() => {
- assert.isTrue(loadSpy.called);
- assert.isTrue(storageStub.called);
- done();
- });
- });
-
- test('_getPatchNum', () => {
- element.side = 'PARENT';
- element.patchNum = 1;
- assert.equal(element._getPatchNum(), 'PARENT');
- element.side = 'REVISION';
- assert.equal(element._getPatchNum(), 1);
- });
-
- test('comment expand and collapse', () => {
- element.collapsed = true;
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- element.collapsed = false;
- assert.isFalse(element.collapsed);
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is is not visible');
- });
-
- suite('while editing', () => {
+ suite('when text is empty', () => {
setup(() => {
- element.editing = true;
- element._messageText = 'test';
- sandbox.stub(element, '_handleCancel');
- sandbox.stub(element, '_handleSave');
- flushAsynchronousOperations();
+ element._messageText = '';
+ element.comment = {};
});
- suite('when text is empty', () => {
- setup(() => {
- element._messageText = '';
- element.comment = {};
- });
-
- test('esc closes comment when text is empty', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 27); // esc
- assert.isTrue(element._handleCancel.called);
- });
-
- test('ctrl+enter does not save', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 13, 'ctrl'); // ctrl + enter
- assert.isFalse(element._handleSave.called);
- });
-
- test('meta+enter does not save', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 13, 'meta'); // meta + enter
- assert.isFalse(element._handleSave.called);
- });
-
- test('ctrl+s does not save', () => {
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea, 83, 'ctrl'); // ctrl + s
- assert.isFalse(element._handleSave.called);
- });
- });
-
- test('esc does not close comment that has content', () => {
+ test('esc closes comment when text is empty', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 27); // esc
- assert.isFalse(element._handleCancel.called);
+ assert.isTrue(element._handleCancel.called);
});
- test('ctrl+enter saves', () => {
+ test('ctrl+enter does not save', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 13, 'ctrl'); // ctrl + enter
- assert.isTrue(element._handleSave.called);
+ assert.isFalse(element._handleSave.called);
});
- test('meta+enter saves', () => {
+ test('meta+enter does not save', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 13, 'meta'); // meta + enter
- assert.isTrue(element._handleSave.called);
+ assert.isFalse(element._handleSave.called);
});
- test('ctrl+s saves', () => {
+ test('ctrl+s does not save', () => {
MockInteractions.pressAndReleaseKeyOn(
element.textarea, 83, 'ctrl'); // ctrl + s
- assert.isTrue(element._handleSave.called);
- });
- });
- test('delete comment button for non-admins is hidden', () => {
- element._isAdmin = false;
- assert.isFalse(element.shadowRoot
- .querySelector('.action.delete')
- .classList.contains('showDeleteButtons'));
- });
-
- test('delete comment button for admins with draft is hidden', () => {
- element._isAdmin = false;
- element.draft = true;
- assert.isFalse(element.shadowRoot
- .querySelector('.action.delete')
- .classList.contains('showDeleteButtons'));
- });
-
- test('delete comment', done => {
- sandbox.stub(
- element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
- sandbox.spy(element.confirmDeleteOverlay, 'open');
- element.changeNum = 42;
- element.patchNum = 0xDEADBEEF;
- element._isAdmin = true;
- assert.isTrue(element.shadowRoot
- .querySelector('.action.delete')
- .classList.contains('showDeleteButtons'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('.action.delete'));
- flush(() => {
- element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
- const dialog =
- window.confirmDeleteOverlay
- .querySelector('#confirmDeleteComment');
- dialog.message = 'removal reason';
- element._handleConfirmDeleteComment();
- assert.isTrue(element.$.restAPI.deleteComment.calledWith(
- 42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
- done();
- });
+ assert.isFalse(element._handleSave.called);
});
});
- suite('draft update reporting', () => {
- let endStub;
- let getTimerStub;
- let mockEvent;
-
- setup(() => {
- mockEvent = {preventDefault() {}};
- sandbox.stub(element, 'save')
- .returns(Promise.resolve({}));
- sandbox.stub(element, '_discardDraft')
- .returns(Promise.resolve({}));
- endStub = sinon.stub();
- getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
- .returns({end: endStub});
- });
-
- test('create', () => {
- element.comment = {};
- return element._handleSave(mockEvent).then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
- });
- });
-
- test('update', () => {
- element.comment = {id: 'abc_123'};
- return element._handleSave(mockEvent).then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
- });
- });
-
- test('discard', () => {
- element.comment = {id: 'abc_123'};
- sandbox.stub(element, '_closeConfirmDiscardOverlay');
- return element._handleConfirmDiscard(mockEvent).then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
- });
- });
+ test('esc does not close comment that has content', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 27); // esc
+ assert.isFalse(element._handleCancel.called);
});
- test('edit reports interaction', () => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- assert.isTrue(reportStub.calledOnce);
+ test('ctrl+enter saves', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 13, 'ctrl'); // ctrl + enter
+ assert.isTrue(element._handleSave.called);
});
- test('discard reports interaction', () => {
- const reportStub = sandbox.stub(element.$.reporting,
- 'recordDraftInteraction');
- element.draft = true;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.discard'));
- assert.isTrue(reportStub.calledOnce);
+ test('meta+enter saves', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 13, 'meta'); // meta + enter
+ assert.isTrue(element._handleSave.called);
+ });
+
+ test('ctrl+s saves', () => {
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea, 83, 'ctrl'); // ctrl + s
+ assert.isTrue(element._handleSave.called);
+ });
+ });
+ test('delete comment button for non-admins is hidden', () => {
+ element._isAdmin = false;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.action.delete')
+ .classList.contains('showDeleteButtons'));
+ });
+
+ test('delete comment button for admins with draft is hidden', () => {
+ element._isAdmin = false;
+ element.draft = true;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.action.delete')
+ .classList.contains('showDeleteButtons'));
+ });
+
+ test('delete comment', done => {
+ sandbox.stub(
+ element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
+ sandbox.spy(element.confirmDeleteOverlay, 'open');
+ element.changeNum = 42;
+ element.patchNum = 0xDEADBEEF;
+ element._isAdmin = true;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.action.delete')
+ .classList.contains('showDeleteButtons'));
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.action.delete'));
+ flush(() => {
+ element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
+ const dialog =
+ window.confirmDeleteOverlay
+ .querySelector('#confirmDeleteComment');
+ dialog.message = 'removal reason';
+ element._handleConfirmDeleteComment();
+ assert.isTrue(element.$.restAPI.deleteComment.calledWith(
+ 42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
+ done();
+ });
});
});
- suite('gr-comment draft tests', () => {
- let element;
- let sandbox;
+ suite('draft update reporting', () => {
+ let endStub;
+ let getTimerStub;
+ let mockEvent;
setup(() => {
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- saveDiffDraft() {
- return Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(
- ')]}\'\n{' +
- '"id": "baf0414d_40572e03",' +
- '"path": "/path/to/file",' +
- '"line": 5,' +
- '"updated": "2015-12-08 21:52:36.177000000",' +
- '"message": "saved!"' +
- '}'
- );
- },
- });
- },
- removeChangeReviewer() {
- return Promise.resolve({ok: true});
- },
- });
- stub('gr-storage', {
- getDraftComment() { return null; },
- });
- element = fixture('draft');
- element.changeNum = 42;
- element.patchNum = 1;
- element.editing = false;
- element.comment = {
- __commentSide: 'right',
- __draft: true,
- __draftID: 'temp_draft_id',
- path: '/path/to/file',
- line: 5,
- };
- element.commentSide = 'right';
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('button visibility states', () => {
- element.showActions = false;
- assert.isTrue(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.showActions = true;
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.draft = true;
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.edit')), 'edit is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.discard')), 'discard is visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.save')), 'save is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.resolve')), 'resolve is visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.editing = true;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.edit')), 'edit is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.discard')), 'discard not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.save')), 'save is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.resolve')), 'resolve is visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.draft = false;
- element.editing = false;
- flushAsynchronousOperations();
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.edit')), 'edit is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.discard')),
- 'discard is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.save')), 'save is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is not visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- element.comment.id = 'foo';
- element.draft = true;
- element.editing = true;
- flushAsynchronousOperations();
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.cancel')), 'cancel is visible');
- assert.isFalse(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isTrue(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- // Delete button is not hidden by default
- assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);
-
- element.isRobotComment = true;
- element.draft = true;
- assert.isTrue(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isFalse(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- // It is not expected to see Robot comment drafts, but if they appear,
- // they will behave the same as non-drafts.
- element.draft = false;
- assert.isTrue(element.shadowRoot
- .querySelector('.humanActions').hasAttribute('hidden'));
- assert.isFalse(element.shadowRoot
- .querySelector('.robotActions').hasAttribute('hidden'));
-
- // A robot comment with run ID should display plain text.
- element.set(['comment', 'robot_run_id'], 'text');
- element.editing = false;
- element.collapsed = false;
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('.robotRun.link').textContent === 'Run Details');
-
- // A robot comment with run ID and url should display a link.
- element.set(['comment', 'url'], '/path/to/run');
- flushAsynchronousOperations();
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.robotRun.link')).display,
- 'none');
-
- // Delete button is hidden for robot comments
- assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
- });
-
- test('collapsible drafts', () => {
- assert.isTrue(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- MockInteractions.tap(element.$.header);
- assert.isFalse(element.collapsed);
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is is not visible');
-
- // When the edit button is pressed, should still see the actions
- // and also textarea
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- flushAsynchronousOperations();
- assert.isFalse(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isTrue(isVisible(element.textarea), 'textarea is visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is not visible');
-
- // When toggle again, everything should be hidden except for textarea
- // and header middle content should be visible
- MockInteractions.tap(element.$.header);
- assert.isTrue(element.collapsed);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are not visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-textarea')),
- 'textarea is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is visible');
-
- // When toggle again, textarea should remain open in the state it was
- // before
- MockInteractions.tap(element.$.header);
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('gr-formatted-text')),
- 'gr-formatted-text is not visible');
- assert.isTrue(isVisible(element.shadowRoot
- .querySelector('.actions')),
- 'actions are visible');
- assert.isTrue(isVisible(element.textarea), 'textarea is visible');
- assert.isFalse(isVisible(element.shadowRoot
- .querySelector('.collapsedContent')),
- 'header middle content is not visible');
- });
-
- test('robot comment layout', () => {
- const comment = Object.assign({
- robot_id: 'happy_robot_id',
- url: '/robot/comment',
- author: {
- name: 'Happy Robot',
- },
- }, element.comment);
- element.comment = comment;
- element.collapsed = false;
- flushAsynchronousOperations();
-
- let runIdMessage;
- runIdMessage = element.shadowRoot
- .querySelector('.runIdMessage');
- assert.isFalse(runIdMessage.hidden);
-
- const runDetailsLink = element.shadowRoot
- .querySelector('.robotRunLink');
- assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);
-
- const robotServiceName = element.shadowRoot
- .querySelector('.authorName');
- assert.isTrue(robotServiceName.textContent === 'happy_robot_id');
-
- const authorName = element.shadowRoot
- .querySelector('.robotId');
- assert.isTrue(authorName.innerText === 'Happy Robot');
-
- element.collapsed = true;
- flushAsynchronousOperations();
- runIdMessage = element.shadowRoot
- .querySelector('.runIdMessage');
- assert.isTrue(runIdMessage.hidden);
- });
-
- test('draft creation/cancellation', done => {
- assert.isFalse(element.editing);
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- assert.isTrue(element.editing);
-
- element._messageText = '';
- const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
-
- // Save should be disabled on an empty message.
- let disabled = element.shadowRoot
- .querySelector('.save').hasAttribute('disabled');
- assert.isTrue(disabled, 'save button should be disabled.');
- element._messageText = ' ';
- disabled = element.shadowRoot
- .querySelector('.save').hasAttribute('disabled');
- assert.isTrue(disabled, 'save button should be disabled.');
-
- const updateStub = sinon.stub();
- element.addEventListener('comment-update', updateStub);
-
- let numDiscardEvents = 0;
- element.addEventListener('comment-discard', e => {
- numDiscardEvents++;
- assert.isFalse(eraseMessageDraftSpy.called);
- if (numDiscardEvents === 2) {
- assert.isFalse(updateStub.called);
- done();
- }
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('.cancel'));
- element.flushDebouncer('fire-update');
- element._messageText = '';
- flushAsynchronousOperations();
- MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
- });
-
- test('draft discard removes message from storage', done => {
- element._messageText = '';
- const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
- sandbox.stub(element, '_closeConfirmDiscardOverlay');
-
- element.addEventListener('comment-discard', e => {
- assert.isTrue(eraseMessageDraftSpy.called);
- done();
- });
- element._handleConfirmDiscard({preventDefault: sinon.stub()});
- });
-
- test('storage is cleared only after save success', () => {
- element._messageText = 'test';
- const eraseStub = sandbox.stub(element, '_eraseDraftComment');
- sandbox.stub(element.$.restAPI, 'getResponseObject')
+ mockEvent = {preventDefault() {}};
+ sandbox.stub(element, 'save')
.returns(Promise.resolve({}));
+ sandbox.stub(element, '_discardDraft')
+ .returns(Promise.resolve({}));
+ endStub = sinon.stub();
+ getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
+ .returns({end: endStub});
+ });
- sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+ test('create', () => {
+ element.comment = {};
+ return element._handleSave(mockEvent).then(() => {
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
+ });
+ });
- const savePromise = element.save();
- assert.isFalse(eraseStub.called);
- return savePromise.then(() => {
- assert.isFalse(eraseStub.called);
+ test('update', () => {
+ element.comment = {id: 'abc_123'};
+ return element._handleSave(mockEvent).then(() => {
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
+ });
+ });
- element._saveDraft.restore();
- sandbox.stub(element, '_saveDraft')
- .returns(Promise.resolve({ok: true}));
- return element.save().then(() => {
- assert.isTrue(eraseStub.called);
+ test('discard', () => {
+ element.comment = {id: 'abc_123'};
+ sandbox.stub(element, '_closeConfirmDiscardOverlay');
+ return element._handleConfirmDiscard(mockEvent).then(() => {
+ assert.isTrue(endStub.calledOnce);
+ assert.isTrue(getTimerStub.calledOnce);
+ assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
+ });
+ });
+ });
+
+ test('edit reports interaction', () => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ assert.isTrue(reportStub.calledOnce);
+ });
+
+ test('discard reports interaction', () => {
+ const reportStub = sandbox.stub(element.$.reporting,
+ 'recordDraftInteraction');
+ element.draft = true;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.discard'));
+ assert.isTrue(reportStub.calledOnce);
+ });
+ });
+
+ suite('gr-comment draft tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ saveDiffDraft() {
+ return Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(
+ ')]}\'\n{' +
+ '"id": "baf0414d_40572e03",' +
+ '"path": "/path/to/file",' +
+ '"line": 5,' +
+ '"updated": "2015-12-08 21:52:36.177000000",' +
+ '"message": "saved!"' +
+ '}'
+ );
+ },
});
- });
+ },
+ removeChangeReviewer() {
+ return Promise.resolve({ok: true});
+ },
});
-
- test('_computeSaveDisabled', () => {
- const comment = {unresolved: true};
- const msgComment = {message: 'test', unresolved: true};
- assert.equal(element._computeSaveDisabled('', comment, false), true);
- assert.equal(element._computeSaveDisabled('test', comment, false), false);
- assert.equal(element._computeSaveDisabled('', msgComment, false), true);
- assert.equal(
- element._computeSaveDisabled('test', msgComment, false), false);
- assert.equal(
- element._computeSaveDisabled('test2', msgComment, false), false);
- assert.equal(element._computeSaveDisabled('test', comment, true), false);
- assert.equal(element._computeSaveDisabled('', comment, true), true);
- assert.equal(element._computeSaveDisabled('', comment, false), true);
+ stub('gr-storage', {
+ getDraftComment() { return null; },
});
+ element = fixture('draft');
+ element.changeNum = 42;
+ element.patchNum = 1;
+ element.editing = false;
+ element.comment = {
+ __commentSide: 'right',
+ __draft: true,
+ __draftID: 'temp_draft_id',
+ path: '/path/to/file',
+ line: 5,
+ };
+ element.commentSide = 'right';
+ sandbox = sinon.sandbox.create();
+ });
- suite('confirm discard', () => {
- let discardStub;
- let overlayStub;
- let mockEvent;
+ teardown(() => {
+ sandbox.restore();
+ });
- setup(() => {
- discardStub = sandbox.stub(element, '_discardDraft');
- overlayStub = sandbox.stub(element, '_openOverlay')
- .returns(Promise.resolve());
- mockEvent = {preventDefault: sinon.stub()};
- });
+ test('button visibility states', () => {
+ element.showActions = false;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
- test('confirms discard of comments with message text', () => {
- element._messageText = 'test';
- element._handleDiscard(mockEvent);
- assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
- assert.isFalse(discardStub.called);
- });
+ element.showActions = true;
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
- test('no confirmation for comments without message text', () => {
- element._messageText = '';
- element._handleDiscard(mockEvent);
- assert.isFalse(overlayStub.called);
- assert.isTrue(discardStub.calledOnce);
- });
- });
+ element.draft = true;
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.edit')), 'edit is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.discard')), 'discard is visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.resolve')), 'resolve is visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
- test('ctrl+s saves comment', done => {
- const stub = sinon.stub(element, 'save', () => {
- assert.isTrue(stub.called);
- stub.restore();
+ element.editing = true;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.edit')), 'edit is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.discard')), 'discard not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.save')), 'save is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.resolve')), 'resolve is visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ element.draft = false;
+ element.editing = false;
+ flushAsynchronousOperations();
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.edit')), 'edit is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.discard')),
+ 'discard is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is not visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ element.comment.id = 'foo';
+ element.draft = true;
+ element.editing = true;
+ flushAsynchronousOperations();
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.cancel')), 'cancel is visible');
+ assert.isFalse(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ // Delete button is not hidden by default
+ assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);
+
+ element.isRobotComment = true;
+ element.draft = true;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isFalse(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ // It is not expected to see Robot comment drafts, but if they appear,
+ // they will behave the same as non-drafts.
+ element.draft = false;
+ assert.isTrue(element.shadowRoot
+ .querySelector('.humanActions').hasAttribute('hidden'));
+ assert.isFalse(element.shadowRoot
+ .querySelector('.robotActions').hasAttribute('hidden'));
+
+ // A robot comment with run ID should display plain text.
+ element.set(['comment', 'robot_run_id'], 'text');
+ element.editing = false;
+ element.collapsed = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('.robotRun.link').textContent === 'Run Details');
+
+ // A robot comment with run ID and url should display a link.
+ element.set(['comment', 'url'], '/path/to/run');
+ flushAsynchronousOperations();
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.robotRun.link')).display,
+ 'none');
+
+ // Delete button is hidden for robot comments
+ assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
+ });
+
+ test('collapsible drafts', () => {
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isNotOk(element.textarea, 'textarea is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is is not visible');
+
+ // When the edit button is pressed, should still see the actions
+ // and also textarea
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ flushAsynchronousOperations();
+ assert.isFalse(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is not visible');
+
+ // When toggle again, everything should be hidden except for textarea
+ // and header middle content should be visible
+ MockInteractions.tap(element.$.header);
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-textarea')),
+ 'textarea is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When toggle again, textarea should remain open in the state it was
+ // before
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('gr-formatted-text')),
+ 'gr-formatted-text is not visible');
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
+ test('robot comment layout', () => {
+ const comment = Object.assign({
+ robot_id: 'happy_robot_id',
+ url: '/robot/comment',
+ author: {
+ name: 'Happy Robot',
+ },
+ }, element.comment);
+ element.comment = comment;
+ element.collapsed = false;
+ flushAsynchronousOperations();
+
+ let runIdMessage;
+ runIdMessage = element.shadowRoot
+ .querySelector('.runIdMessage');
+ assert.isFalse(runIdMessage.hidden);
+
+ const runDetailsLink = element.shadowRoot
+ .querySelector('.robotRunLink');
+ assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);
+
+ const robotServiceName = element.shadowRoot
+ .querySelector('.authorName');
+ assert.isTrue(robotServiceName.textContent === 'happy_robot_id');
+
+ const authorName = element.shadowRoot
+ .querySelector('.robotId');
+ assert.isTrue(authorName.innerText === 'Happy Robot');
+
+ element.collapsed = true;
+ flushAsynchronousOperations();
+ runIdMessage = element.shadowRoot
+ .querySelector('.runIdMessage');
+ assert.isTrue(runIdMessage.hidden);
+ });
+
+ test('draft creation/cancellation', done => {
+ assert.isFalse(element.editing);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ assert.isTrue(element.editing);
+
+ element._messageText = '';
+ const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+
+ // Save should be disabled on an empty message.
+ let disabled = element.shadowRoot
+ .querySelector('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+ element._messageText = ' ';
+ disabled = element.shadowRoot
+ .querySelector('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+
+ const updateStub = sinon.stub();
+ element.addEventListener('comment-update', updateStub);
+
+ let numDiscardEvents = 0;
+ element.addEventListener('comment-discard', e => {
+ numDiscardEvents++;
+ assert.isFalse(eraseMessageDraftSpy.called);
+ if (numDiscardEvents === 2) {
+ assert.isFalse(updateStub.called);
done();
- return Promise.resolve();
+ }
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.cancel'));
+ element.flushDebouncer('fire-update');
+ element._messageText = '';
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
+ });
+
+ test('draft discard removes message from storage', done => {
+ element._messageText = '';
+ const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+ sandbox.stub(element, '_closeConfirmDiscardOverlay');
+
+ element.addEventListener('comment-discard', e => {
+ assert.isTrue(eraseMessageDraftSpy.called);
+ done();
+ });
+ element._handleConfirmDiscard({preventDefault: sinon.stub()});
+ });
+
+ test('storage is cleared only after save success', () => {
+ element._messageText = 'test';
+ const eraseStub = sandbox.stub(element, '_eraseDraftComment');
+ sandbox.stub(element.$.restAPI, 'getResponseObject')
+ .returns(Promise.resolve({}));
+
+ sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+
+ const savePromise = element.save();
+ assert.isFalse(eraseStub.called);
+ return savePromise.then(() => {
+ assert.isFalse(eraseStub.called);
+
+ element._saveDraft.restore();
+ sandbox.stub(element, '_saveDraft')
+ .returns(Promise.resolve({ok: true}));
+ return element.save().then(() => {
+ assert.isTrue(eraseStub.called);
});
- element._messageText = 'is that the horse from horsing around??';
- element.editing = true;
- flushAsynchronousOperations();
- MockInteractions.pressAndReleaseKeyOn(
- element.textarea.$.textarea.textarea,
- 83, 'ctrl'); // 'ctrl + s'
+ });
+ });
+
+ test('_computeSaveDisabled', () => {
+ const comment = {unresolved: true};
+ const msgComment = {message: 'test', unresolved: true};
+ assert.equal(element._computeSaveDisabled('', comment, false), true);
+ assert.equal(element._computeSaveDisabled('test', comment, false), false);
+ assert.equal(element._computeSaveDisabled('', msgComment, false), true);
+ assert.equal(
+ element._computeSaveDisabled('test', msgComment, false), false);
+ assert.equal(
+ element._computeSaveDisabled('test2', msgComment, false), false);
+ assert.equal(element._computeSaveDisabled('test', comment, true), false);
+ assert.equal(element._computeSaveDisabled('', comment, true), true);
+ assert.equal(element._computeSaveDisabled('', comment, false), true);
+ });
+
+ suite('confirm discard', () => {
+ let discardStub;
+ let overlayStub;
+ let mockEvent;
+
+ setup(() => {
+ discardStub = sandbox.stub(element, '_discardDraft');
+ overlayStub = sandbox.stub(element, '_openOverlay')
+ .returns(Promise.resolve());
+ mockEvent = {preventDefault: sinon.stub()};
});
- test('draft saving/editing', done => {
- const fireStub = sinon.stub(element, 'fire');
- const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+ test('confirms discard of comments with message text', () => {
+ element._messageText = 'test';
+ element._handleDiscard(mockEvent);
+ assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
+ assert.isFalse(discardStub.called);
+ });
- element.draft = true;
+ test('no confirmation for comments without message text', () => {
+ element._messageText = '';
+ element._handleDiscard(mockEvent);
+ assert.isFalse(overlayStub.called);
+ assert.isTrue(discardStub.calledOnce);
+ });
+ });
+
+ test('ctrl+s saves comment', done => {
+ const stub = sinon.stub(element, 'save', () => {
+ assert.isTrue(stub.called);
+ stub.restore();
+ done();
+ return Promise.resolve();
+ });
+ element._messageText = 'is that the horse from horsing around??';
+ element.editing = true;
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(
+ element.textarea.$.textarea.textarea,
+ 83, 'ctrl'); // 'ctrl + s'
+ });
+
+ test('draft saving/editing', done => {
+ const fireStub = sinon.stub(element, 'fire');
+ const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+
+ element.draft = true;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ element._messageText = 'good news, everyone!';
+ element.flushDebouncer('fire-update');
+ element.flushDebouncer('store');
+ assert(fireStub.calledWith('comment-update'),
+ 'comment-update should be sent');
+ assert.isTrue(fireStub.calledOnce);
+
+ element._messageText = 'good news, everyone!';
+ element.flushDebouncer('fire-update');
+ element.flushDebouncer('store');
+ assert.isTrue(fireStub.calledOnce,
+ 'No events should fire for text editing');
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+
+ assert.isTrue(element.disabled,
+ 'Element should be disabled when creating draft.');
+
+ element._xhrPromise.then(draft => {
+ assert(fireStub.calledWith('comment-save'),
+ 'comment-save should be sent');
+ assert(cancelDebounce.calledWith('store'));
+
+ assert.deepEqual(fireStub.lastCall.args[1], {
+ comment: {
+ __commentSide: 'right',
+ __draft: true,
+ __draftID: 'temp_draft_id',
+ id: 'baf0414d_40572e03',
+ line: 5,
+ message: 'saved!',
+ path: '/path/to/file',
+ updated: '2015-12-08 21:52:36.177000000',
+ },
+ patchNum: 1,
+ });
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done creating draft.');
+ assert.equal(draft.message, 'saved!');
+ assert.isFalse(element.editing);
+ }).then(() => {
MockInteractions.tap(element.shadowRoot
.querySelector('.edit'));
- element._messageText = 'good news, everyone!';
- element.flushDebouncer('fire-update');
- element.flushDebouncer('store');
- assert(fireStub.calledWith('comment-update'),
- 'comment-update should be sent');
- assert.isTrue(fireStub.calledOnce);
-
- element._messageText = 'good news, everyone!';
- element.flushDebouncer('fire-update');
- element.flushDebouncer('store');
- assert.isTrue(fireStub.calledOnce,
- 'No events should fire for text editing');
-
+ element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
+ 'a world where humans are killed on sight.';
MockInteractions.tap(element.shadowRoot
.querySelector('.save'));
-
assert.isTrue(element.disabled,
- 'Element should be disabled when creating draft.');
+ 'Element should be disabled when updating draft.');
element._xhrPromise.then(draft => {
- assert(fireStub.calledWith('comment-save'),
- 'comment-save should be sent');
- assert(cancelDebounce.calledWith('store'));
-
- assert.deepEqual(fireStub.lastCall.args[1], {
- comment: {
- __commentSide: 'right',
- __draft: true,
- __draftID: 'temp_draft_id',
- id: 'baf0414d_40572e03',
- line: 5,
- message: 'saved!',
- path: '/path/to/file',
- updated: '2015-12-08 21:52:36.177000000',
- },
- patchNum: 1,
- });
assert.isFalse(element.disabled,
- 'Element should be enabled when done creating draft.');
+ 'Element should be enabled when done updating draft.');
assert.equal(draft.message, 'saved!');
assert.isFalse(element.editing);
- }).then(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
- 'a world where humans are killed on sight.';
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- assert.isTrue(element.disabled,
- 'Element should be disabled when updating draft.');
-
- element._xhrPromise.then(draft => {
- assert.isFalse(element.disabled,
- 'Element should be enabled when done updating draft.');
- assert.equal(draft.message, 'saved!');
- assert.isFalse(element.editing);
- fireStub.restore();
- done();
- });
- });
- });
-
- test('draft prevent save when disabled', () => {
- const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
- element.showActions = true;
- element.draft = true;
- MockInteractions.tap(element.$.header);
- MockInteractions.tap(element.shadowRoot
- .querySelector('.edit'));
- element._messageText = 'good news, everyone!';
- element.flushDebouncer('fire-update');
- element.flushDebouncer('store');
-
- element.disabled = true;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- assert.isFalse(saveStub.called);
-
- element.disabled = false;
- MockInteractions.tap(element.shadowRoot
- .querySelector('.save'));
- assert.isTrue(saveStub.calledOnce);
- });
-
- test('proper event fires on resolve, comment is not saved', done => {
- const save = sandbox.stub(element, 'save');
- element.addEventListener('comment-update', e => {
- assert.isTrue(e.detail.comment.unresolved);
- assert.isFalse(save.called);
+ fireStub.restore();
done();
});
- MockInteractions.tap(element.shadowRoot
- .querySelector('.resolve input'));
- });
-
- test('resolved comment state indicated by checkbox', () => {
- sandbox.stub(element, 'save');
- element.comment = {unresolved: false};
- assert.isTrue(element.shadowRoot
- .querySelector('.resolve input').checked);
- element.comment = {unresolved: true};
- assert.isFalse(element.shadowRoot
- .querySelector('.resolve input').checked);
- });
-
- test('resolved checkbox saves with tap when !editing', () => {
- element.editing = false;
- const save = sandbox.stub(element, 'save');
-
- element.comment = {unresolved: false};
- assert.isTrue(element.shadowRoot
- .querySelector('.resolve input').checked);
- element.comment = {unresolved: true};
- assert.isFalse(element.shadowRoot
- .querySelector('.resolve input').checked);
- assert.isFalse(save.called);
- MockInteractions.tap(element.$.resolvedCheckbox);
- assert.isTrue(element.shadowRoot
- .querySelector('.resolve input').checked);
- assert.isTrue(save.called);
- });
-
- suite('draft saving messages', () => {
- test('_getSavingMessage', () => {
- assert.equal(element._getSavingMessage(0), 'All changes saved');
- assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
- assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
- assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
- });
-
- test('_show{Start,End}Request', () => {
- const updateStub = sandbox.stub(element, '_updateRequestToast');
- element._numPendingDraftRequests.number = 1;
-
- element._showStartRequest();
- assert.isTrue(updateStub.calledOnce);
- assert.equal(updateStub.lastCall.args[0], 2);
- assert.equal(element._numPendingDraftRequests.number, 2);
-
- element._showEndRequest();
- assert.isTrue(updateStub.calledTwice);
- assert.equal(updateStub.lastCall.args[0], 1);
- assert.equal(element._numPendingDraftRequests.number, 1);
-
- element._showEndRequest();
- assert.isTrue(updateStub.calledThrice);
- assert.equal(updateStub.lastCall.args[0], 0);
- assert.equal(element._numPendingDraftRequests.number, 0);
- });
- });
-
- test('cancelling an unsaved draft discards, persists in storage', () => {
- const discardSpy = sandbox.spy(element, '_fireDiscard');
- const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
- const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
- element._messageText = 'test text';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isTrue(storeStub.called);
- assert.equal(storeStub.lastCall.args[1], 'test text');
- element._handleCancel({preventDefault: () => {}});
- assert.isTrue(discardSpy.called);
- assert.isFalse(eraseStub.called);
- });
-
- test('cancelling edit on a saved draft does not store', () => {
- element.comment.id = 'foo';
- const discardSpy = sandbox.spy(element, '_fireDiscard');
- const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
- element._messageText = 'test text';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isFalse(storeStub.called);
- element._handleCancel({preventDefault: () => {}});
- assert.isTrue(discardSpy.called);
- });
-
- test('deleting text from saved draft and saving deletes the draft', () => {
- element.comment = {id: 'foo', message: 'test'};
- element._messageText = '';
- const discardStub = sandbox.stub(element, '_discardDraft');
-
- element.save();
- assert.isTrue(discardStub.called);
- });
-
- test('_handleFix fires create-fix event', done => {
- element.addEventListener('create-fix-comment', e => {
- assert.deepEqual(e.detail, element._getEventPayload());
- done();
- });
- element.isRobotComment = true;
- element.comments = [element.comment];
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.fix'));
- });
-
- test('do not show Please Fix button if human reply exists', () => {
- element.comments = [
- {
- robot_id: 'happy_robot_id',
- robot_run_id: '5838406743490560',
- fix_suggestions: [
- {
- fix_id: '478ff847_3bf47aaf',
- description: 'Make the smiley happier by giving it a nose.',
- replacements: [
- {
- path: 'Documentation/config-gerrit.txt',
- range: {
- start_line: 10,
- start_character: 7,
- end_line: 10,
- end_character: 9,
- },
- replacement: ':-)',
- },
- ],
- },
- ],
- author: {
- _account_id: 1030912,
- name: 'Alice Kober-Sotzek',
- email: 'aliceks@google.com',
- avatars: [
- {
- url: '/s32-p/photo.jpg',
- height: 32,
- },
- {
- url: '/AaAdOFzPlFI/s56-p/photo.jpg',
- height: 56,
- },
- {
- url: '/AaAdOFzPlFI/s100-p/photo.jpg',
- height: 100,
- },
- {
- url: '/AaAdOFzPlFI/s120-p/photo.jpg',
- height: 120,
- },
- ],
- },
- patch_set: 1,
- id: 'eb0d03fd_5e95904f',
- line: 10,
- updated: '2017-04-04 15:36:17.000000000',
- message: 'This is a robot comment with a fix.',
- unresolved: false,
- __commentSide: 'right',
- collapsed: false,
- },
- {
- __draft: true,
- __draftID: '0.wbrfbwj89sa',
- __date: '2019-12-04T13:41:03.689Z',
- path: 'Documentation/config-gerrit.txt',
- patchNum: 1,
- side: 'REVISION',
- __commentSide: 'right',
- line: 10,
- in_reply_to: 'eb0d03fd_5e95904f',
- message: '> This is a robot comment with a fix.\n\nPlease fix.',
- unresolved: true,
- },
- ];
- element.comment = element.comments[0];
- flushAsynchronousOperations();
- assert.isNull(element.shadowRoot
- .querySelector('robotActions gr-button'));
- });
-
- test('show Please Fix if no human reply', () => {
- element.comments = [
- {
- robot_id: 'happy_robot_id',
- robot_run_id: '5838406743490560',
- fix_suggestions: [
- {
- fix_id: '478ff847_3bf47aaf',
- description: 'Make the smiley happier by giving it a nose.',
- replacements: [
- {
- path: 'Documentation/config-gerrit.txt',
- range: {
- start_line: 10,
- start_character: 7,
- end_line: 10,
- end_character: 9,
- },
- replacement: ':-)',
- },
- ],
- },
- ],
- author: {
- _account_id: 1030912,
- name: 'Alice Kober-Sotzek',
- email: 'aliceks@google.com',
- avatars: [
- {
- url: '/s32-p/photo.jpg',
- height: 32,
- },
- {
- url: '/AaAdOFzPlFI/s56-p/photo.jpg',
- height: 56,
- },
- {
- url: '/AaAdOFzPlFI/s100-p/photo.jpg',
- height: 100,
- },
- {
- url: '/AaAdOFzPlFI/s120-p/photo.jpg',
- height: 120,
- },
- ],
- },
- patch_set: 1,
- id: 'eb0d03fd_5e95904f',
- line: 10,
- updated: '2017-04-04 15:36:17.000000000',
- message: 'This is a robot comment with a fix.',
- unresolved: false,
- __commentSide: 'right',
- collapsed: false,
- },
- ];
- element.comment = element.comments[0];
- flushAsynchronousOperations();
- assert.isNotNull(element.shadowRoot
- .querySelector('.robotActions gr-button'));
- });
-
- test('_handleShowFix fires open-fix-preview event', done => {
- element.addEventListener('open-fix-preview', e => {
- assert.deepEqual(e.detail, element._getEventPayload());
- done();
- });
- element.comment = {fix_suggestions: [{}]};
- element.isRobotComment = true;
- flushAsynchronousOperations();
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.show-fix'));
});
});
- suite('respectful tips', () => {
- let element;
- let sandbox;
- let clock;
- setup(() => {
- stub('gr-rest-api-interface', {
- getAccount() { return Promise.resolve(null); },
- });
- clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
+ test('draft prevent save when disabled', () => {
+ const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
+ element.showActions = true;
+ element.draft = true;
+ MockInteractions.tap(element.$.header);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.edit'));
+ element._messageText = 'good news, everyone!';
+ element.flushDebouncer('fire-update');
+ element.flushDebouncer('store');
+
+ element.disabled = true;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+ assert.isFalse(saveStub.called);
+
+ element.disabled = false;
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.save'));
+ assert.isTrue(saveStub.calledOnce);
+ });
+
+ test('proper event fires on resolve, comment is not saved', done => {
+ const save = sandbox.stub(element, 'save');
+ element.addEventListener('comment-update', e => {
+ assert.isTrue(e.detail.comment.unresolved);
+ assert.isFalse(save.called);
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.resolve input'));
+ });
+
+ test('resolved comment state indicated by checkbox', () => {
+ sandbox.stub(element, 'save');
+ element.comment = {unresolved: false};
+ assert.isTrue(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ element.comment = {unresolved: true};
+ assert.isFalse(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ });
+
+ test('resolved checkbox saves with tap when !editing', () => {
+ element.editing = false;
+ const save = sandbox.stub(element, 'save');
+
+ element.comment = {unresolved: false};
+ assert.isTrue(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ element.comment = {unresolved: true};
+ assert.isFalse(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ assert.isFalse(save.called);
+ MockInteractions.tap(element.$.resolvedCheckbox);
+ assert.isTrue(element.shadowRoot
+ .querySelector('.resolve input').checked);
+ assert.isTrue(save.called);
+ });
+
+ suite('draft saving messages', () => {
+ test('_getSavingMessage', () => {
+ assert.equal(element._getSavingMessage(0), 'All changes saved');
+ assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
+ assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
+ assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
});
- teardown(() => {
- clock.restore();
- sandbox.restore();
- });
+ test('_show{Start,End}Request', () => {
+ const updateStub = sandbox.stub(element, '_updateRequestToast');
+ element._numPendingDraftRequests.number = 1;
- test('show tip when no cached record', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true};
+ element._showStartRequest();
+ assert.isTrue(updateStub.calledOnce);
+ assert.equal(updateStub.lastCall.args[0], 2);
+ assert.equal(element._numPendingDraftRequests.number, 2);
+
+ element._showEndRequest();
+ assert.isTrue(updateStub.calledTwice);
+ assert.equal(updateStub.lastCall.args[0], 1);
+ assert.equal(element._numPendingDraftRequests.number, 1);
+
+ element._showEndRequest();
+ assert.isTrue(updateStub.calledThrice);
+ assert.equal(updateStub.lastCall.args[0], 0);
+ assert.equal(element._numPendingDraftRequests.number, 0);
+ });
+ });
+
+ test('cancelling an unsaved draft discards, persists in storage', () => {
+ const discardSpy = sandbox.spy(element, '_fireDiscard');
+ const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+ const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
+ element._messageText = 'test text';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isTrue(storeStub.called);
+ assert.equal(storeStub.lastCall.args[1], 'test text');
+ element._handleCancel({preventDefault: () => {}});
+ assert.isTrue(discardSpy.called);
+ assert.isFalse(eraseStub.called);
+ });
+
+ test('cancelling edit on a saved draft does not store', () => {
+ element.comment.id = 'foo';
+ const discardSpy = sandbox.spy(element, '_fireDiscard');
+ const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+ element._messageText = 'test text';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isFalse(storeStub.called);
+ element._handleCancel({preventDefault: () => {}});
+ assert.isTrue(discardSpy.called);
+ });
+
+ test('deleting text from saved draft and saving deletes the draft', () => {
+ element.comment = {id: 'foo', message: 'test'};
+ element._messageText = '';
+ const discardStub = sandbox.stub(element, '_discardDraft');
+
+ element.save();
+ assert.isTrue(discardStub.called);
+ });
+
+ test('_handleFix fires create-fix event', done => {
+ element.addEventListener('create-fix-comment', e => {
+ assert.deepEqual(e.detail, element._getEventPayload());
+ done();
+ });
+ element.isRobotComment = true;
+ element.comments = [element.comment];
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.fix'));
+ });
+
+ test('do not show Please Fix button if human reply exists', () => {
+ element.comments = [
+ {
+ robot_id: 'happy_robot_id',
+ robot_run_id: '5838406743490560',
+ fix_suggestions: [
+ {
+ fix_id: '478ff847_3bf47aaf',
+ description: 'Make the smiley happier by giving it a nose.',
+ replacements: [
+ {
+ path: 'Documentation/config-gerrit.txt',
+ range: {
+ start_line: 10,
+ start_character: 7,
+ end_line: 10,
+ end_character: 9,
+ },
+ replacement: ':-)',
+ },
+ ],
+ },
+ ],
+ author: {
+ _account_id: 1030912,
+ name: 'Alice Kober-Sotzek',
+ email: 'aliceks@google.com',
+ avatars: [
+ {
+ url: '/s32-p/photo.jpg',
+ height: 32,
+ },
+ {
+ url: '/AaAdOFzPlFI/s56-p/photo.jpg',
+ height: 56,
+ },
+ {
+ url: '/AaAdOFzPlFI/s100-p/photo.jpg',
+ height: 100,
+ },
+ {
+ url: '/AaAdOFzPlFI/s120-p/photo.jpg',
+ height: 120,
+ },
+ ],
+ },
+ patch_set: 1,
+ id: 'eb0d03fd_5e95904f',
+ line: 10,
+ updated: '2017-04-04 15:36:17.000000000',
+ message: 'This is a robot comment with a fix.',
+ unresolved: false,
+ __commentSide: 'right',
+ collapsed: false,
+ },
+ {
+ __draft: true,
+ __draftID: '0.wbrfbwj89sa',
+ __date: '2019-12-04T13:41:03.689Z',
+ path: 'Documentation/config-gerrit.txt',
+ patchNum: 1,
+ side: 'REVISION',
+ __commentSide: 'right',
+ line: 10,
+ in_reply_to: 'eb0d03fd_5e95904f',
+ message: '> This is a robot comment with a fix.\n\nPlease fix.',
+ unresolved: true,
+ },
+ ];
+ element.comment = element.comments[0];
+ flushAsynchronousOperations();
+ assert.isNull(element.shadowRoot
+ .querySelector('robotActions gr-button'));
+ });
+
+ test('show Please Fix if no human reply', () => {
+ element.comments = [
+ {
+ robot_id: 'happy_robot_id',
+ robot_run_id: '5838406743490560',
+ fix_suggestions: [
+ {
+ fix_id: '478ff847_3bf47aaf',
+ description: 'Make the smiley happier by giving it a nose.',
+ replacements: [
+ {
+ path: 'Documentation/config-gerrit.txt',
+ range: {
+ start_line: 10,
+ start_character: 7,
+ end_line: 10,
+ end_character: 9,
+ },
+ replacement: ':-)',
+ },
+ ],
+ },
+ ],
+ author: {
+ _account_id: 1030912,
+ name: 'Alice Kober-Sotzek',
+ email: 'aliceks@google.com',
+ avatars: [
+ {
+ url: '/s32-p/photo.jpg',
+ height: 32,
+ },
+ {
+ url: '/AaAdOFzPlFI/s56-p/photo.jpg',
+ height: 56,
+ },
+ {
+ url: '/AaAdOFzPlFI/s100-p/photo.jpg',
+ height: 100,
+ },
+ {
+ url: '/AaAdOFzPlFI/s120-p/photo.jpg',
+ height: 120,
+ },
+ ],
+ },
+ patch_set: 1,
+ id: 'eb0d03fd_5e95904f',
+ line: 10,
+ updated: '2017-04-04 15:36:17.000000000',
+ message: 'This is a robot comment with a fix.',
+ unresolved: false,
+ __commentSide: 'right',
+ collapsed: false,
+ },
+ ];
+ element.comment = element.comments[0];
+ flushAsynchronousOperations();
+ assert.isNotNull(element.shadowRoot
+ .querySelector('.robotActions gr-button'));
+ });
+
+ test('_handleShowFix fires open-fix-preview event', done => {
+ element.addEventListener('open-fix-preview', e => {
+ assert.deepEqual(e.detail, element._getEventPayload());
+ done();
+ });
+ element.comment = {fix_suggestions: [{}]};
+ element.isRobotComment = true;
+ flushAsynchronousOperations();
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.show-fix'));
+ });
+ });
+
+ suite('respectful tips', () => {
+ let element;
+ let sandbox;
+ let clock;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getAccount() { return Promise.resolve(null); },
+ });
+ clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ clock.restore();
+ sandbox.restore();
+ });
+
+ test('show tip when no cached record', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isTrue(respectfulSetStub.called);
+ assert.isTrue(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+ done();
+ });
+ });
+
+ test('add 3 day delays once dismissed', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isTrue(respectfulSetStub.called);
+ assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
+ assert.isTrue(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.respectfulReviewTip .close'));
+ flushAsynchronousOperations();
+ assert.isTrue(respectfulSetStub.lastCall.args[0] === 3);
+ done();
+ });
+ });
+
+ test('do not show tip when fall out of probability', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 3;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isFalse(respectfulSetStub.called);
+ assert.isFalse(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+ done();
+ });
+ });
+
+ test('show tip when editing changed to true', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
+ });
+ respectfulGetStub.returns(null);
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: false};
+ flush(() => {
+ assert.isFalse(respectfulGetStub.called);
+ assert.isFalse(respectfulSetStub.called);
+ assert.isFalse(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+
+ element.editing = true;
flush(() => {
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
@@ -1166,113 +1257,30 @@
done();
});
});
+ });
- test('add 3 day delays once dismissed', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true};
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isTrue(respectfulSetStub.called);
- assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
- assert.isTrue(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('.respectfulReviewTip .close'));
- flushAsynchronousOperations();
- assert.isTrue(respectfulSetStub.lastCall.args[0] === 3);
- done();
- });
+ test('no tip when cached record', done => {
+ // fake stub for storage
+ const respectfulGetStub = sinon.stub();
+ const respectfulSetStub = sinon.stub();
+ stub('gr-storage', {
+ getRespectfulTipVisibility() { return respectfulGetStub(); },
+ setRespectfulTipVisibility() { return respectfulSetStub(); },
});
-
- test('do not show tip when fall out of probability', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 3;
- element.comment = {__editing: true};
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isFalse(respectfulSetStub.called);
- assert.isFalse(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
- done();
- });
- });
-
- test('show tip when editing changed to true', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns(null);
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: false};
- flush(() => {
- assert.isFalse(respectfulGetStub.called);
- assert.isFalse(respectfulSetStub.called);
- assert.isFalse(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
-
- element.editing = true;
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isTrue(respectfulSetStub.called);
- assert.isTrue(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
- done();
- });
- });
- });
-
- test('no tip when cached record', done => {
- // fake stub for storage
- const respectfulGetStub = sinon.stub();
- const respectfulSetStub = sinon.stub();
- stub('gr-storage', {
- getRespectfulTipVisibility() { return respectfulGetStub(); },
- setRespectfulTipVisibility() { return respectfulSetStub(); },
- });
- respectfulGetStub.returns({});
- element = fixture('draft');
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true};
- flush(() => {
- assert.isTrue(respectfulGetStub.called);
- assert.isFalse(respectfulSetStub.called);
- assert.isFalse(
- !!element.shadowRoot.querySelector('.respectfulReviewTip')
- );
- done();
- });
+ respectfulGetStub.returns({});
+ element = fixture('draft');
+ // fake random
+ element.getRandomNum = () => 0;
+ element.comment = {__editing: true};
+ flush(() => {
+ assert.isTrue(respectfulGetStub.called);
+ assert.isFalse(respectfulSetStub.called);
+ assert.isFalse(
+ !!element.shadowRoot.querySelector('.respectfulReviewTip')
+ );
+ done();
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
index 8d50fe0..7db24c2 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -14,54 +14,64 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-dialog/gr-dialog.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-confirm-delete-comment-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrConfirmDeleteCommentDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-confirm-delete-comment-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrConfirmDeleteCommentDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-confirm-delete-comment-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- message: String,
- };
- }
-
- resetFocus() {
- this.$.messageInput.textarea.focus();
- }
-
- _handleConfirmTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', {reason: this.message}, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
+ static get properties() {
+ return {
+ message: String,
+ };
}
- customElements.define(GrConfirmDeleteCommentDialog.is,
- GrConfirmDeleteCommentDialog);
-})();
+ resetFocus() {
+ this.$.messageInput.textarea.focus();
+ }
+
+ _handleConfirmTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', {reason: this.message}, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+}
+
+customElements.define(GrConfirmDeleteCommentDialog.is,
+ GrConfirmDeleteCommentDialog);
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
index e92bddb..f6caaa1 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
@@ -1,28 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-confirm-delete-comment-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -51,22 +45,12 @@
width: 73ch; /* Add a char to account for the border. */
}
</style>
- <gr-dialog
- confirm-label="Delete"
- on-confirm="_handleConfirmTap"
- on-cancel="_handleCancelTap">
+ <gr-dialog confirm-label="Delete" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">Delete Comment</div>
<div class="main" slot="main">
<p>This is an admin function. Please only use in exceptional circumstances.</p>
<label for="messageInput">Enter comment delete reason</label>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- placeholder="<Insert reasoning here>"
- bind-value="{{message}}"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" placeholder="<Insert reasoning here>" bind-value="{{message}}"></iron-autogrow-textarea>
</div>
</gr-dialog>
- </template>
- <script src="gr-confirm-delete-comment-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
index 9be6852..0f6168e 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -14,64 +14,74 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const COPY_TIMEOUT_MS = 1000;
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-copy-clipboard_html.js';
- /** @extends Polymer.Element */
- class GrCopyClipboard extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-copy-clipboard'; }
+const COPY_TIMEOUT_MS = 1000;
- static get properties() {
- return {
- text: String,
- buttonTitle: String,
- hasTooltip: {
- type: Boolean,
- value: false,
- },
- hideInput: {
- type: Boolean,
- value: false,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrCopyClipboard extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- focusOnCopy() {
- this.$.button.focus();
- }
+ static get is() { return 'gr-copy-clipboard'; }
- _computeInputClass(hideInput) {
- return hideInput ? 'hideInput' : '';
- }
-
- _handleInputClick(e) {
- e.preventDefault();
- Polymer.dom(e).rootTarget.select();
- }
-
- _copyToClipboard(e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (this.hideInput) {
- this.$.input.style.display = 'block';
- }
- this.$.input.focus();
- this.$.input.select();
- document.execCommand('copy');
- if (this.hideInput) {
- this.$.input.style.display = 'none';
- }
- this.$.icon.icon = 'gr-icons:check';
- this.async(
- () => this.$.icon.icon = 'gr-icons:content-copy',
- COPY_TIMEOUT_MS);
- }
+ static get properties() {
+ return {
+ text: String,
+ buttonTitle: String,
+ hasTooltip: {
+ type: Boolean,
+ value: false,
+ },
+ hideInput: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrCopyClipboard.is, GrCopyClipboard);
-})();
+ focusOnCopy() {
+ this.$.button.focus();
+ }
+
+ _computeInputClass(hideInput) {
+ return hideInput ? 'hideInput' : '';
+ }
+
+ _handleInputClick(e) {
+ e.preventDefault();
+ dom(e).rootTarget.select();
+ }
+
+ _copyToClipboard(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.hideInput) {
+ this.$.input.style.display = 'block';
+ }
+ this.$.input.focus();
+ this.$.input.select();
+ document.execCommand('copy');
+ if (this.hideInput) {
+ this.$.input.style.display = 'none';
+ }
+ this.$.icon.icon = 'gr-icons:check';
+ this.async(
+ () => this.$.icon.icon = 'gr-icons:content-copy',
+ COPY_TIMEOUT_MS);
+ }
+}
+
+customElements.define(GrCopyClipboard.is, GrCopyClipboard);
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
index c344bf64..29becbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
@@ -1,28 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-
-<dom-module id="gr-copy-clipboard">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.text {
align-items: center;
@@ -61,30 +55,11 @@
}
</style>
<div class="text">
- <iron-input
- class="copyText"
- type="text"
- bind-value="[[text]]"
- on-tap="_handleInputClick"
- readonly>
- <input
- id="input"
- is="iron-input"
- class$="[[_computeInputClass(hideInput)]]"
- type="text"
- bind-value="[[text]]"
- on-click="_handleInputClick"
- readonly>
+ <iron-input class="copyText" type="text" bind-value="[[text]]" on-tap="_handleInputClick" readonly="">
+ <input id="input" is="iron-input" class\$="[[_computeInputClass(hideInput)]]" type="text" bind-value="[[text]]" on-click="_handleInputClick" readonly="">
</iron-input>
- <gr-button id="button"
- link
- has-tooltip="[[hasTooltip]]"
- class="copyToClipboard"
- title="[[buttonTitle]]"
- on-click="_copyToClipboard">
+ <gr-button id="button" link="" has-tooltip="[[hasTooltip]]" class="copyToClipboard" title="[[buttonTitle]]" on-click="_copyToClipboard">
<iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
</gr-button>
</div>
- </template>
- <script src="gr-copy-clipboard.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
index 45ade85..a39e4c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_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-copy-clipboard</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-copy-clipboard.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-copy-clipboard.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-copy-clipboard.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,73 +40,76 @@
</template>
</test-fixture>
-<script>
- suite('gr-copy-clipboard 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-copy-clipboard.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-copy-clipboard tests', () => {
+ let element;
+ let sandbox;
- setup(done => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.text = `git fetch http://gerrit@localhost:8080/a/test-project
- refs/changes/05/5/1 && git checkout FETCH_HEAD`;
- flushAsynchronousOperations();
- flush(done);
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('copy to clipboard', () => {
- const clipboardSpy = sandbox.spy(element, '_copyToClipboard');
- const copyBtn = element.shadowRoot
- .querySelector('.copyToClipboard');
- MockInteractions.tap(copyBtn);
- assert.isTrue(clipboardSpy.called);
- });
-
- test('focusOnCopy', () => {
- element.focusOnCopy();
- assert.deepEqual(Polymer.dom(element.root).activeElement,
- element.shadowRoot
- .querySelector('.copyToClipboard'));
- });
-
- test('_handleInputClick', () => {
- // iron-input as parent should never be hidden as copy won't work
- // on nested hidden elements
- const ironInputElement = element.shadowRoot.querySelector('iron-input');
- assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
-
- const inputElement = element.shadowRoot.querySelector('input');
- MockInteractions.tap(inputElement);
- assert.equal(inputElement.selectionStart, 0);
- assert.equal(inputElement.selectionEnd, element.text.length - 1);
- });
-
- test('hideInput', () => {
- // iron-input as parent should never be hidden as copy won't work
- // on nested hidden elements
- const ironInputElement = element.shadowRoot.querySelector('iron-input');
- assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
-
- assert.notEqual(getComputedStyle(element.$.input).display, 'none');
- element.hideInput = true;
- flushAsynchronousOperations();
- assert.equal(getComputedStyle(element.$.input).display, 'none');
- });
-
- test('stop events propagation', () => {
- const divParent = document.createElement('div');
- divParent.appendChild(element);
- const clickStub = sinon.stub();
- divParent.addEventListener('click', clickStub);
- element.stopPropagation = true;
- const copyBtn = element.shadowRoot.querySelector('.copyToClipboard');
- MockInteractions.tap(copyBtn);
- assert.isFalse(clickStub.called);
- });
+ setup(done => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.text = `git fetch http://gerrit@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git checkout FETCH_HEAD`;
+ flushAsynchronousOperations();
+ flush(done);
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('copy to clipboard', () => {
+ const clipboardSpy = sandbox.spy(element, '_copyToClipboard');
+ const copyBtn = element.shadowRoot
+ .querySelector('.copyToClipboard');
+ MockInteractions.tap(copyBtn);
+ assert.isTrue(clipboardSpy.called);
+ });
+
+ test('focusOnCopy', () => {
+ element.focusOnCopy();
+ assert.deepEqual(dom(element.root).activeElement,
+ element.shadowRoot
+ .querySelector('.copyToClipboard'));
+ });
+
+ test('_handleInputClick', () => {
+ // iron-input as parent should never be hidden as copy won't work
+ // on nested hidden elements
+ const ironInputElement = element.shadowRoot.querySelector('iron-input');
+ assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
+
+ const inputElement = element.shadowRoot.querySelector('input');
+ MockInteractions.tap(inputElement);
+ assert.equal(inputElement.selectionStart, 0);
+ assert.equal(inputElement.selectionEnd, element.text.length - 1);
+ });
+
+ test('hideInput', () => {
+ // iron-input as parent should never be hidden as copy won't work
+ // on nested hidden elements
+ const ironInputElement = element.shadowRoot.querySelector('iron-input');
+ assert.notEqual(getComputedStyle(ironInputElement).display, 'none');
+
+ assert.notEqual(getComputedStyle(element.$.input).display, 'none');
+ element.hideInput = true;
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.$.input).display, 'none');
+ });
+
+ test('stop events propagation', () => {
+ const divParent = document.createElement('div');
+ divParent.appendChild(element);
+ const clickStub = sinon.stub();
+ divParent.addEventListener('click', clickStub);
+ element.stopPropagation = true;
+ const copyBtn = element.shadowRoot.querySelector('.copyToClipboard');
+ MockInteractions.tap(copyBtn);
+ assert.isFalse(clickStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
index b69c61aa..02c57e0 100644
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
@@ -1,58 +1,56 @@
-<!--
-@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';
+ const GrCountStringFormatter = window.GrCountStringFormatter || {};
-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
+ /**
+ * Returns a count plus string that is pluralized when necessary.
+ *
+ * @param {number} count
+ * @param {string} noun
+ * @return {string}
+ */
+ GrCountStringFormatter.computePluralString = function(count, noun) {
+ return this.computeString(count, noun) + (count > 1 ? 's' : '');
+ };
-http://www.apache.org/licenses/LICENSE-2.0
+ /**
+ * Returns a count plus string that is not pluralized.
+ *
+ * @param {number} count
+ * @param {string} noun
+ * @return {string}
+ */
+ GrCountStringFormatter.computeString = function(count, noun) {
+ if (count === 0) { return ''; }
+ return count + ' ' + noun;
+ };
-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';
- const GrCountStringFormatter = window.GrCountStringFormatter || {};
-
- /**
- * Returns a count plus string that is pluralized when necessary.
- *
- * @param {number} count
- * @param {string} noun
- * @return {string}
- */
- GrCountStringFormatter.computePluralString = function(count, noun) {
- return this.computeString(count, noun) + (count > 1 ? 's' : '');
- };
-
- /**
- * Returns a count plus string that is not pluralized.
- *
- * @param {number} count
- * @param {string} noun
- * @return {string}
- */
- GrCountStringFormatter.computeString = function(count, noun) {
- if (count === 0) { return ''; }
- return count + ' ' + noun;
- };
-
- /**
- * Returns a count plus arbitrary text.
- *
- * @param {number} count
- * @param {string} text
- * @return {string}
- */
- GrCountStringFormatter.computeShortString = function(count, text) {
- if (count === 0) { return ''; }
- return count + text;
- };
- window.GrCountStringFormatter = GrCountStringFormatter;
- })(window);
-</script>
+ /**
+ * Returns a count plus arbitrary text.
+ *
+ * @param {number} count
+ * @param {string} text
+ * @return {string}
+ */
+ GrCountStringFormatter.computeShortString = function(count, text) {
+ if (count === 0) { return ''; }
+ return count + text;
+ };
+ window.GrCountStringFormatter = GrCountStringFormatter;
+})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
index 64dff6a..9981d45 100644
--- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -19,41 +19,43 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-count-string-formatter</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-count-string-formatter.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-count-string-formatter.js"></script>
-<script>
- suite('gr-count-string-formatter tests', async () => {
- await readyToTest();
- test('computeString', () => {
- const noun = 'unresolved';
- assert.equal(GrCountStringFormatter.computeString(0, noun), '');
- assert.equal(GrCountStringFormatter.computeString(1, noun),
- '1 unresolved');
- assert.equal(GrCountStringFormatter.computeString(2, noun),
- '2 unresolved');
- });
-
- test('computeShortString', () => {
- const noun = 'c';
- assert.equal(GrCountStringFormatter.computeShortString(0, noun), '');
- assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c');
- assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c');
- });
-
- test('computePluralString', () => {
- const noun = 'comment';
- assert.equal(GrCountStringFormatter.computePluralString(0, noun), '');
- assert.equal(GrCountStringFormatter.computePluralString(1, noun),
- '1 comment');
- assert.equal(GrCountStringFormatter.computePluralString(2, noun),
- '2 comments');
- });
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-count-string-formatter.js';
+suite('gr-count-string-formatter tests', () => {
+ test('computeString', () => {
+ const noun = 'unresolved';
+ assert.equal(GrCountStringFormatter.computeString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computeString(1, noun),
+ '1 unresolved');
+ assert.equal(GrCountStringFormatter.computeString(2, noun),
+ '2 unresolved');
});
+
+ test('computeShortString', () => {
+ const noun = 'c';
+ assert.equal(GrCountStringFormatter.computeShortString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c');
+ assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c');
+ });
+
+ test('computePluralString', () => {
+ const noun = 'comment';
+ assert.equal(GrCountStringFormatter.computePluralString(0, noun), '');
+ assert.equal(GrCountStringFormatter.computePluralString(1, noun),
+ '1 comment');
+ assert.equal(GrCountStringFormatter.computePluralString(2, noun),
+ '2 comments');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 4f98e88..f184b6c 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -14,422 +14,426 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.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-cursor-manager_html.js';
- const ScrollBehavior = {
- NEVER: 'never',
- KEEP_VISIBLE: 'keep-visible',
- };
+const ScrollBehavior = {
+ NEVER: 'never',
+ KEEP_VISIBLE: 'keep-visible',
+};
- /** @extends Polymer.Element */
- class GrCursorManager extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-cursor-manager'; }
+/** @extends Polymer.Element */
+class GrCursorManager extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get properties() {
- return {
- stops: {
- type: Array,
- value() {
- return [];
- },
- observer: '_updateIndex',
+ static get is() { return 'gr-cursor-manager'; }
+
+ static get properties() {
+ return {
+ stops: {
+ type: Array,
+ value() {
+ return [];
},
- /**
- * @type {?Object}
- */
- target: {
- type: Object,
- notify: true,
- observer: '_scrollToTarget',
- },
- /**
- * The height of content intended to be included with the target.
- *
- * @type {?number}
- */
- _targetHeight: Number,
+ observer: '_updateIndex',
+ },
+ /**
+ * @type {?Object}
+ */
+ target: {
+ type: Object,
+ notify: true,
+ observer: '_scrollToTarget',
+ },
+ /**
+ * The height of content intended to be included with the target.
+ *
+ * @type {?number}
+ */
+ _targetHeight: Number,
- /**
- * The index of the current target (if any). -1 otherwise.
- */
- index: {
- type: Number,
- value: -1,
- notify: true,
- },
+ /**
+ * The index of the current target (if any). -1 otherwise.
+ */
+ index: {
+ type: Number,
+ value: -1,
+ notify: true,
+ },
- /**
- * The class to apply to the current target. Use null for no class.
- */
- cursorTargetClass: {
- type: String,
- value: null,
- },
+ /**
+ * The class to apply to the current target. Use null for no class.
+ */
+ cursorTargetClass: {
+ type: String,
+ value: null,
+ },
- /**
- * The scroll behavior for the cursor. Values are 'never' and
- * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
- * the viewport.
- * TODO (beckysiegel) figure out why it can be undefined
- *
- * @type {string|undefined}
- */
- scrollBehavior: {
- type: String,
- value: ScrollBehavior.NEVER,
- },
+ /**
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ * TODO (beckysiegel) figure out why it can be undefined
+ *
+ * @type {string|undefined}
+ */
+ scrollBehavior: {
+ type: String,
+ value: ScrollBehavior.NEVER,
+ },
- /**
- * When true, will call element.focus() during scrolling.
- */
- focusOnMove: {
- type: Boolean,
- value: false,
- },
+ /**
+ * When true, will call element.focus() during scrolling.
+ */
+ focusOnMove: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * The scrollTopMargin defines height of invisible area at the top
- * of the page. If cursor locates inside this margin - it is
- * not visible, because it is covered by some other element.
- */
- scrollTopMargin: {
- type: Number,
- value: 0,
- },
- };
+ /**
+ * The scrollTopMargin defines height of invisible area at the top
+ * of the page. If cursor locates inside this margin - it is
+ * not visible, because it is covered by some other element.
+ */
+ scrollTopMargin: {
+ type: Number,
+ value: 0,
+ },
+ };
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unsetCursor();
+ }
+
+ /**
+ * Move the cursor forward. Clipped to the ends of the stop list.
+ *
+ * @param {!Function=} opt_condition Optional stop condition. If a condition
+ * is passed the cursor will continue to move in the specified direction
+ * until the condition is met.
+ * @param {!Function=} opt_getTargetHeight Optional function to calculate the
+ * height of the target's 'section'. The height of the target itself is
+ * sometimes different, used by the diff cursor.
+ * @param {boolean=} opt_clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @private
+ */
+
+ next(opt_condition, opt_getTargetHeight, opt_clipToTop) {
+ this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop);
+ }
+
+ previous(opt_condition) {
+ this._moveCursor(-1, opt_condition);
+ }
+
+ /**
+ * Move the cursor to the row which is the closest to the viewport center
+ * in vertical direction.
+ * The method uses IntersectionObservers API. If browser
+ * doesn't support this API the method does nothing
+ *
+ * @param {!Function=} opt_condition Optional condition. If a condition
+ * is passed only stops which meet conditions are taken into account.
+ */
+ moveToVisibleArea(opt_condition) {
+ if (!this.stops || !this._isIntersectionObserverSupported()) {
+ return;
}
+ const filteredStops = opt_condition ? this.stops.filter(opt_condition)
+ : this.stops;
+ const dims = this._getWindowDims();
+ const windowCenter =
+ Math.round((dims.innerHeight + this.scrollTopMargin) / 2);
- /** @override */
- detached() {
- super.detached();
- this.unsetCursor();
- }
+ let closestToTheCenter = null;
+ let minDistanceToCenter = null;
+ let unobservedCount = filteredStops.length;
- /**
- * Move the cursor forward. Clipped to the ends of the stop list.
- *
- * @param {!Function=} opt_condition Optional stop condition. If a condition
- * is passed the cursor will continue to move in the specified direction
- * until the condition is met.
- * @param {!Function=} opt_getTargetHeight Optional function to calculate the
- * height of the target's 'section'. The height of the target itself is
- * sometimes different, used by the diff cursor.
- * @param {boolean=} opt_clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @private
- */
+ const observer = new IntersectionObserver(entries => {
+ // This callback is called for the first time immediately.
+ // Typically it gets all observed stops at once, but
+ // sometimes can get them in several chunks.
+ entries.forEach(entry => {
+ observer.unobserve(entry.target);
- next(opt_condition, opt_getTargetHeight, opt_clipToTop) {
- this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop);
- }
-
- previous(opt_condition) {
- this._moveCursor(-1, opt_condition);
- }
-
- /**
- * Move the cursor to the row which is the closest to the viewport center
- * in vertical direction.
- * The method uses IntersectionObservers API. If browser
- * doesn't support this API the method does nothing
- *
- * @param {!Function=} opt_condition Optional condition. If a condition
- * is passed only stops which meet conditions are taken into account.
- */
- moveToVisibleArea(opt_condition) {
- if (!this.stops || !this._isIntersectionObserverSupported()) {
- return;
- }
- const filteredStops = opt_condition ? this.stops.filter(opt_condition)
- : this.stops;
- const dims = this._getWindowDims();
- const windowCenter =
- Math.round((dims.innerHeight + this.scrollTopMargin) / 2);
-
- let closestToTheCenter = null;
- let minDistanceToCenter = null;
- let unobservedCount = filteredStops.length;
-
- const observer = new IntersectionObserver(entries => {
- // This callback is called for the first time immediately.
- // Typically it gets all observed stops at once, but
- // sometimes can get them in several chunks.
- entries.forEach(entry => {
- observer.unobserve(entry.target);
-
- // In Edge it is recommended to use intersectionRatio instead of
- // isIntersecting.
- const isInsideViewport =
- entry.isIntersecting || entry.intersectionRatio > 0;
- if (!isInsideViewport) {
- return;
- }
- const center = entry.boundingClientRect.top + Math.round(
- entry.boundingClientRect.height / 2);
- const distanceToWindowCenter = Math.abs(center - windowCenter);
- if (minDistanceToCenter === null ||
- distanceToWindowCenter < minDistanceToCenter) {
- closestToTheCenter = entry.target;
- minDistanceToCenter = distanceToWindowCenter;
- }
- });
- unobservedCount -= entries.length;
- if (unobservedCount == 0 && closestToTheCenter) {
- // set cursor when all stops were observed.
- // In most cases the target is visible, so scroll is not
- // needed. But in rare cases the target can become invisible
- // at this point (due to some scrolling in window).
- // To avoid jumps set noScroll options.
- this.setCursor(closestToTheCenter, true);
- }
- });
- filteredStops.forEach(stop => {
- observer.observe(stop);
- });
- }
-
- _isIntersectionObserverSupported() {
- // The copy of this method exists in gr-app-element.js under the
- // name _isCursorManagerSupportMoveToVisibleLine
- // If you update this method, you must update gr-app-element.js
- // as well.
- return 'IntersectionObserver' in window;
- }
-
- /**
- * Set the cursor to an arbitrary element.
- *
- * @param {!HTMLElement} element
- * @param {boolean=} opt_noScroll prevent any potential scrolling in response
- * setting the cursor.
- */
- setCursor(element, opt_noScroll) {
- let behavior;
- if (opt_noScroll) {
- behavior = this.scrollBehavior;
- this.scrollBehavior = ScrollBehavior.NEVER;
- }
-
- this.unsetCursor();
- this.target = element;
- this._updateIndex();
- this._decorateTarget();
-
- if (opt_noScroll) { this.scrollBehavior = behavior; }
- }
-
- unsetCursor() {
- this._unDecorateTarget();
- this.index = -1;
- this.target = null;
- this._targetHeight = null;
- }
-
- isAtStart() {
- return this.index === 0;
- }
-
- isAtEnd() {
- return this.index === this.stops.length - 1;
- }
-
- moveToStart() {
- if (this.stops.length) {
- this.setCursor(this.stops[0]);
- }
- }
-
- moveToEnd() {
- if (this.stops.length) {
- this.setCursor(this.stops[this.stops.length - 1]);
- }
- }
-
- setCursorAtIndex(index, opt_noScroll) {
- this.setCursor(this.stops[index], opt_noScroll);
- }
-
- /**
- * Move the cursor forward or backward by delta. Clipped to the beginning or
- * end of stop list.
- *
- * @param {number} delta either -1 or 1.
- * @param {!Function=} opt_condition Optional stop condition. If a condition
- * is passed the cursor will continue to move in the specified direction
- * until the condition is met.
- * @param {!Function=} opt_getTargetHeight Optional function to calculate the
- * height of the target's 'section'. The height of the target itself is
- * sometimes different, used by the diff cursor.
- * @param {boolean=} opt_clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @private
- */
- _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop) {
- if (!this.stops.length) {
- this.unsetCursor();
- return;
- }
-
- this._unDecorateTarget();
-
- const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop);
-
- let newTarget = null;
- if (newIndex !== -1) {
- newTarget = this.stops[newIndex];
- }
-
- this.index = newIndex;
- this.target = newTarget;
-
- if (!this.target) { return; }
-
- if (opt_getTargetHeight) {
- this._targetHeight = opt_getTargetHeight(newTarget);
- } else {
- this._targetHeight = newTarget.scrollHeight;
- }
-
- if (this.focusOnMove) { this.target.focus(); }
-
- this._decorateTarget();
- }
-
- _decorateTarget() {
- if (this.target && this.cursorTargetClass) {
- this.target.classList.add(this.cursorTargetClass);
- }
- }
-
- _unDecorateTarget() {
- if (this.target && this.cursorTargetClass) {
- this.target.classList.remove(this.cursorTargetClass);
- }
- }
-
- /**
- * Get the next stop index indicated by the delta direction.
- *
- * @param {number} delta either -1 or 1.
- * @param {!Function=} opt_condition Optional stop condition.
- * @param {boolean=} opt_clipToTop When none of the next indices match, move
- * back to first instead of to last.
- * @return {number} the new index.
- * @private
- */
- _getNextindex(delta, opt_condition, opt_clipToTop) {
- if (!this.stops.length || this.index === -1) {
- return -1;
- }
-
- let newIndex = this.index;
- do {
- newIndex = newIndex + delta;
- } while (newIndex > 0 &&
- newIndex < this.stops.length - 1 &&
- opt_condition && !opt_condition(this.stops[newIndex]));
-
- newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
-
- // If we failed to satisfy the condition:
- if (opt_condition && !opt_condition(this.stops[newIndex])) {
- if (delta < 0 || opt_clipToTop) {
- return 0;
- } else if (delta > 0) {
- return this.stops.length - 1;
- }
- return this.index;
- }
-
- return newIndex;
- }
-
- _updateIndex() {
- if (!this.target) {
- this.index = -1;
- return;
- }
-
- const newIndex = Array.prototype.indexOf.call(this.stops, this.target);
- if (newIndex === -1) {
- this.unsetCursor();
- } else {
- this.index = newIndex;
- }
- }
-
- /**
- * Calculate where the element is relative to the window.
- *
- * @param {!Object} target Target to scroll to.
- * @return {number} Distance to top of the target.
- */
- _getTop(target) {
- let top = target.offsetTop;
- for (let offsetParent = target.offsetParent;
- offsetParent;
- offsetParent = offsetParent.offsetParent) {
- top += offsetParent.offsetTop;
- }
- return top;
- }
-
- /**
- * @return {boolean}
- */
- _targetIsVisible(top) {
- const dims = this._getWindowDims();
- return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE &&
- top > (dims.pageYOffset + this.scrollTopMargin) &&
- top < dims.pageYOffset + dims.innerHeight;
- }
-
- _calculateScrollToValue(top, target) {
- const dims = this._getWindowDims();
- return top + this.scrollTopMargin - (dims.innerHeight / 3) +
- (target.offsetHeight / 2);
- }
-
- _scrollToTarget() {
- if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) {
- return;
- }
-
- const dims = this._getWindowDims();
- const top = this._getTop(this.target);
- const bottomIsVisible = this._targetHeight ?
- this._targetIsVisible(top + this._targetHeight) : true;
- const scrollToValue = this._calculateScrollToValue(top, this.target);
-
- if (this._targetIsVisible(top)) {
- // Don't scroll if either the bottom is visible or if the position that
- // would get scrolled to is higher up than the current position. this
- // woulld cause less of the target content to be displayed than is
- // already.
- if (bottomIsVisible || scrollToValue < dims.scrollY) {
+ // In Edge it is recommended to use intersectionRatio instead of
+ // isIntersecting.
+ const isInsideViewport =
+ entry.isIntersecting || entry.intersectionRatio > 0;
+ if (!isInsideViewport) {
return;
}
+ const center = entry.boundingClientRect.top + Math.round(
+ entry.boundingClientRect.height / 2);
+ const distanceToWindowCenter = Math.abs(center - windowCenter);
+ if (minDistanceToCenter === null ||
+ distanceToWindowCenter < minDistanceToCenter) {
+ closestToTheCenter = entry.target;
+ minDistanceToCenter = distanceToWindowCenter;
+ }
+ });
+ unobservedCount -= entries.length;
+ if (unobservedCount == 0 && closestToTheCenter) {
+ // set cursor when all stops were observed.
+ // In most cases the target is visible, so scroll is not
+ // needed. But in rare cases the target can become invisible
+ // at this point (due to some scrolling in window).
+ // To avoid jumps set noScroll options.
+ this.setCursor(closestToTheCenter, true);
}
+ });
+ filteredStops.forEach(stop => {
+ observer.observe(stop);
+ });
+ }
- // Scroll the element to the middle of the window. Dividing by a third
- // instead of half the inner height feels a bit better otherwise the
- // element appears to be below the center of the window even when it
- // isn't.
- window.scrollTo(dims.scrollX, scrollToValue);
+ _isIntersectionObserverSupported() {
+ // The copy of this method exists in gr-app-element.js under the
+ // name _isCursorManagerSupportMoveToVisibleLine
+ // If you update this method, you must update gr-app-element.js
+ // as well.
+ return 'IntersectionObserver' in window;
+ }
+
+ /**
+ * Set the cursor to an arbitrary element.
+ *
+ * @param {!HTMLElement} element
+ * @param {boolean=} opt_noScroll prevent any potential scrolling in response
+ * setting the cursor.
+ */
+ setCursor(element, opt_noScroll) {
+ let behavior;
+ if (opt_noScroll) {
+ behavior = this.scrollBehavior;
+ this.scrollBehavior = ScrollBehavior.NEVER;
}
- _getWindowDims() {
- return {
- scrollX: window.scrollX,
- scrollY: window.scrollY,
- innerHeight: window.innerHeight,
- pageYOffset: window.pageYOffset,
- };
+ this.unsetCursor();
+ this.target = element;
+ this._updateIndex();
+ this._decorateTarget();
+
+ if (opt_noScroll) { this.scrollBehavior = behavior; }
+ }
+
+ unsetCursor() {
+ this._unDecorateTarget();
+ this.index = -1;
+ this.target = null;
+ this._targetHeight = null;
+ }
+
+ isAtStart() {
+ return this.index === 0;
+ }
+
+ isAtEnd() {
+ return this.index === this.stops.length - 1;
+ }
+
+ moveToStart() {
+ if (this.stops.length) {
+ this.setCursor(this.stops[0]);
}
}
- customElements.define(GrCursorManager.is, GrCursorManager);
-})();
+ moveToEnd() {
+ if (this.stops.length) {
+ this.setCursor(this.stops[this.stops.length - 1]);
+ }
+ }
+
+ setCursorAtIndex(index, opt_noScroll) {
+ this.setCursor(this.stops[index], opt_noScroll);
+ }
+
+ /**
+ * Move the cursor forward or backward by delta. Clipped to the beginning or
+ * end of stop list.
+ *
+ * @param {number} delta either -1 or 1.
+ * @param {!Function=} opt_condition Optional stop condition. If a condition
+ * is passed the cursor will continue to move in the specified direction
+ * until the condition is met.
+ * @param {!Function=} opt_getTargetHeight Optional function to calculate the
+ * height of the target's 'section'. The height of the target itself is
+ * sometimes different, used by the diff cursor.
+ * @param {boolean=} opt_clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @private
+ */
+ _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop) {
+ if (!this.stops.length) {
+ this.unsetCursor();
+ return;
+ }
+
+ this._unDecorateTarget();
+
+ const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop);
+
+ let newTarget = null;
+ if (newIndex !== -1) {
+ newTarget = this.stops[newIndex];
+ }
+
+ this.index = newIndex;
+ this.target = newTarget;
+
+ if (!this.target) { return; }
+
+ if (opt_getTargetHeight) {
+ this._targetHeight = opt_getTargetHeight(newTarget);
+ } else {
+ this._targetHeight = newTarget.scrollHeight;
+ }
+
+ if (this.focusOnMove) { this.target.focus(); }
+
+ this._decorateTarget();
+ }
+
+ _decorateTarget() {
+ if (this.target && this.cursorTargetClass) {
+ this.target.classList.add(this.cursorTargetClass);
+ }
+ }
+
+ _unDecorateTarget() {
+ if (this.target && this.cursorTargetClass) {
+ this.target.classList.remove(this.cursorTargetClass);
+ }
+ }
+
+ /**
+ * Get the next stop index indicated by the delta direction.
+ *
+ * @param {number} delta either -1 or 1.
+ * @param {!Function=} opt_condition Optional stop condition.
+ * @param {boolean=} opt_clipToTop When none of the next indices match, move
+ * back to first instead of to last.
+ * @return {number} the new index.
+ * @private
+ */
+ _getNextindex(delta, opt_condition, opt_clipToTop) {
+ if (!this.stops.length || this.index === -1) {
+ return -1;
+ }
+
+ let newIndex = this.index;
+ do {
+ newIndex = newIndex + delta;
+ } while (newIndex > 0 &&
+ newIndex < this.stops.length - 1 &&
+ opt_condition && !opt_condition(this.stops[newIndex]));
+
+ newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex));
+
+ // If we failed to satisfy the condition:
+ if (opt_condition && !opt_condition(this.stops[newIndex])) {
+ if (delta < 0 || opt_clipToTop) {
+ return 0;
+ } else if (delta > 0) {
+ return this.stops.length - 1;
+ }
+ return this.index;
+ }
+
+ return newIndex;
+ }
+
+ _updateIndex() {
+ if (!this.target) {
+ this.index = -1;
+ return;
+ }
+
+ const newIndex = Array.prototype.indexOf.call(this.stops, this.target);
+ if (newIndex === -1) {
+ this.unsetCursor();
+ } else {
+ this.index = newIndex;
+ }
+ }
+
+ /**
+ * Calculate where the element is relative to the window.
+ *
+ * @param {!Object} target Target to scroll to.
+ * @return {number} Distance to top of the target.
+ */
+ _getTop(target) {
+ let top = target.offsetTop;
+ for (let offsetParent = target.offsetParent;
+ offsetParent;
+ offsetParent = offsetParent.offsetParent) {
+ top += offsetParent.offsetTop;
+ }
+ return top;
+ }
+
+ /**
+ * @return {boolean}
+ */
+ _targetIsVisible(top) {
+ const dims = this._getWindowDims();
+ return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE &&
+ top > (dims.pageYOffset + this.scrollTopMargin) &&
+ top < dims.pageYOffset + dims.innerHeight;
+ }
+
+ _calculateScrollToValue(top, target) {
+ const dims = this._getWindowDims();
+ return top + this.scrollTopMargin - (dims.innerHeight / 3) +
+ (target.offsetHeight / 2);
+ }
+
+ _scrollToTarget() {
+ if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) {
+ return;
+ }
+
+ const dims = this._getWindowDims();
+ const top = this._getTop(this.target);
+ const bottomIsVisible = this._targetHeight ?
+ this._targetIsVisible(top + this._targetHeight) : true;
+ const scrollToValue = this._calculateScrollToValue(top, this.target);
+
+ if (this._targetIsVisible(top)) {
+ // Don't scroll if either the bottom is visible or if the position that
+ // would get scrolled to is higher up than the current position. this
+ // woulld cause less of the target content to be displayed than is
+ // already.
+ if (bottomIsVisible || scrollToValue < dims.scrollY) {
+ return;
+ }
+ }
+
+ // Scroll the element to the middle of the window. Dividing by a third
+ // instead of half the inner height feels a bit better otherwise the
+ // element appears to be below the center of the window even when it
+ // isn't.
+ window.scrollTo(dims.scrollX, scrollToValue);
+ }
+
+ _getWindowDims() {
+ return {
+ scrollX: window.scrollX,
+ scrollY: window.scrollY,
+ innerHeight: window.innerHeight,
+ pageYOffset: window.pageYOffset,
+ };
+ }
+}
+
+customElements.define(GrCursorManager.is, GrCursorManager);
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
index 94d7aaa..29757e5 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
@@ -1,23 +1,21 @@
-<!--
-@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
+export const htmlTemplate = html`
-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-cursor-manager">
- <template></template>
- <script src="gr-cursor-manager.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
index e7d5d74..5264464 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_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-cursor-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>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-cursor-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/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script type="module" src="./gr-cursor-manager.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-cursor-manager.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -41,243 +46,245 @@
</template>
</test-fixture>
-<script>
- suite('gr-cursor-manager tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
- let list;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-cursor-manager.js';
+suite('gr-cursor-manager tests', () => {
+ let sandbox;
+ let element;
+ let list;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ const fixtureElements = fixture('basic');
+ element = fixtureElements[0];
+ list = fixtureElements[1];
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('core cursor functionality', () => {
+ // The element is initialized into the proper state.
+ assert.isArray(element.stops);
+ assert.equal(element.stops.length, 0);
+ assert.equal(element.index, -1);
+ assert.isNotOk(element.target);
+
+ // Initialize the cursor with its stops.
+ element.stops = list.querySelectorAll('li');
+
+ // It should have the stops but it should not be targeting any of them.
+ assert.isNotNull(element.stops);
+ assert.equal(element.stops.length, 4);
+ assert.equal(element.index, -1);
+ assert.isNotOk(element.target);
+
+ // Select the third stop.
+ element.setCursor(list.children[2]);
+
+ // It should update its internal state and update the element's class.
+ assert.equal(element.index, 2);
+ assert.equal(element.target, list.children[2]);
+ assert.isTrue(list.children[2].classList.contains('targeted'));
+ assert.isFalse(element.isAtStart());
+ assert.isFalse(element.isAtEnd());
+
+ // Progress the cursor.
+ element.next();
+
+ // Confirm that the next stop is selected and that the previous stop is
+ // unselected.
+ assert.equal(element.index, 3);
+ assert.equal(element.target, list.children[3]);
+ assert.isTrue(element.isAtEnd());
+ assert.isFalse(list.children[2].classList.contains('targeted'));
+ assert.isTrue(list.children[3].classList.contains('targeted'));
+
+ // Progress the cursor.
+ element.next();
+
+ // We should still be at the end.
+ assert.equal(element.index, 3);
+ assert.equal(element.target, list.children[3]);
+ assert.isTrue(element.isAtEnd());
+
+ // Wind the cursor all the way back to the first stop.
+ element.previous();
+ element.previous();
+ element.previous();
+
+ // The element state should reflect the end of the list.
+ assert.equal(element.index, 0);
+ assert.equal(element.target, list.children[0]);
+ assert.isTrue(element.isAtStart());
+ assert.isTrue(list.children[0].classList.contains('targeted'));
+
+ const newLi = document.createElement('li');
+ newLi.textContent = 'Z';
+ list.insertBefore(newLi, list.children[0]);
+ element.stops = list.querySelectorAll('li');
+
+ assert.equal(element.index, 1);
+
+ // De-select all targets.
+ element.unsetCursor();
+
+ // There should now be no cursor target.
+ assert.isFalse(list.children[1].classList.contains('targeted'));
+ assert.isNotOk(element.target);
+ assert.equal(element.index, -1);
+ });
+
+ test('_moveCursor', () => {
+ // Initialize the cursor with its stops.
+ element.stops = list.querySelectorAll('li');
+ // Select the first stop.
+ element.setCursor(list.children[0]);
+ const getTargetHeight = sinon.stub();
+
+ // Move the cursor without an optional get target height function.
+ element._moveCursor(1);
+ assert.isFalse(getTargetHeight.called);
+
+ // Move the cursor with an optional get target height function.
+ element._moveCursor(1, null, getTargetHeight);
+ assert.isTrue(getTargetHeight.called);
+ });
+
+ test('_moveCursor from -1 does not check height', () => {
+ element.stops = list.querySelectorAll('li');
+ const getTargetHeight = sinon.stub();
+ element._moveCursor(1, () => false, getTargetHeight);
+ assert.isFalse(getTargetHeight.called);
+ });
+
+ test('opt_noScroll', () => {
+ sandbox.stub(element, '_targetIsVisible', () => false);
+ const scrollStub = sandbox.stub(window, 'scrollTo');
+ element.stops = list.querySelectorAll('li');
+ element.scrollBehavior = 'keep-visible';
+
+ element.setCursorAtIndex(1, true);
+ assert.isFalse(scrollStub.called);
+
+ element.setCursorAtIndex(2);
+ assert.isTrue(scrollStub.called);
+ });
+
+ test('_getNextindex', () => {
+ const isLetterB = function(row) {
+ return row.textContent === 'B';
+ };
+ element.stops = list.querySelectorAll('li');
+ // Start cursor at the first stop.
+ element.setCursor(list.children[0]);
+
+ // Move forward to meet the next condition.
+ assert.equal(element._getNextindex(1, isLetterB), 1);
+ element.index = 1;
+
+ // Nothing else meets the condition, should be at last stop.
+ assert.equal(element._getNextindex(1, isLetterB), 3);
+ element.index = 3;
+
+ // Should stay at last stop if try to proceed.
+ assert.equal(element._getNextindex(1, isLetterB), 3);
+
+ // Go back to the previous condition met. Should be back at.
+ // stop 1.
+ assert.equal(element._getNextindex(-1, isLetterB), 1);
+ element.index = 1;
+
+ // Go back. No more meet the condition. Should be at stop 0.
+ assert.equal(element._getNextindex(-1, isLetterB), 0);
+ });
+
+ test('focusOnMove prop', () => {
+ const listEls = list.querySelectorAll('li');
+ for (let i = 0; i < listEls.length; i++) {
+ sandbox.spy(listEls[i], 'focus');
+ }
+ element.stops = listEls;
+ element.setCursor(list.children[0]);
+
+ element.focusOnMove = false;
+ element.next();
+ assert.isFalse(element.target.focus.called);
+
+ element.focusOnMove = true;
+ element.next();
+ assert.isTrue(element.target.focus.called);
+ });
+
+ suite('_scrollToTarget', () => {
+ let scrollStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- const fixtureElements = fixture('basic');
- element = fixtureElements[0];
- list = fixtureElements[1];
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('core cursor functionality', () => {
- // The element is initialized into the proper state.
- assert.isArray(element.stops);
- assert.equal(element.stops.length, 0);
- assert.equal(element.index, -1);
- assert.isNotOk(element.target);
-
- // Initialize the cursor with its stops.
- element.stops = list.querySelectorAll('li');
-
- // It should have the stops but it should not be targeting any of them.
- assert.isNotNull(element.stops);
- assert.equal(element.stops.length, 4);
- assert.equal(element.index, -1);
- assert.isNotOk(element.target);
-
- // Select the third stop.
- element.setCursor(list.children[2]);
-
- // It should update its internal state and update the element's class.
- assert.equal(element.index, 2);
- assert.equal(element.target, list.children[2]);
- assert.isTrue(list.children[2].classList.contains('targeted'));
- assert.isFalse(element.isAtStart());
- assert.isFalse(element.isAtEnd());
-
- // Progress the cursor.
- element.next();
-
- // Confirm that the next stop is selected and that the previous stop is
- // unselected.
- assert.equal(element.index, 3);
- assert.equal(element.target, list.children[3]);
- assert.isTrue(element.isAtEnd());
- assert.isFalse(list.children[2].classList.contains('targeted'));
- assert.isTrue(list.children[3].classList.contains('targeted'));
-
- // Progress the cursor.
- element.next();
-
- // We should still be at the end.
- assert.equal(element.index, 3);
- assert.equal(element.target, list.children[3]);
- assert.isTrue(element.isAtEnd());
-
- // Wind the cursor all the way back to the first stop.
- element.previous();
- element.previous();
- element.previous();
-
- // The element state should reflect the end of the list.
- assert.equal(element.index, 0);
- assert.equal(element.target, list.children[0]);
- assert.isTrue(element.isAtStart());
- assert.isTrue(list.children[0].classList.contains('targeted'));
-
- const newLi = document.createElement('li');
- newLi.textContent = 'Z';
- list.insertBefore(newLi, list.children[0]);
- element.stops = list.querySelectorAll('li');
-
- assert.equal(element.index, 1);
-
- // De-select all targets.
- element.unsetCursor();
-
- // There should now be no cursor target.
- assert.isFalse(list.children[1].classList.contains('targeted'));
- assert.isNotOk(element.target);
- assert.equal(element.index, -1);
- });
-
- test('_moveCursor', () => {
- // Initialize the cursor with its stops.
- element.stops = list.querySelectorAll('li');
- // Select the first stop.
- element.setCursor(list.children[0]);
- const getTargetHeight = sinon.stub();
-
- // Move the cursor without an optional get target height function.
- element._moveCursor(1);
- assert.isFalse(getTargetHeight.called);
-
- // Move the cursor with an optional get target height function.
- element._moveCursor(1, null, getTargetHeight);
- assert.isTrue(getTargetHeight.called);
- });
-
- test('_moveCursor from -1 does not check height', () => {
- element.stops = list.querySelectorAll('li');
- const getTargetHeight = sinon.stub();
- element._moveCursor(1, () => false, getTargetHeight);
- assert.isFalse(getTargetHeight.called);
- });
-
- test('opt_noScroll', () => {
- sandbox.stub(element, '_targetIsVisible', () => false);
- const scrollStub = sandbox.stub(window, 'scrollTo');
element.stops = list.querySelectorAll('li');
element.scrollBehavior = 'keep-visible';
- element.setCursorAtIndex(1, true);
- assert.isFalse(scrollStub.called);
+ // There is a target which has a targetNext
+ element.setCursor(list.children[0]);
+ element._moveCursor(1);
+ scrollStub = sandbox.stub(window, 'scrollTo');
+ window.innerHeight = 60;
+ });
- element.setCursorAtIndex(2);
+ test('Called when top and bottom not visible', () => {
+ sandbox.stub(element, '_targetIsVisible').returns(false);
+ element._scrollToTarget();
assert.isTrue(scrollStub.called);
});
- test('_getNextindex', () => {
- const isLetterB = function(row) {
- return row.textContent === 'B';
- };
- element.stops = list.querySelectorAll('li');
- // Start cursor at the first stop.
- element.setCursor(list.children[0]);
-
- // Move forward to meet the next condition.
- assert.equal(element._getNextindex(1, isLetterB), 1);
- element.index = 1;
-
- // Nothing else meets the condition, should be at last stop.
- assert.equal(element._getNextindex(1, isLetterB), 3);
- element.index = 3;
-
- // Should stay at last stop if try to proceed.
- assert.equal(element._getNextindex(1, isLetterB), 3);
-
- // Go back to the previous condition met. Should be back at.
- // stop 1.
- assert.equal(element._getNextindex(-1, isLetterB), 1);
- element.index = 1;
-
- // Go back. No more meet the condition. Should be at stop 0.
- assert.equal(element._getNextindex(-1, isLetterB), 0);
+ test('Not called when top and bottom visible', () => {
+ sandbox.stub(element, '_targetIsVisible').returns(true);
+ element._scrollToTarget();
+ assert.isFalse(scrollStub.called);
});
- test('focusOnMove prop', () => {
- const listEls = list.querySelectorAll('li');
- for (let i = 0; i < listEls.length; i++) {
- sandbox.spy(listEls[i], 'focus');
- }
- element.stops = listEls;
- element.setCursor(list.children[0]);
-
- element.focusOnMove = false;
- element.next();
- assert.isFalse(element.target.focus.called);
-
- element.focusOnMove = true;
- element.next();
- assert.isTrue(element.target.focus.called);
+ test('Called when top is visible, bottom is not, scroll is lower', () => {
+ const visibleStub = sandbox.stub(element, '_targetIsVisible',
+ () => visibleStub.callCount === 2);
+ sandbox.stub(element, '_getWindowDims').returns({
+ scrollX: 123,
+ scrollY: 15,
+ innerHeight: 1000,
+ pageYOffset: 0,
+ });
+ sandbox.stub(element, '_calculateScrollToValue').returns(20);
+ element._scrollToTarget();
+ assert.isTrue(scrollStub.called);
+ assert.isTrue(scrollStub.calledWithExactly(123, 20));
+ assert.equal(visibleStub.callCount, 2);
});
- suite('_scrollToTarget', () => {
- let scrollStub;
- setup(() => {
- element.stops = list.querySelectorAll('li');
- element.scrollBehavior = 'keep-visible';
-
- // There is a target which has a targetNext
- element.setCursor(list.children[0]);
- element._moveCursor(1);
- scrollStub = sandbox.stub(window, 'scrollTo');
- window.innerHeight = 60;
+ test('Called when top is visible, bottom not, scroll is higher', () => {
+ const visibleStub = sandbox.stub(element, '_targetIsVisible',
+ () => visibleStub.callCount === 2);
+ sandbox.stub(element, '_getWindowDims').returns({
+ scrollX: 123,
+ scrollY: 25,
+ innerHeight: 1000,
+ pageYOffset: 0,
});
+ sandbox.stub(element, '_calculateScrollToValue').returns(20);
+ element._scrollToTarget();
+ assert.isFalse(scrollStub.called);
+ assert.equal(visibleStub.callCount, 2);
+ });
- test('Called when top and bottom not visible', () => {
- sandbox.stub(element, '_targetIsVisible').returns(false);
- element._scrollToTarget();
- assert.isTrue(scrollStub.called);
+ test('_calculateScrollToValue', () => {
+ sandbox.stub(element, '_getWindowDims').returns({
+ scrollX: 123,
+ scrollY: 25,
+ innerHeight: 300,
+ pageYOffset: 0,
});
-
- test('Not called when top and bottom visible', () => {
- sandbox.stub(element, '_targetIsVisible').returns(true);
- element._scrollToTarget();
- assert.isFalse(scrollStub.called);
- });
-
- test('Called when top is visible, bottom is not, scroll is lower', () => {
- const visibleStub = sandbox.stub(element, '_targetIsVisible',
- () => visibleStub.callCount === 2);
- sandbox.stub(element, '_getWindowDims').returns({
- scrollX: 123,
- scrollY: 15,
- innerHeight: 1000,
- pageYOffset: 0,
- });
- sandbox.stub(element, '_calculateScrollToValue').returns(20);
- element._scrollToTarget();
- assert.isTrue(scrollStub.called);
- assert.isTrue(scrollStub.calledWithExactly(123, 20));
- assert.equal(visibleStub.callCount, 2);
- });
-
- test('Called when top is visible, bottom not, scroll is higher', () => {
- const visibleStub = sandbox.stub(element, '_targetIsVisible',
- () => visibleStub.callCount === 2);
- sandbox.stub(element, '_getWindowDims').returns({
- scrollX: 123,
- scrollY: 25,
- innerHeight: 1000,
- pageYOffset: 0,
- });
- sandbox.stub(element, '_calculateScrollToValue').returns(20);
- element._scrollToTarget();
- assert.isFalse(scrollStub.called);
- assert.equal(visibleStub.callCount, 2);
- });
-
- test('_calculateScrollToValue', () => {
- sandbox.stub(element, '_getWindowDims').returns({
- scrollX: 123,
- scrollY: 25,
- innerHeight: 300,
- pageYOffset: 0,
- });
- assert.equal(element._calculateScrollToValue(1000, {offsetHeight: 10}),
- 905);
- });
+ assert.equal(element._calculateScrollToValue(1000, {offsetHeight: 10}),
+ 905);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 7be041b..c46bb17 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.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,243 +14,253 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const Duration = {
- HOUR: 1000 * 60 * 60,
- DAY: 1000 * 60 * 60 * 24,
- };
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../../../styles/shared-styles.js';
+import '../../../scripts/util.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-date-formatter_html.js';
- const TimeFormats = {
- TIME_12: 'h:mm A', // 2:14 PM
- TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM
- TIME_24: 'HH:mm', // 14:14
- TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00
- };
+const Duration = {
+ HOUR: 1000 * 60 * 60,
+ DAY: 1000 * 60 * 60 * 24,
+};
- const DateFormats = {
- STD: {
- short: 'MMM DD', // Aug 29
- full: 'MMM DD, YYYY', // Aug 29, 1997
- },
- US: {
- short: 'MM/DD', // 08/29
- full: 'MM/DD/YY', // 08/29/97
- },
- ISO: {
- short: 'MM-DD', // 08-29
- full: 'YYYY-MM-DD', // 1997-08-29
- },
- EURO: {
- short: 'DD. MMM', // 29. Aug
- full: 'DD.MM.YYYY', // 29.08.1997
- },
- UK: {
- short: 'DD/MM', // 29/08
- full: 'DD/MM/YYYY', // 29/08/1997
- },
- };
+const TimeFormats = {
+ TIME_12: 'h:mm A', // 2:14 PM
+ TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM
+ TIME_24: 'HH:mm', // 14:14
+ TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00
+};
- /**
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrDateFormatter extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-date-formatter'; }
+const DateFormats = {
+ STD: {
+ short: 'MMM DD', // Aug 29
+ full: 'MMM DD, YYYY', // Aug 29, 1997
+ },
+ US: {
+ short: 'MM/DD', // 08/29
+ full: 'MM/DD/YY', // 08/29/97
+ },
+ ISO: {
+ short: 'MM-DD', // 08-29
+ full: 'YYYY-MM-DD', // 1997-08-29
+ },
+ EURO: {
+ short: 'DD. MMM', // 29. Aug
+ full: 'DD.MM.YYYY', // 29.08.1997
+ },
+ UK: {
+ short: 'DD/MM', // 29/08
+ full: 'DD/MM/YYYY', // 29/08/1997
+ },
+};
- static get properties() {
- return {
- dateStr: {
- type: String,
- value: null,
- notify: true,
- },
- showDateAndTime: {
- type: Boolean,
- value: false,
- },
+/**
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrDateFormatter extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /**
- * When true, the detailed date appears in a GR-TOOLTIP rather than in the
- * native browser tooltip.
- */
- hasTooltip: Boolean,
+ static get is() { return 'gr-date-formatter'; }
- /**
- * The title to be used as the native tooltip or by the tooltip behavior.
- */
- title: {
- type: String,
- reflectToAttribute: true,
- computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
- },
+ static get properties() {
+ return {
+ dateStr: {
+ type: String,
+ value: null,
+ notify: true,
+ },
+ showDateAndTime: {
+ type: Boolean,
+ value: false,
+ },
- /** @type {?{short: string, full: string}} */
- _dateFormat: Object,
- _timeFormat: String, // No default value to prevent flickering.
- _relative: Boolean, // No default value to prevent flickering.
- };
- }
+ /**
+ * When true, the detailed date appears in a GR-TOOLTIP rather than in the
+ * native browser tooltip.
+ */
+ hasTooltip: Boolean,
- /** @override */
- attached() {
- super.attached();
- this._loadPreferences();
- }
+ /**
+ * The title to be used as the native tooltip or by the tooltip behavior.
+ */
+ title: {
+ type: String,
+ reflectToAttribute: true,
+ computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
+ },
- _getUtcOffsetString() {
- return ' UTC' + moment().format('Z');
- }
+ /** @type {?{short: string, full: string}} */
+ _dateFormat: Object,
+ _timeFormat: String, // No default value to prevent flickering.
+ _relative: Boolean, // No default value to prevent flickering.
+ };
+ }
- _loadPreferences() {
- return this._getLoggedIn().then(loggedIn => {
- if (!loggedIn) {
- this._timeFormat = TimeFormats.TIME_24;
- this._dateFormat = DateFormats.STD;
- this._relative = false;
- return;
- }
- return Promise.all([
- this._loadTimeFormat(),
- this._loadRelative(),
- ]);
- });
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._loadPreferences();
+ }
- _loadTimeFormat() {
- return this._getPreferences().then(preferences => {
- const timeFormat = preferences && preferences.time_format;
- const dateFormat = preferences && preferences.date_format;
- this._decideTimeFormat(timeFormat);
- this._decideDateFormat(dateFormat);
- });
- }
+ _getUtcOffsetString() {
+ return ' UTC' + moment().format('Z');
+ }
- _decideTimeFormat(timeFormat) {
- switch (timeFormat) {
- case 'HHMM_12':
- this._timeFormat = TimeFormats.TIME_12;
- break;
- case 'HHMM_24':
- this._timeFormat = TimeFormats.TIME_24;
- break;
- default:
- throw Error('Invalid time format: ' + timeFormat);
+ _loadPreferences() {
+ return this._getLoggedIn().then(loggedIn => {
+ if (!loggedIn) {
+ this._timeFormat = TimeFormats.TIME_24;
+ this._dateFormat = DateFormats.STD;
+ this._relative = false;
+ return;
}
- }
+ return Promise.all([
+ this._loadTimeFormat(),
+ this._loadRelative(),
+ ]);
+ });
+ }
- _decideDateFormat(dateFormat) {
- switch (dateFormat) {
- case 'STD':
- this._dateFormat = DateFormats.STD;
- break;
- case 'US':
- this._dateFormat = DateFormats.US;
- break;
- case 'ISO':
- this._dateFormat = DateFormats.ISO;
- break;
- case 'EURO':
- this._dateFormat = DateFormats.EURO;
- break;
- case 'UK':
- this._dateFormat = DateFormats.UK;
- break;
- default:
- throw Error('Invalid date format: ' + dateFormat);
- }
- }
+ _loadTimeFormat() {
+ return this._getPreferences().then(preferences => {
+ const timeFormat = preferences && preferences.time_format;
+ const dateFormat = preferences && preferences.date_format;
+ this._decideTimeFormat(timeFormat);
+ this._decideDateFormat(dateFormat);
+ });
+ }
- _loadRelative() {
- return this._getPreferences().then(prefs => {
- // prefs.relative_date_in_change_table is not set when false.
- this._relative = !!(prefs && prefs.relative_date_in_change_table);
- });
- }
-
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
-
- _getPreferences() {
- return this.$.restAPI.getPreferences();
- }
-
- /**
- * Return true if date is within 24 hours and on the same day.
- */
- _isWithinDay(now, date) {
- const diff = -date.diff(now);
- return diff < Duration.DAY && date.day() === now.getDay();
- }
-
- /**
- * Returns true if date is from one to six months.
- */
- _isWithinHalfYear(now, date) {
- const diff = -date.diff(now);
- return (date.day() !== now.getDay() || diff >= Duration.DAY) &&
- diff < 180 * Duration.DAY;
- }
-
- _computeDateStr(
- dateStr, timeFormat, dateFormat, relative, showDateAndTime
- ) {
- if (!dateStr || !timeFormat || !dateFormat) { return ''; }
- const date = moment(util.parseDate(dateStr));
- if (!date.isValid()) { return ''; }
- if (relative) {
- const dateFromNow = date.fromNow();
- if (dateFromNow === 'a few seconds ago') {
- return 'just now';
- } else {
- return dateFromNow;
- }
- }
- const now = new Date();
- let format = dateFormat.full;
- if (this._isWithinDay(now, date)) {
- format = timeFormat;
- } else {
- if (this._isWithinHalfYear(now, date)) {
- format = dateFormat.short;
- }
- if (this.showDateAndTime) {
- format = `${format} ${timeFormat}`;
- }
- }
- return date.format(format);
- }
-
- _timeToSecondsFormat(timeFormat) {
- return timeFormat === TimeFormats.TIME_12 ?
- TimeFormats.TIME_12_WITH_SEC :
- TimeFormats.TIME_24_WITH_SEC;
- }
-
- _computeFullDateStr(dateStr, timeFormat, dateFormat) {
- // Polymer 2: check for undefined
- if ([
- dateStr,
- timeFormat,
- dateFormat,
- ].some(arg => arg === undefined)) {
- return undefined;
- }
-
- if (!dateStr) { return ''; }
- const date = moment(util.parseDate(dateStr));
- if (!date.isValid()) { return ''; }
- let format = dateFormat.full + ', ';
- format += this._timeToSecondsFormat(timeFormat);
- return date.format(format) + this._getUtcOffsetString();
+ _decideTimeFormat(timeFormat) {
+ switch (timeFormat) {
+ case 'HHMM_12':
+ this._timeFormat = TimeFormats.TIME_12;
+ break;
+ case 'HHMM_24':
+ this._timeFormat = TimeFormats.TIME_24;
+ break;
+ default:
+ throw Error('Invalid time format: ' + timeFormat);
}
}
- customElements.define(GrDateFormatter.is, GrDateFormatter);
-})();
+ _decideDateFormat(dateFormat) {
+ switch (dateFormat) {
+ case 'STD':
+ this._dateFormat = DateFormats.STD;
+ break;
+ case 'US':
+ this._dateFormat = DateFormats.US;
+ break;
+ case 'ISO':
+ this._dateFormat = DateFormats.ISO;
+ break;
+ case 'EURO':
+ this._dateFormat = DateFormats.EURO;
+ break;
+ case 'UK':
+ this._dateFormat = DateFormats.UK;
+ break;
+ default:
+ throw Error('Invalid date format: ' + dateFormat);
+ }
+ }
+
+ _loadRelative() {
+ return this._getPreferences().then(prefs => {
+ // prefs.relative_date_in_change_table is not set when false.
+ this._relative = !!(prefs && prefs.relative_date_in_change_table);
+ });
+ }
+
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _getPreferences() {
+ return this.$.restAPI.getPreferences();
+ }
+
+ /**
+ * Return true if date is within 24 hours and on the same day.
+ */
+ _isWithinDay(now, date) {
+ const diff = -date.diff(now);
+ return diff < Duration.DAY && date.day() === now.getDay();
+ }
+
+ /**
+ * Returns true if date is from one to six months.
+ */
+ _isWithinHalfYear(now, date) {
+ const diff = -date.diff(now);
+ return (date.day() !== now.getDay() || diff >= Duration.DAY) &&
+ diff < 180 * Duration.DAY;
+ }
+
+ _computeDateStr(
+ dateStr, timeFormat, dateFormat, relative, showDateAndTime
+ ) {
+ if (!dateStr || !timeFormat || !dateFormat) { return ''; }
+ const date = moment(util.parseDate(dateStr));
+ if (!date.isValid()) { return ''; }
+ if (relative) {
+ const dateFromNow = date.fromNow();
+ if (dateFromNow === 'a few seconds ago') {
+ return 'just now';
+ } else {
+ return dateFromNow;
+ }
+ }
+ const now = new Date();
+ let format = dateFormat.full;
+ if (this._isWithinDay(now, date)) {
+ format = timeFormat;
+ } else {
+ if (this._isWithinHalfYear(now, date)) {
+ format = dateFormat.short;
+ }
+ if (this.showDateAndTime) {
+ format = `${format} ${timeFormat}`;
+ }
+ }
+ return date.format(format);
+ }
+
+ _timeToSecondsFormat(timeFormat) {
+ return timeFormat === TimeFormats.TIME_12 ?
+ TimeFormats.TIME_12_WITH_SEC :
+ TimeFormats.TIME_24_WITH_SEC;
+ }
+
+ _computeFullDateStr(dateStr, timeFormat, dateFormat) {
+ // Polymer 2: check for undefined
+ if ([
+ dateStr,
+ timeFormat,
+ dateFormat,
+ ].some(arg => arg === undefined)) {
+ return undefined;
+ }
+
+ if (!dateStr) { return ''; }
+ const date = moment(util.parseDate(dateStr));
+ if (!date.isValid()) { return ''; }
+ let format = dateFormat.full + ', ';
+ format += this._timeToSecondsFormat(timeFormat);
+ return date.format(format) + this._getUtcOffsetString();
+ }
+}
+
+customElements.define(GrDateFormatter.is, GrDateFormatter);
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js
index ae5a945..19aa143 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<script src="../../../scripts/util.js"></script>
-
-<dom-module id="gr-date-formatter">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
color: inherit;
@@ -34,6 +27,4 @@
[[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative, showDateAndTime)]]
</span>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-date-formatter.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 0b572bf..65f2248 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-date-formatter</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-date-formatter.html">
+<script type="module" src="./gr-date-formatter.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-date-formatter.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,416 +43,419 @@
</template>
</test-fixture>
-<script>
- suite('gr-date-formatter tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-date-formatter.js';
+suite('gr-date-formatter tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ /**
+ * Parse server-formatter date and normalize into current timezone.
+ */
+ function normalizedDate(dateStr) {
+ const d = util.parseDate(dateStr);
+ d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
+ return d;
+ }
+
+ function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
+ expectedTooltip, done) {
+ // Normalize and convert the date to mimic server response.
+ dateStr = normalizedDate(dateStr)
+ .toJSON()
+ .replace('T', ' ')
+ .slice(0, -1);
+ sandbox.useFakeTimers(normalizedDate(nowStr).getTime());
+ element.dateStr = dateStr;
+ flush(() => {
+ const span = element.shadowRoot
+ .querySelector('span');
+ assert.equal(span.textContent.trim(), expected);
+ assert.equal(element.title, expectedTooltip);
+ element.showDateAndTime = true;
+ flushAsynchronousOperations();
+ assert.equal(span.textContent.trim(), expectedWithDateAndTime);
+ done();
+ });
+ }
+
+ function stubRestAPI(preferences) {
+ const loggedInPromise = Promise.resolve(preferences !== null);
+ const preferencesPromise = Promise.resolve(preferences);
+ stub('gr-rest-api-interface', {
+ getLoggedIn: sinon.stub().returns(loggedInPromise),
+ getPreferences: sinon.stub().returns(preferencesPromise),
+ });
+ return Promise.all([loggedInPromise, preferencesPromise]);
+ }
+
+ suite('STD + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'STD',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('invalid dates are quietly rejected', () => {
+ assert.notOk((new Date('foo')).valueOf());
+ assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
});
- teardown(() => {
- sandbox.restore();
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ 'Jul 29, 2015, 15:34:14', done);
});
- /**
- * Parse server-formatter date and normalize into current timezone.
- */
- function normalizedDate(dateStr) {
- const d = util.parseDate(dateStr);
- d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
- return d;
- }
-
- function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
- expectedTooltip, done) {
- // Normalize and convert the date to mimic server response.
- dateStr = normalizedDate(dateStr)
- .toJSON()
- .replace('T', ' ')
- .slice(0, -1);
- sandbox.useFakeTimers(normalizedDate(nowStr).getTime());
- element.dateStr = dateStr;
- flush(() => {
- const span = element.shadowRoot
- .querySelector('span');
- assert.equal(span.textContent.trim(), expected);
- assert.equal(element.title, expectedTooltip);
- element.showDateAndTime = true;
- flushAsynchronousOperations();
- assert.equal(span.textContent.trim(), expectedWithDateAndTime);
- done();
- });
- }
-
- function stubRestAPI(preferences) {
- const loggedInPromise = Promise.resolve(preferences !== null);
- const preferencesPromise = Promise.resolve(preferences);
- stub('gr-rest-api-interface', {
- getLoggedIn: sinon.stub().returns(loggedInPromise),
- getPreferences: sinon.stub().returns(preferencesPromise),
- });
- return Promise.all([loggedInPromise, preferencesPromise]);
- }
-
- suite('STD + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'STD',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('invalid dates are quietly rejected', () => {
- assert.notOk((new Date('foo')).valueOf());
- assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
- });
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- 'Jul 29, 2015, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- 'Jul 28',
- 'Jul 28 20:25',
- 'Jul 28, 2015, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- 'Jun 15',
- 'Jun 15 03:25',
- 'Jun 15, 2015, 03:25:14', done);
- });
-
- test('More than six months', done => {
- testDates('2015-09-15 20:34:00.000000000',
- '2015-01-15 03:25:00.000000000',
- 'Jan 15, 2015',
- 'Jan 15, 2015 03:25',
- 'Jan 15, 2015, 03:25:00', done);
- });
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ 'Jul 28',
+ 'Jul 28 20:25',
+ 'Jul 28, 2015, 20:25:14', done);
});
- suite('US + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'US',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '07/29/15, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '07/28',
- '07/28 20:25',
- '07/28/15, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '06/15',
- '06/15 03:25',
- '06/15/15, 03:25:14', done);
- });
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ 'Jun 15',
+ 'Jun 15 03:25',
+ 'Jun 15, 2015, 03:25:14', done);
});
- suite('ISO + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'ISO',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '2015-07-29, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '07-28',
- '07-28 20:25',
- '2015-07-28, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '06-15',
- '06-15 03:25',
- '2015-06-15, 03:25:14', done);
- });
- });
-
- suite('EURO + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'EURO',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '29.07.2015, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '28. Jul',
- '28. Jul 20:25',
- '28.07.2015, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '15. Jun',
- '15. Jun 03:25',
- '15.06.2015, 03:25:14', done);
- });
- });
-
- suite('UK + 24 hours time format preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'UK',
- relative_date_in_change_table: false,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '15:34',
- '15:34',
- '29/07/2015, 15:34:14', done);
- });
-
- test('Within 24 hours on different days', done => {
- testDates('2015-07-29 03:34:14.985000000',
- '2015-07-28 20:25:14.985000000',
- '28/07',
- '28/07 20:25',
- '28/07/2015, 20:25:14', done);
- });
-
- test('More than 24 hours but less than six months', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-06-15 03:25:14.985000000',
- '15/06',
- '15/06 03:25',
- '15/06/2015, 03:25:14', done);
- });
- });
-
- suite('STD + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'STD'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- 'Jul 29, 2015, 3:34:14 PM', done);
- });
- });
-
- suite('US + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'US'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '07/29/15, 3:34:14 PM', done);
- });
- });
-
- suite('ISO + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'ISO'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '2015-07-29, 3:34:14 PM', done);
- });
- });
-
- suite('EURO + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'EURO'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '29.07.2015, 3:34:14 PM', done);
- });
- });
-
- suite('UK + 12 hours time format preference', () => {
- setup(() =>
- // relative_date_in_change_table is not set when false.
- stubRestAPI(
- {time_format: 'HHMM_12', date_format: 'UK'}
- ).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- })
- );
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '3:34 PM',
- '3:34 PM',
- '29/07/2015, 3:34:14 PM', done);
- });
- });
-
- suite('relative date preference', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_12',
- date_format: 'STD',
- relative_date_in_change_table: true,
- }).then(() => {
- element = fixture('basic');
- sandbox.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- }));
-
- test('Within 24 hours on same day', done => {
- testDates('2015-07-29 20:34:14.985000000',
- '2015-07-29 15:34:14.985000000',
- '5 hours ago',
- '5 hours ago',
- 'Jul 29, 2015, 3:34:14 PM', done);
- });
-
- test('More than six months', done => {
- testDates('2015-09-15 20:34:00.000000000',
- '2015-01-15 03:25:00.000000000',
- '8 months ago',
- '8 months ago',
- 'Jan 15, 2015, 3:25:00 AM', done);
- });
- });
-
- suite('logged in', () => {
- setup(() => stubRestAPI({
- time_format: 'HHMM_12',
- date_format: 'US',
- relative_date_in_change_table: true,
- }).then(() => {
- element = fixture('basic');
- return element._loadPreferences();
- }));
-
- test('Preferences are respected', () => {
- assert.equal(element._timeFormat, 'h:mm A');
- assert.equal(element._dateFormat.short, 'MM/DD');
- assert.equal(element._dateFormat.full, 'MM/DD/YY');
- assert.isTrue(element._relative);
- });
- });
-
- suite('logged out', () => {
- setup(() => stubRestAPI(null).then(() => {
- element = fixture('basic');
- return element._loadPreferences();
- }));
-
- test('Default preferences are respected', () => {
- assert.equal(element._timeFormat, 'HH:mm');
- assert.equal(element._dateFormat.short, 'MMM DD');
- assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
- assert.isFalse(element._relative);
- });
+ test('More than six months', done => {
+ testDates('2015-09-15 20:34:00.000000000',
+ '2015-01-15 03:25:00.000000000',
+ 'Jan 15, 2015',
+ 'Jan 15, 2015 03:25',
+ 'Jan 15, 2015, 03:25:00', done);
});
});
+
+ suite('US + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'US',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '07/29/15, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '07/28',
+ '07/28 20:25',
+ '07/28/15, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '06/15',
+ '06/15 03:25',
+ '06/15/15, 03:25:14', done);
+ });
+ });
+
+ suite('ISO + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'ISO',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '2015-07-29, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '07-28',
+ '07-28 20:25',
+ '2015-07-28, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '06-15',
+ '06-15 03:25',
+ '2015-06-15, 03:25:14', done);
+ });
+ });
+
+ suite('EURO + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'EURO',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '29.07.2015, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '28. Jul',
+ '28. Jul 20:25',
+ '28.07.2015, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '15. Jun',
+ '15. Jun 03:25',
+ '15.06.2015, 03:25:14', done);
+ });
+ });
+
+ suite('UK + 24 hours time format preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_24',
+ date_format: 'UK',
+ relative_date_in_change_table: false,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '15:34',
+ '15:34',
+ '29/07/2015, 15:34:14', done);
+ });
+
+ test('Within 24 hours on different days', done => {
+ testDates('2015-07-29 03:34:14.985000000',
+ '2015-07-28 20:25:14.985000000',
+ '28/07',
+ '28/07 20:25',
+ '28/07/2015, 20:25:14', done);
+ });
+
+ test('More than 24 hours but less than six months', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-06-15 03:25:14.985000000',
+ '15/06',
+ '15/06 03:25',
+ '15/06/2015, 03:25:14', done);
+ });
+ });
+
+ suite('STD + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'STD'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ 'Jul 29, 2015, 3:34:14 PM', done);
+ });
+ });
+
+ suite('US + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'US'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '07/29/15, 3:34:14 PM', done);
+ });
+ });
+
+ suite('ISO + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'ISO'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '2015-07-29, 3:34:14 PM', done);
+ });
+ });
+
+ suite('EURO + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'EURO'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '29.07.2015, 3:34:14 PM', done);
+ });
+ });
+
+ suite('UK + 12 hours time format preference', () => {
+ setup(() =>
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI(
+ {time_format: 'HHMM_12', date_format: 'UK'}
+ ).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ })
+ );
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '3:34 PM',
+ '3:34 PM',
+ '29/07/2015, 3:34:14 PM', done);
+ });
+ });
+
+ suite('relative date preference', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_12',
+ date_format: 'STD',
+ relative_date_in_change_table: true,
+ }).then(() => {
+ element = fixture('basic');
+ sandbox.stub(element, '_getUtcOffsetString').returns('');
+ return element._loadPreferences();
+ }));
+
+ test('Within 24 hours on same day', done => {
+ testDates('2015-07-29 20:34:14.985000000',
+ '2015-07-29 15:34:14.985000000',
+ '5 hours ago',
+ '5 hours ago',
+ 'Jul 29, 2015, 3:34:14 PM', done);
+ });
+
+ test('More than six months', done => {
+ testDates('2015-09-15 20:34:00.000000000',
+ '2015-01-15 03:25:00.000000000',
+ '8 months ago',
+ '8 months ago',
+ 'Jan 15, 2015, 3:25:00 AM', done);
+ });
+ });
+
+ suite('logged in', () => {
+ setup(() => stubRestAPI({
+ time_format: 'HHMM_12',
+ date_format: 'US',
+ relative_date_in_change_table: true,
+ }).then(() => {
+ element = fixture('basic');
+ return element._loadPreferences();
+ }));
+
+ test('Preferences are respected', () => {
+ assert.equal(element._timeFormat, 'h:mm A');
+ assert.equal(element._dateFormat.short, 'MM/DD');
+ assert.equal(element._dateFormat.full, 'MM/DD/YY');
+ assert.isTrue(element._relative);
+ });
+ });
+
+ suite('logged out', () => {
+ setup(() => stubRestAPI(null).then(() => {
+ element = fixture('basic');
+ return element._loadPreferences();
+ }));
+
+ test('Default preferences are respected', () => {
+ assert.equal(element._timeFormat, 'HH:mm');
+ assert.equal(element._dateFormat.short, 'MMM DD');
+ assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
+ assert.isFalse(element._relative);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
index 8d00452..8141863 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -14,85 +14,94 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../gr-button/gr-button.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-dialog_html.js';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrDialog extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dialog'; }
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
*/
- class GrDialog extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- confirmLabel: {
- type: String,
- value: 'Confirm',
- },
- // Supplying an empty cancel label will hide the button completely.
- cancelLabel: {
- type: String,
- value: 'Cancel',
- },
- disabled: {
- type: Boolean,
- value: false,
- },
- confirmOnEnter: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
-
- _handleConfirm(e) {
- if (this.disabled) { return; }
-
- e.preventDefault();
- e.stopPropagation();
- this.fire('confirm', null, {bubbles: false});
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.fire('cancel', null, {bubbles: false});
- }
-
- _handleKeydown(e) {
- if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); }
- }
-
- resetFocus() {
- this.$.confirm.focus();
- }
-
- _computeCancelClass(cancelLabel) {
- return cancelLabel.length ? '' : 'hidden';
- }
+ static get properties() {
+ return {
+ confirmLabel: {
+ type: String,
+ value: 'Confirm',
+ },
+ // Supplying an empty cancel label will hide the button completely.
+ cancelLabel: {
+ type: String,
+ value: 'Cancel',
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ },
+ confirmOnEnter: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
- customElements.define(GrDialog.is, GrDialog);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
+
+ _handleConfirm(e) {
+ if (this.disabled) { return; }
+
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('confirm', null, {bubbles: false});
+ }
+
+ _handleCancelTap(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.fire('cancel', null, {bubbles: false});
+ }
+
+ _handleKeydown(e) {
+ if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); }
+ }
+
+ resetFocus() {
+ this.$.confirm.focus();
+ }
+
+ _computeCancelClass(cancelLabel) {
+ return cancelLabel.length ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrDialog.is, GrDialog);
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
index 475f6e2..ba85fd4 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
@@ -1,27 +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="../gr-button/gr-button.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-dialog">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
color: var(--primary-text-color);
@@ -73,14 +68,12 @@
</main>
<footer>
<slot name="footer"></slot>
- <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-click="_handleCancelTap">
+ <gr-button id="cancel" class\$="[[_computeCancelClass(cancelLabel)]]" link="" on-click="_handleCancelTap">
[[cancelLabel]]
</gr-button>
- <gr-button id="confirm" link primary on-click="_handleConfirm" disabled="[[disabled]]">
+ <gr-button id="confirm" link="" primary="" on-click="_handleConfirm" disabled="[[disabled]]">
[[confirmLabel]]
</gr-button>
</footer>
</div>
- </template>
- <script src="gr-dialog.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
index ad93962..87e31a0 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-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-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-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-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,65 +40,67 @@
</template>
</test-fixture>
-<script>
- suite('gr-dialog 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-dialog.js';
+suite('gr-dialog tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('events', done => {
- let numEvents = 0;
- function handler() { if (++numEvents == 2) { done(); } }
-
- element.addEventListener('confirm', handler);
- element.addEventListener('cancel', handler);
-
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button[primary]'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button:not([primary])'));
- });
-
- test('confirmOnEnter', () => {
- element.confirmOnEnter = false;
- const handleConfirmStub = sandbox.stub(element, '_handleConfirm');
- const handleKeydownSpy = sandbox.spy(element, '_handleKeydown');
- MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
- .querySelector('main'),
- 13, null, 'enter');
- flushAsynchronousOperations();
-
- assert.isTrue(handleKeydownSpy.called);
- assert.isFalse(handleConfirmStub.called);
-
- element.confirmOnEnter = true;
- MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
- .querySelector('main'),
- 13, null, 'enter');
- flushAsynchronousOperations();
-
- assert.isTrue(handleConfirmStub.called);
- });
-
- test('resetFocus', () => {
- const focusStub = sandbox.stub(element.$.confirm, 'focus');
- element.resetFocus();
- assert.isTrue(focusStub.calledOnce);
- });
-
- test('empty cancel label hides cancel btn', () => {
- assert.isFalse(isHidden(element.$.cancel));
- element.cancelLabel = '';
- flushAsynchronousOperations();
-
- assert.isTrue(isHidden(element.$.cancel));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('events', done => {
+ let numEvents = 0;
+ function handler() { if (++numEvents == 2) { done(); } }
+
+ element.addEventListener('confirm', handler);
+ element.addEventListener('cancel', handler);
+
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button[primary]'));
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button:not([primary])'));
+ });
+
+ test('confirmOnEnter', () => {
+ element.confirmOnEnter = false;
+ const handleConfirmStub = sandbox.stub(element, '_handleConfirm');
+ const handleKeydownSpy = sandbox.spy(element, '_handleKeydown');
+ MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
+ .querySelector('main'),
+ 13, null, 'enter');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleKeydownSpy.called);
+ assert.isFalse(handleConfirmStub.called);
+
+ element.confirmOnEnter = true;
+ MockInteractions.pressAndReleaseKeyOn(element.shadowRoot
+ .querySelector('main'),
+ 13, null, 'enter');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleConfirmStub.called);
+ });
+
+ test('resetFocus', () => {
+ const focusStub = sandbox.stub(element.$.confirm, 'focus');
+ element.resetFocus();
+ assert.isTrue(focusStub.calledOnce);
+ });
+
+ test('empty cancel label hides cancel btn', () => {
+ assert.isFalse(isHidden(element.$.cancel));
+ element.cancelLabel = '';
+ flushAsynchronousOperations();
+
+ assert.isTrue(isHidden(element.$.cancel));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
index c408e5a..00f9078 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -14,72 +14,82 @@
* 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 GrDiffPreferences extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-diff-preferences'; }
+import '@polymer/iron-input/iron-input.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-select/gr-select.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-diff-preferences_html.js';
- static get properties() {
- return {
- hasUnsavedChanges: {
- type: Boolean,
- notify: true,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrDiffPreferences extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @type {?} */
- diffPrefs: Object,
- };
- }
+ static get is() { return 'gr-diff-preferences'; }
- loadData() {
- return this.$.restAPI.getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
- }
+ static get properties() {
+ return {
+ hasUnsavedChanges: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ },
- _handleDiffPrefsChanged() {
- this.hasUnsavedChanges = true;
- }
-
- _handleLineWrappingTap() {
- this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleShowTabsTap() {
- this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleShowTrailingWhitespaceTap() {
- this.set('diffPrefs.show_whitespace_errors',
- this.$.showTrailingWhitespaceInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleSyntaxHighlightTap() {
- this.set('diffPrefs.syntax_highlighting',
- this.$.syntaxHighlightInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- _handleAutomaticReviewTap() {
- this.set('diffPrefs.manual_review',
- !this.$.automaticReviewInput.checked);
- this._handleDiffPrefsChanged();
- }
-
- save() {
- return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => {
- this.hasUnsavedChanges = false;
- });
- }
+ /** @type {?} */
+ diffPrefs: Object,
+ };
}
- customElements.define(GrDiffPreferences.is, GrDiffPreferences);
-})();
+ loadData() {
+ return this.$.restAPI.getDiffPreferences().then(prefs => {
+ this.diffPrefs = prefs;
+ });
+ }
+
+ _handleDiffPrefsChanged() {
+ this.hasUnsavedChanges = true;
+ }
+
+ _handleLineWrappingTap() {
+ this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleShowTabsTap() {
+ this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleShowTrailingWhitespaceTap() {
+ this.set('diffPrefs.show_whitespace_errors',
+ this.$.showTrailingWhitespaceInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleSyntaxHighlightTap() {
+ this.set('diffPrefs.syntax_highlighting',
+ this.$.syntaxHighlightInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ _handleAutomaticReviewTap() {
+ this.set('diffPrefs.manual_review',
+ !this.$.automaticReviewInput.checked);
+ this._handleDiffPrefsChanged();
+ }
+
+ save() {
+ return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => {
+ this.hasUnsavedChanges = false;
+ });
+ }
+}
+
+customElements.define(GrDiffPreferences.is, GrDiffPreferences);
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js
index 367e30c..7869c2c 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-select/gr-select.html">
-
-<dom-module id="gr-diff-preferences">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -34,12 +27,8 @@
<section>
<span class="title">Context</span>
<span class="value">
- <gr-select
- id="contextSelect"
- bind-value="{{diffPrefs.context}}">
- <select
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
+ <gr-select id="contextSelect" bind-value="{{diffPrefs.context}}">
+ <select on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
<option value="3">3 lines</option>
<option value="10">10 lines</option>
<option value="25">25 lines</option>
@@ -54,117 +43,55 @@
<section>
<span class="title">Fit to screen</span>
<span class="value">
- <input
- id="lineWrappingInput"
- type="checkbox"
- checked$="[[diffPrefs.line_wrapping]]"
- on-change="_handleLineWrappingTap">
+ <input id="lineWrappingInput" type="checkbox" checked\$="[[diffPrefs.line_wrapping]]" on-change="_handleLineWrappingTap">
</span>
</section>
<section>
<span class="title">Diff width</span>
<span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.line_length}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <input
- is="iron-input"
- type="number"
- id="columnsInput"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.line_length}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.line_length}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <input is="iron-input" type="number" id="columnsInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.line_length}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
</iron-input>
</span>
</section>
<section>
<span class="title">Tab width</span>
<span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.tab_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <input
- is="iron-input"
- type="number"
- id="tabSizeInput"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.tab_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.tab_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <input is="iron-input" type="number" id="tabSizeInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.tab_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
</iron-input>
</span>
</section>
- <section hidden$="[[!diffPrefs.font_size]]">
+ <section hidden\$="[[!diffPrefs.font_size]]">
<span class="title">Font size</span>
<span class="value">
- <iron-input
- type="number"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.font_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
- <input
- is="iron-input"
- type="number"
- id="fontSizeInput"
- prevent-invalid-input
- allowed-pattern="[0-9]"
- bind-value="{{diffPrefs.font_size}}"
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
+ <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.font_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
+ <input is="iron-input" type="number" id="fontSizeInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.font_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
</iron-input>
</span>
</section>
<section>
<span class="title">Show tabs</span>
<span class="value">
- <input
- id="showTabsInput"
- type="checkbox"
- checked$="[[diffPrefs.show_tabs]]"
- on-change="_handleShowTabsTap">
+ <input id="showTabsInput" type="checkbox" checked\$="[[diffPrefs.show_tabs]]" on-change="_handleShowTabsTap">
</span>
</section>
<section>
<span class="title">Show trailing whitespace</span>
<span class="value">
- <input
- id="showTrailingWhitespaceInput"
- type="checkbox"
- checked$="[[diffPrefs.show_whitespace_errors]]"
- on-change="_handleShowTrailingWhitespaceTap">
+ <input id="showTrailingWhitespaceInput" type="checkbox" checked\$="[[diffPrefs.show_whitespace_errors]]" on-change="_handleShowTrailingWhitespaceTap">
</span>
</section>
<section>
<span class="title">Syntax highlighting</span>
<span class="value">
- <input
- id="syntaxHighlightInput"
- type="checkbox"
- checked$="[[diffPrefs.syntax_highlighting]]"
- on-change="_handleSyntaxHighlightTap">
+ <input id="syntaxHighlightInput" type="checkbox" checked\$="[[diffPrefs.syntax_highlighting]]" on-change="_handleSyntaxHighlightTap">
</span>
</section>
<section>
<span class="title">Automatically mark viewed files reviewed</span>
<span class="value">
- <input
- id="automaticReviewInput"
- type="checkbox"
- checked$="[[!diffPrefs.manual_review]]"
- on-change="_handleAutomaticReviewTap">
+ <input id="automaticReviewInput" type="checkbox" checked\$="[[!diffPrefs.manual_review]]" on-change="_handleAutomaticReviewTap">
</span>
</section>
<section>
@@ -172,12 +99,10 @@
<span class="title">Ignore Whitespace</span>
<span class="value">
<gr-select bind-value="{{diffPrefs.ignore_whitespace}}">
- <select
- on-keypress="_handleDiffPrefsChanged"
- on-change="_handleDiffPrefsChanged">
+ <select on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged">
<option value="IGNORE_NONE">None</option>
<option value="IGNORE_TRAILING">Trailing</option>
- <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
+ <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
<option value="IGNORE_ALL">All</option>
</select>
</gr-select>
@@ -186,6 +111,4 @@
</section>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-diff-preferences.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
index 3c2a7d1..0387b89 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_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-diff-preferences</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-diff-preferences.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-diff-preferences.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-preferences.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,93 +40,95 @@
</template>
</test-fixture>
-<script>
- suite('gr-diff-preferences tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let diffPreferences;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-diff-preferences.js';
+suite('gr-diff-preferences tests', () => {
+ let element;
+ let sandbox;
+ let diffPreferences;
- function valueOf(title, fieldsetid) {
- const sections = element.$[fieldsetid].querySelectorAll('section');
- let titleEl;
- for (let i = 0; i < sections.length; i++) {
- titleEl = sections[i].querySelector('.title');
- if (titleEl.textContent.trim() === title) {
- return sections[i].querySelector('.value');
- }
+ function valueOf(title, fieldsetid) {
+ const sections = element.$[fieldsetid].querySelectorAll('section');
+ let titleEl;
+ for (let i = 0; i < sections.length; i++) {
+ titleEl = sections[i].querySelector('.title');
+ if (titleEl.textContent.trim() === title) {
+ return sections[i].querySelector('.value');
}
}
+ }
- setup(() => {
- diffPreferences = {
- context: 10,
- line_wrapping: false,
- line_length: 100,
- tab_size: 8,
- font_size: 12,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- manual_review: false,
- ignore_whitespace: 'IGNORE_NONE',
- };
+ setup(() => {
+ diffPreferences = {
+ context: 10,
+ line_wrapping: false,
+ line_length: 100,
+ tab_size: 8,
+ font_size: 12,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ manual_review: false,
+ ignore_whitespace: 'IGNORE_NONE',
+ };
- stub('gr-rest-api-interface', {
- getDiffPreferences() {
- return Promise.resolve(diffPreferences);
- },
- });
-
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- return element.loadData();
+ stub('gr-rest-api-interface', {
+ getDiffPreferences() {
+ return Promise.resolve(diffPreferences);
+ },
});
- teardown(() => { sandbox.restore(); });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ return element.loadData();
+ });
- test('renders', () => {
- // Rendered with the expected preferences selected.
- assert.equal(valueOf('Context', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.context);
- assert.equal(valueOf('Fit to screen', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.line_wrapping);
- assert.equal(valueOf('Diff width', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.line_length);
- assert.equal(valueOf('Tab width', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.tab_size);
- assert.equal(valueOf('Font size', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.font_size);
- assert.equal(valueOf('Show tabs', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.show_tabs);
- assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.show_whitespace_errors);
- assert.equal(valueOf('Syntax highlighting', 'diffPreferences')
- .firstElementChild.checked, diffPreferences.syntax_highlighting);
- assert.equal(
- valueOf('Automatically mark viewed files reviewed', 'diffPreferences')
- .firstElementChild.checked, !diffPreferences.manual_review);
- assert.equal(valueOf('Ignore Whitespace', 'diffPreferences')
- .firstElementChild.bindValue, diffPreferences.ignore_whitespace);
+ teardown(() => { sandbox.restore(); });
+ test('renders', () => {
+ // Rendered with the expected preferences selected.
+ assert.equal(valueOf('Context', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.context);
+ assert.equal(valueOf('Fit to screen', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.line_wrapping);
+ assert.equal(valueOf('Diff width', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.line_length);
+ assert.equal(valueOf('Tab width', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.tab_size);
+ assert.equal(valueOf('Font size', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.font_size);
+ assert.equal(valueOf('Show tabs', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.show_tabs);
+ assert.equal(valueOf('Show trailing whitespace', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.show_whitespace_errors);
+ assert.equal(valueOf('Syntax highlighting', 'diffPreferences')
+ .firstElementChild.checked, diffPreferences.syntax_highlighting);
+ assert.equal(
+ valueOf('Automatically mark viewed files reviewed', 'diffPreferences')
+ .firstElementChild.checked, !diffPreferences.manual_review);
+ assert.equal(valueOf('Ignore Whitespace', 'diffPreferences')
+ .firstElementChild.bindValue, diffPreferences.ignore_whitespace);
+
+ assert.isFalse(element.hasUnsavedChanges);
+ });
+
+ test('save changes', () => {
+ sandbox.stub(element.$.restAPI, 'saveDiffPreferences')
+ .returns(Promise.resolve());
+ const showTrailingWhitespaceCheckbox =
+ valueOf('Show trailing whitespace', 'diffPreferences')
+ .firstElementChild;
+ showTrailingWhitespaceCheckbox.checked = false;
+ element._handleShowTrailingWhitespaceTap();
+
+ assert.isTrue(element.hasUnsavedChanges);
+
+ // Save the change.
+ return element.save().then(() => {
assert.isFalse(element.hasUnsavedChanges);
});
-
- test('save changes', () => {
- sandbox.stub(element.$.restAPI, 'saveDiffPreferences')
- .returns(Promise.resolve());
- const showTrailingWhitespaceCheckbox =
- valueOf('Show trailing whitespace', 'diffPreferences')
- .firstElementChild;
- showTrailingWhitespaceCheckbox.checked = false;
- element._handleShowTrailingWhitespaceTap();
-
- assert.isTrue(element.hasUnsavedChanges);
-
- // Save the change.
- return element.save().then(() => {
- assert.isFalse(element.hasUnsavedChanges);
- });
- });
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index 04df531..e9befaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -14,82 +14,93 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
- */
- class GrDownloadCommands extends Polymer.mixinBehaviors( [
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-download-commands'; }
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import '@polymer/paper-tabs/paper-tabs.js';
+import '../gr-shell-command/gr-shell-command.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.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-download-commands_html.js';
- static get properties() {
- return {
- commands: Array,
- _loggedIn: {
- type: Boolean,
- value: false,
- observer: '_loggedInChanged',
- },
- schemes: Array,
- selectedScheme: {
- type: String,
- notify: true,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrDownloadCommands extends mixinBehaviors( [
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- });
- }
+ static get is() { return 'gr-download-commands'; }
- focusOnCopy() {
- this.shadowRoot.querySelector('gr-shell-command').focusOnCopy();
- }
+ static get properties() {
+ return {
+ commands: Array,
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ observer: '_loggedInChanged',
+ },
+ schemes: Array,
+ selectedScheme: {
+ type: String,
+ notify: true,
+ },
+ };
+ }
- _getLoggedIn() {
- return this.$.restAPI.getLoggedIn();
- }
+ /** @override */
+ attached() {
+ super.attached();
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ });
+ }
- _loggedInChanged(loggedIn) {
- if (!loggedIn) { return; }
- return this.$.restAPI.getPreferences().then(prefs => {
- if (prefs.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this.selectedScheme = prefs.download_scheme.toLowerCase();
- }
- });
- }
+ focusOnCopy() {
+ this.shadowRoot.querySelector('gr-shell-command').focusOnCopy();
+ }
- _handleTabChange(e) {
- const scheme = this.schemes[e.detail.value];
- if (scheme && scheme !== this.selectedScheme) {
- this.set('selectedScheme', scheme);
- if (this._loggedIn) {
- this.$.restAPI.savePreferences(
- {download_scheme: this.selectedScheme});
- }
+ _getLoggedIn() {
+ return this.$.restAPI.getLoggedIn();
+ }
+
+ _loggedInChanged(loggedIn) {
+ if (!loggedIn) { return; }
+ return this.$.restAPI.getPreferences().then(prefs => {
+ if (prefs.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this.selectedScheme = prefs.download_scheme.toLowerCase();
}
- }
+ });
+ }
- _computeSelected(schemes, selectedScheme) {
- return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) +
- '';
- }
-
- _computeShowTabs(schemes) {
- return schemes.length > 1 ? '' : 'hidden';
+ _handleTabChange(e) {
+ const scheme = this.schemes[e.detail.value];
+ if (scheme && scheme !== this.selectedScheme) {
+ this.set('selectedScheme', scheme);
+ if (this._loggedIn) {
+ this.$.restAPI.savePreferences(
+ {download_scheme: this.selectedScheme});
+ }
}
}
- customElements.define(GrDownloadCommands.is, GrDownloadCommands);
-})();
+ _computeSelected(schemes, selectedScheme) {
+ return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) +
+ '';
+ }
+
+ _computeShowTabs(schemes) {
+ return schemes.length > 1 ? '' : 'hidden';
+ }
+}
+
+customElements.define(GrDownloadCommands.is, GrDownloadCommands);
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
index 14a65b2..12a8d01 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
@@ -1,31 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html">
-<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.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-download-commands">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
paper-tabs {
height: 3rem;
@@ -61,26 +52,16 @@
}
</style>
<div class="schemes">
- <paper-tabs
- id="downloadTabs"
- class$="[[_computeShowTabs(schemes)]]"
- selected="[[_computeSelected(schemes, selectedScheme)]]"
- on-selected-changed="_handleTabChange">
+ <paper-tabs id="downloadTabs" class\$="[[_computeShowTabs(schemes)]]" selected="[[_computeSelected(schemes, selectedScheme)]]" on-selected-changed="_handleTabChange">
<template is="dom-repeat" items="[[schemes]]" as="scheme">
- <paper-tab data-scheme$="[[scheme]]">[[scheme]]</paper-tab>
+ <paper-tab data-scheme\$="[[scheme]]">[[scheme]]</paper-tab>
</template>
</paper-tabs>
</div>
- <div class="commands" hidden$="[[!schemes.length]]" hidden>
- <template is="dom-repeat"
- items="[[commands]]"
- as="command">
- <gr-shell-command
- label=[[command.title]]
- command=[[command.command]]></gr-shell-command>
+ <div class="commands" hidden\$="[[!schemes.length]]" hidden="">
+ <template is="dom-repeat" items="[[commands]]" as="command">
+ <gr-shell-command label="[[command.title]]" command="[[command.command]]"></gr-shell-command>
</template>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-download-commands.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
index 4e37f9e..a39a433 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_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-download-commands</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-download-commands.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-download-commands.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-download-commands.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,123 +40,125 @@
</template>
</test-fixture>
-<script>
- suite('gr-download-commands', async () => {
- await readyToTest();
- let element;
- let sandbox;
- const SCHEMES = ['http', 'repo', 'ssh'];
- const COMMANDS = [{
- title: 'Checkout',
- command: `git fetch http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1 && git checkout FETCH_HEAD`,
- }, {
- title: 'Cherry Pick',
- command: `git fetch http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1 && git cherry-pick FETCH_HEAD`,
- }, {
- title: 'Format Patch',
- command: `git fetch http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD`,
- }, {
- title: 'Pull',
- command: `git pull http://andybons@localhost:8080/a/test-project
- refs/changes/05/5/1`,
- }];
- const SELECTED_SCHEME = 'http';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-download-commands.js';
+suite('gr-download-commands', () => {
+ let element;
+ let sandbox;
+ const SCHEMES = ['http', 'repo', 'ssh'];
+ const COMMANDS = [{
+ title: 'Checkout',
+ command: `git fetch http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git checkout FETCH_HEAD`,
+ }, {
+ title: 'Cherry Pick',
+ command: `git fetch http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git cherry-pick FETCH_HEAD`,
+ }, {
+ title: 'Format Patch',
+ command: `git fetch http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD`,
+ }, {
+ title: 'Pull',
+ command: `git pull http://andybons@localhost:8080/a/test-project
+ refs/changes/05/5/1`,
+ }];
+ const SELECTED_SCHEME = 'http';
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('unauthenticated', () => {
+ setup(done => {
+ element = fixture('basic');
+ element.schemes = SCHEMES;
+ element.commands = COMMANDS;
+ element.selectedScheme = SELECTED_SCHEME;
+ flushAsynchronousOperations();
+ flush(done);
});
- teardown(() => {
- sandbox.restore();
+ test('focusOnCopy', () => {
+ const focusStub = sandbox.stub(element.shadowRoot
+ .querySelector('gr-shell-command'),
+ 'focusOnCopy');
+ element.focusOnCopy();
+ assert.isTrue(focusStub.called);
});
- suite('unauthenticated', () => {
- setup(done => {
- element = fixture('basic');
- element.schemes = SCHEMES;
- element.commands = COMMANDS;
- element.selectedScheme = SELECTED_SCHEME;
- flushAsynchronousOperations();
- flush(done);
+ test('element visibility', () => {
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('paper-tabs')));
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.commands')));
+
+ element.schemes = [];
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('paper-tabs')));
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.commands')));
+ });
+
+ test('tab selection', done => {
+ assert.equal(element.$.downloadTabs.selected, '0');
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-scheme="ssh"]'));
+ flushAsynchronousOperations();
+ assert.equal(element.selectedScheme, 'ssh');
+ assert.equal(element.$.downloadTabs.selected, '2');
+ done();
+ });
+
+ test('loads scheme from preferences', done => {
+ stub('gr-rest-api-interface', {
+ getPreferences() {
+ return Promise.resolve({download_scheme: 'repo'});
+ },
});
-
- test('focusOnCopy', () => {
- const focusStub = sandbox.stub(element.shadowRoot
- .querySelector('gr-shell-command'),
- 'focusOnCopy');
- element.focusOnCopy();
- assert.isTrue(focusStub.called);
- });
-
- test('element visibility', () => {
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('paper-tabs')));
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.commands')));
-
- element.schemes = [];
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('paper-tabs')));
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('.commands')));
- });
-
- test('tab selection', done => {
- assert.equal(element.$.downloadTabs.selected, '0');
- MockInteractions.tap(element.shadowRoot
- .querySelector('[data-scheme="ssh"]'));
- flushAsynchronousOperations();
- assert.equal(element.selectedScheme, 'ssh');
- assert.equal(element.$.downloadTabs.selected, '2');
+ element._loggedIn = true;
+ assert.isTrue(element.$.restAPI.getPreferences.called);
+ element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
+ assert.equal(element.selectedScheme, 'repo');
done();
});
+ });
- test('loads scheme from preferences', done => {
- stub('gr-rest-api-interface', {
- getPreferences() {
- return Promise.resolve({download_scheme: 'repo'});
- },
- });
- element._loggedIn = true;
- assert.isTrue(element.$.restAPI.getPreferences.called);
- element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
- assert.equal(element.selectedScheme, 'repo');
- done();
- });
+ test('normalize scheme from preferences', done => {
+ stub('gr-rest-api-interface', {
+ getPreferences() {
+ return Promise.resolve({download_scheme: 'REPO'});
+ },
});
-
- test('normalize scheme from preferences', done => {
- stub('gr-rest-api-interface', {
- getPreferences() {
- return Promise.resolve({download_scheme: 'REPO'});
- },
- });
- element._loggedIn = true;
- element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
- assert.equal(element.selectedScheme, 'repo');
- done();
- });
- });
-
- test('saves scheme to preferences', () => {
- element._loggedIn = true;
- const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences',
- () => Promise.resolve());
-
- flushAsynchronousOperations();
-
- const repoTab = element.shadowRoot
- .querySelector('paper-tab[data-scheme="repo"]');
-
- MockInteractions.tap(repoTab);
-
- assert.isTrue(savePrefsStub.called);
- assert.equal(savePrefsStub.lastCall.args[0].download_scheme,
- repoTab.getAttribute('data-scheme'));
+ element._loggedIn = true;
+ element.$.restAPI.getPreferences.lastCall.returnValue.then(() => {
+ assert.equal(element.selectedScheme, 'repo');
+ done();
});
});
+
+ test('saves scheme to preferences', () => {
+ element._loggedIn = true;
+ const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences',
+ () => Promise.resolve());
+
+ flushAsynchronousOperations();
+
+ const repoTab = element.shadowRoot
+ .querySelector('paper-tab[data-scheme="repo"]');
+
+ MockInteractions.tap(repoTab);
+
+ assert.isTrue(savePrefsStub.called);
+ assert.equal(savePrefsStub.lastCall.args[0].download_scheme,
+ repoTab.getAttribute('data-scheme'));
+ });
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 06b4a72..6b250de 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -14,123 +14,135 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '@polymer/paper-item/paper-item.js';
+import '@polymer/paper-listbox/paper-listbox.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.js';
+import '../gr-date-formatter/gr-date-formatter.js';
+import '../gr-select/gr-select.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-dropdown-list_html.js';
+
+/**
+ * fired when the selected value of the dropdown changes
+ *
+ * @event {change}
+ */
+
+const Defs = {};
+
+/**
+ * Requred values are text and value. mobileText and triggerText will
+ * fall back to text if not provided.
+ *
+ * If bottomText is not provided, nothing will display on the second
+ * line.
+ *
+ * If date is not provided, nothing will be displayed in its place.
+ *
+ * @typedef {{
+ * text: string,
+ * value: (string|number),
+ * bottomText: (string|undefined),
+ * triggerText: (string|undefined),
+ * mobileText: (string|undefined),
+ * date: (!Date|undefined),
+ * }}
+ */
+Defs.item;
+
+/** @extends Polymer.Element */
+class GrDropdownList extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dropdown-list'; }
/**
- * fired when the selected value of the dropdown changes
+ * Fired when the selected value changes
*
- * @event {change}
+ * @event value-change
+ *
+ * @property {string|number} value
*/
- const Defs = {};
-
- /**
- * Requred values are text and value. mobileText and triggerText will
- * fall back to text if not provided.
- *
- * If bottomText is not provided, nothing will display on the second
- * line.
- *
- * If date is not provided, nothing will be displayed in its place.
- *
- * @typedef {{
- * text: string,
- * value: (string|number),
- * bottomText: (string|undefined),
- * triggerText: (string|undefined),
- * mobileText: (string|undefined),
- * date: (!Date|undefined),
- * }}
- */
- Defs.item;
-
- /** @extends Polymer.Element */
- class GrDropdownList extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-dropdown-list'; }
- /**
- * Fired when the selected value changes
- *
- * @event value-change
- *
- * @property {string|number} value
- */
-
- static get properties() {
- return {
- initialCount: Number,
- /** @type {!Array<!Defs.item>} */
- items: Object,
- text: String,
- disabled: {
- type: Boolean,
- value: false,
- },
- value: {
- type: String,
- notify: true,
- },
- };
- }
-
- static get observers() {
- return [
- '_handleValueChange(value, items)',
- ];
- }
-
- /**
- * Handle a click on the iron-dropdown element.
- *
- * @param {!Event} e
- */
- _handleDropdownClick(e) {
- // async is needed so that that the click event is fired before the
- // dropdown closes (This was a bug for touch devices).
- this.async(() => {
- this.$.dropdown.close();
- }, 1);
- }
-
- /**
- * Handle a click on the button to open the dropdown.
- *
- * @param {!Event} e
- */
- _showDropdownTapHandler(e) {
- this._open();
- }
-
- /**
- * Open the dropdown.
- */
- _open() {
- this.$.dropdown.open();
- }
-
- _computeMobileText(item) {
- return item.mobileText ? item.mobileText : item.text;
- }
-
- _handleValueChange(value, items) {
- // Polymer 2: check for undefined
- if ([value, items].some(arg => arg === undefined)) {
- return;
- }
-
- if (!value) { return; }
- const selectedObj = items.find(item => item.value + '' === value + '');
- if (!selectedObj) { return; }
- this.text = selectedObj.triggerText? selectedObj.triggerText :
- selectedObj.text;
- this.dispatchEvent(new CustomEvent('value-change', {
- detail: {value},
- bubbles: false,
- }));
- }
+ static get properties() {
+ return {
+ initialCount: Number,
+ /** @type {!Array<!Defs.item>} */
+ items: Object,
+ text: String,
+ disabled: {
+ type: Boolean,
+ value: false,
+ },
+ value: {
+ type: String,
+ notify: true,
+ },
+ };
}
- customElements.define(GrDropdownList.is, GrDropdownList);
-})();
+ static get observers() {
+ return [
+ '_handleValueChange(value, items)',
+ ];
+ }
+
+ /**
+ * Handle a click on the iron-dropdown element.
+ *
+ * @param {!Event} e
+ */
+ _handleDropdownClick(e) {
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(() => {
+ this.$.dropdown.close();
+ }, 1);
+ }
+
+ /**
+ * Handle a click on the button to open the dropdown.
+ *
+ * @param {!Event} e
+ */
+ _showDropdownTapHandler(e) {
+ this._open();
+ }
+
+ /**
+ * Open the dropdown.
+ */
+ _open() {
+ this.$.dropdown.open();
+ }
+
+ _computeMobileText(item) {
+ return item.mobileText ? item.mobileText : item.text;
+ }
+
+ _handleValueChange(value, items) {
+ // Polymer 2: check for undefined
+ if ([value, items].some(arg => arg === undefined)) {
+ return;
+ }
+
+ if (!value) { return; }
+ const selectedObj = items.find(item => item.value + '' === value + '');
+ if (!selectedObj) { return; }
+ this.text = selectedObj.triggerText? selectedObj.triggerText :
+ selectedObj.text;
+ this.dispatchEvent(new CustomEvent('value-change', {
+ detail: {value},
+ bubbles: false,
+ }));
+ }
+}
+
+customElements.define(GrDropdownList.is, GrDropdownList);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
index 7586876..3b454c2 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
@@ -1,33 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/paper-item/paper-item.html">
-<link rel="import" href="/bower_components/paper-listbox/paper-listbox.html">
-
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-select/gr-select.html">
-
-
-<dom-module id="gr-dropdown-list">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: inline-block;
@@ -128,38 +117,17 @@
}
}
</style>
- <gr-button
- disabled="[[disabled]]"
- down-arrow
- link
- id="trigger"
- class="dropdown-trigger"
- on-click="_showDropdownTapHandler"
- slot="dropdown-trigger">
+ <gr-button disabled="[[disabled]]" down-arrow="" link="" id="trigger" class="dropdown-trigger" on-click="_showDropdownTapHandler" slot="dropdown-trigger">
<span id="triggerText">[[text]]</span>
</gr-button>
- <iron-dropdown
- id="dropdown"
- vertical-align="top"
- allow-outside-scroll="true"
- on-click="_handleDropdownClick">
- <paper-listbox
- class="dropdown-content"
- slot="dropdown-content"
- attr-for-selected="data-value"
- selected="{{value}}"
- on-tap="_handleDropdownTap">
- <template is="dom-repeat"
- items="[[items]]"
- initial-count="[[initialCount]]">
- <paper-item
- disabled="[[item.disabled]]"
- data-value$="[[item.value]]">
+ <iron-dropdown id="dropdown" vertical-align="top" allow-outside-scroll="true" on-click="_handleDropdownClick">
+ <paper-listbox class="dropdown-content" slot="dropdown-content" attr-for-selected="data-value" selected="{{value}}" on-tap="_handleDropdownTap">
+ <template is="dom-repeat" items="[[items]]" initial-count="[[initialCount]]">
+ <paper-item disabled="[[item.disabled]]" data-value\$="[[item.value]]">
<div class="topContent">
<div>[[item.text]]</div>
<template is="dom-if" if="[[item.date]]">
- <gr-date-formatter
- date-str="[[item.date]]"></gr-date-formatter>
+ <gr-date-formatter date-str="[[item.date]]"></gr-date-formatter>
</template>
</div>
<template is="dom-if" if="[[item.bottomText]]">
@@ -174,14 +142,10 @@
<gr-select bind-value="{{value}}">
<select>
<template is="dom-repeat" items="[[items]]">
- <option
- disabled$="[[item.disabled]]"
- value="[[item.value]]">
+ <option disabled\$="[[item.disabled]]" value="[[item.value]]">
[[_computeMobileText(item)]]
</option>
</template>
</select>
</gr-select>
- </template>
- <script src="gr-dropdown-list.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
index 40e43fd..b3fda65 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_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-dropdown-list</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-dropdown-list.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-dropdown-list.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-dropdown-list.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,140 +40,143 @@
</template>
</test-fixture>
-<script>
- suite('gr-dropdown-list 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-dropdown-list.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-dropdown-list tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('tap on trigger opens menu', () => {
- sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.tap(element.$.trigger);
- assert.isTrue(element.$.dropdown.opened);
- });
+ test('tap on trigger opens menu', () => {
+ sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.$.dropdown.opened);
+ });
- test('_computeMobileText', () => {
- const item = {
+ test('_computeMobileText', () => {
+ const item = {
+ value: 1,
+ text: 'text',
+ };
+ assert.equal(element._computeMobileText(item), item.text);
+ item.mobileText = 'mobile text';
+ assert.equal(element._computeMobileText(item), item.mobileText);
+ });
+
+ test('options are selected and laid out correctly', done => {
+ element.value = 2;
+ element.items = [
+ {
value: 1,
- text: 'text',
- };
- assert.equal(element._computeMobileText(item), item.text);
- item.mobileText = 'mobile text';
- assert.equal(element._computeMobileText(item), item.mobileText);
- });
+ text: 'Top Text 1',
+ },
+ {
+ value: 2,
+ bottomText: 'Bottom Text 2',
+ triggerText: 'Button Text 2',
+ text: 'Top Text 2',
+ mobileText: 'Mobile Text 2',
+ },
+ {
+ value: 3,
+ disabled: true,
+ bottomText: 'Bottom Text 3',
+ triggerText: 'Button Text 3',
+ date: '2017-08-18 23:11:42.569000000',
+ text: 'Top Text 3',
+ mobileText: 'Mobile Text 3',
+ },
+ ];
+ assert.equal(element.shadowRoot
+ .querySelector('paper-listbox').selected, element.value);
+ assert.equal(element.text, 'Button Text 2');
+ flush(() => {
+ const items = dom(element.root).querySelectorAll('paper-item');
+ const mobileItems = dom(element.root).querySelectorAll('option');
+ assert.equal(items.length, 3);
+ assert.equal(mobileItems.length, 3);
- test('options are selected and laid out correctly', done => {
- element.value = 2;
- element.items = [
- {
- value: 1,
- text: 'Top Text 1',
- },
- {
- value: 2,
- bottomText: 'Bottom Text 2',
- triggerText: 'Button Text 2',
- text: 'Top Text 2',
- mobileText: 'Mobile Text 2',
- },
- {
- value: 3,
- disabled: true,
- bottomText: 'Bottom Text 3',
- triggerText: 'Button Text 3',
- date: '2017-08-18 23:11:42.569000000',
- text: 'Top Text 3',
- mobileText: 'Mobile Text 3',
- },
- ];
- assert.equal(element.shadowRoot
- .querySelector('paper-listbox').selected, element.value);
- assert.equal(element.text, 'Button Text 2');
- flush(() => {
- const items = Polymer.dom(element.root).querySelectorAll('paper-item');
- const mobileItems = Polymer.dom(element.root).querySelectorAll('option');
- assert.equal(items.length, 3);
- assert.equal(mobileItems.length, 3);
+ // First Item
+ // The first item should be disabled, has no bottom text, and no date.
+ assert.isFalse(!!items[0].disabled);
+ assert.isFalse(mobileItems[0].disabled);
+ assert.isFalse(items[0].classList.contains('iron-selected'));
+ assert.isFalse(mobileItems[0].selected);
- // First Item
- // The first item should be disabled, has no bottom text, and no date.
- assert.isFalse(!!items[0].disabled);
- assert.isFalse(mobileItems[0].disabled);
- assert.isFalse(items[0].classList.contains('iron-selected'));
- assert.isFalse(mobileItems[0].selected);
+ assert.isNotOk(dom(items[0]).querySelector('gr-date-formatter'));
+ assert.isNotOk(dom(items[0]).querySelector('.bottomContent'));
+ assert.equal(items[0].dataset.value, element.items[0].value);
+ assert.equal(mobileItems[0].value, element.items[0].value);
+ assert.equal(dom(items[0]).querySelector('.topContent div')
+ .innerText, element.items[0].text);
- assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter'));
- assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent'));
- assert.equal(items[0].dataset.value, element.items[0].value);
- assert.equal(mobileItems[0].value, element.items[0].value);
- assert.equal(Polymer.dom(items[0]).querySelector('.topContent div')
- .innerText, element.items[0].text);
+ // Since no mobile specific text, it should fall back to text.
+ assert.equal(mobileItems[0].text, element.items[0].text);
- // Since no mobile specific text, it should fall back to text.
- assert.equal(mobileItems[0].text, element.items[0].text);
+ // Second Item
+ // The second item should have top text, bottom text, and no date.
+ assert.isFalse(!!items[1].disabled);
+ assert.isFalse(mobileItems[1].disabled);
+ assert.isTrue(items[1].classList.contains('iron-selected'));
+ assert.isTrue(mobileItems[1].selected);
- // Second Item
- // The second item should have top text, bottom text, and no date.
- assert.isFalse(!!items[1].disabled);
- assert.isFalse(mobileItems[1].disabled);
- assert.isTrue(items[1].classList.contains('iron-selected'));
- assert.isTrue(mobileItems[1].selected);
+ assert.isNotOk(dom(items[1]).querySelector('gr-date-formatter'));
+ assert.isOk(dom(items[1]).querySelector('.bottomContent'));
+ assert.equal(items[1].dataset.value, element.items[1].value);
+ assert.equal(mobileItems[1].value, element.items[1].value);
+ assert.equal(dom(items[1]).querySelector('.topContent div')
+ .innerText, element.items[1].text);
- assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter'));
- assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent'));
- assert.equal(items[1].dataset.value, element.items[1].value);
- assert.equal(mobileItems[1].value, element.items[1].value);
- assert.equal(Polymer.dom(items[1]).querySelector('.topContent div')
- .innerText, element.items[1].text);
+ // Since there is mobile specific text, it should that.
+ assert.equal(mobileItems[1].text, element.items[1].mobileText);
- // Since there is mobile specific text, it should that.
- assert.equal(mobileItems[1].text, element.items[1].mobileText);
+ // Since this item is selected, and it has triggerText defined, that
+ // should be used.
+ assert.equal(element.text, element.items[1].triggerText);
- // Since this item is selected, and it has triggerText defined, that
- // should be used.
- assert.equal(element.text, element.items[1].triggerText);
+ // Third item
+ // The third item should be disabled, and have a date, and bottom content.
+ assert.isTrue(!!items[2].disabled);
+ assert.isTrue(mobileItems[2].disabled);
+ assert.isFalse(items[2].classList.contains('iron-selected'));
+ assert.isFalse(mobileItems[2].selected);
- // Third item
- // The third item should be disabled, and have a date, and bottom content.
- assert.isTrue(!!items[2].disabled);
- assert.isTrue(mobileItems[2].disabled);
- assert.isFalse(items[2].classList.contains('iron-selected'));
- assert.isFalse(mobileItems[2].selected);
+ assert.isOk(dom(items[2]).querySelector('gr-date-formatter'));
+ assert.isOk(dom(items[2]).querySelector('.bottomContent'));
+ assert.equal(items[2].dataset.value, element.items[2].value);
+ assert.equal(mobileItems[2].value, element.items[2].value);
+ assert.equal(dom(items[2]).querySelector('.topContent div')
+ .innerText, element.items[2].text);
- assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter'));
- assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent'));
- assert.equal(items[2].dataset.value, element.items[2].value);
- assert.equal(mobileItems[2].value, element.items[2].value);
- assert.equal(Polymer.dom(items[2]).querySelector('.topContent div')
- .innerText, element.items[2].text);
+ // Since there is mobile specific text, it should that.
+ assert.equal(mobileItems[2].text, element.items[2].mobileText);
- // Since there is mobile specific text, it should that.
- assert.equal(mobileItems[2].text, element.items[2].mobileText);
+ // Select a new item.
+ MockInteractions.tap(items[0]);
+ flushAsynchronousOperations();
+ assert.equal(element.value, 1);
+ assert.isTrue(items[0].classList.contains('iron-selected'));
+ assert.isTrue(mobileItems[0].selected);
- // Select a new item.
- MockInteractions.tap(items[0]);
- flushAsynchronousOperations();
- assert.equal(element.value, 1);
- assert.isTrue(items[0].classList.contains('iron-selected'));
- assert.isTrue(mobileItems[0].selected);
-
- // Since no triggerText, the fallback is used.
- assert.equal(element.text, element.items[0].text);
- done();
- });
+ // Since no triggerText, the fallback is used.
+ assert.equal(element.text, element.items[0].text);
+ done();
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 531f2e3..b4190dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,309 +14,324 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
- const REL_NOOPENER = 'noopener';
- const REL_EXTERNAL = 'external';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../../../scripts/bundled-polymer.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '../gr-button/gr-button.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-tooltip-content/gr-tooltip-content.js';
+import '../../../styles/shared-styles.js';
+import {flush, 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-dropdown_html.js';
+
+const REL_NOOPENER = 'noopener';
+const REL_EXTERNAL = 'external';
+
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrDropdown extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-dropdown'; }
+ /**
+ * Fired when a non-link dropdown item with the given ID is tapped.
+ *
+ * @event tap-item-<id>
+ */
/**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * Fired when a non-link dropdown item is tapped.
+ *
+ * @event tap-item
*/
- class GrDropdown extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-dropdown'; }
- /**
- * Fired when a non-link dropdown item with the given ID is tapped.
- *
- * @event tap-item-<id>
- */
- /**
- * Fired when a non-link dropdown item is tapped.
- *
- * @event tap-item
- */
+ static get properties() {
+ return {
+ items: {
+ type: Array,
+ observer: '_resetCursorStops',
+ },
+ downArrow: Boolean,
+ topContent: Object,
+ horizontalAlign: {
+ type: String,
+ value: 'left',
+ },
- static get properties() {
- return {
- items: {
- type: Array,
- observer: '_resetCursorStops',
- },
- downArrow: Boolean,
- topContent: Object,
- horizontalAlign: {
- type: String,
- value: 'left',
- },
+ /**
+ * Style the dropdown trigger as a link (rather than a button).
+ */
+ link: {
+ type: Boolean,
+ value: false,
+ },
- /**
- * Style the dropdown trigger as a link (rather than a button).
- */
- link: {
- type: Boolean,
- value: false,
- },
+ verticalOffset: {
+ type: Number,
+ value: 40,
+ },
- verticalOffset: {
- type: Number,
- value: 40,
- },
+ /**
+ * List the IDs of dropdown buttons to be disabled. (Note this only
+ * diisables bittons and not link entries.)
+ */
+ disabledIds: {
+ type: Array,
+ value() { return []; },
+ },
- /**
- * List the IDs of dropdown buttons to be disabled. (Note this only
- * diisables bittons and not link entries.)
- */
- disabledIds: {
- type: Array,
- value() { return []; },
- },
+ /**
+ * The elements of the list.
+ */
+ _listElements: {
+ type: Array,
+ value() { return []; },
+ },
+ };
+ }
- /**
- * The elements of the list.
- */
- _listElements: {
- type: Array,
- value() { return []; },
- },
- };
- }
+ get keyBindings() {
+ return {
+ 'down': '_handleDown',
+ 'enter space': '_handleEnter',
+ 'tab': '_handleTab',
+ 'up': '_handleUp',
+ };
+ }
- get keyBindings() {
- return {
- 'down': '_handleDown',
- 'enter space': '_handleEnter',
- 'tab': '_handleTab',
- 'up': '_handleUp',
- };
- }
-
- /**
- * Handle the up key.
- *
- * @param {!Event} e
- */
- _handleUp(e) {
- if (this.$.dropdown.opened) {
- e.preventDefault();
- e.stopPropagation();
- this.$.cursor.previous();
- } else {
- this._open();
- }
- }
-
- /**
- * Handle the down key.
- *
- * @param {!Event} e
- */
- _handleDown(e) {
- if (this.$.dropdown.opened) {
- e.preventDefault();
- e.stopPropagation();
- this.$.cursor.next();
- } else {
- this._open();
- }
- }
-
- /**
- * Handle the tab key.
- *
- * @param {!Event} e
- */
- _handleTab(e) {
- if (this.$.dropdown.opened) {
- // Tab in a native select is a no-op. Emulate this.
- e.preventDefault();
- e.stopPropagation();
- }
- }
-
- /**
- * Handle the enter key.
- *
- * @param {!Event} e
- */
- _handleEnter(e) {
+ /**
+ * Handle the up key.
+ *
+ * @param {!Event} e
+ */
+ _handleUp(e) {
+ if (this.$.dropdown.opened) {
e.preventDefault();
e.stopPropagation();
- if (this.$.dropdown.opened) {
- // TODO(milutin): This solution is not particularly robust in general.
- // Since gr-tooltip-content click on shadow dom is not propagated down,
- // we have to target `a` inside it.
- const el = this.$.cursor.target.querySelector(':not([hidden]) a');
- if (el) { el.click(); }
- } else {
- this._open();
- }
- }
-
- /**
- * Handle a click on the iron-dropdown element.
- *
- * @param {!Event} e
- */
- _handleDropdownClick(e) {
- this._close();
- }
-
- /**
- * Hanlde a click on the button to open the dropdown.
- *
- * @param {!Event} e
- */
- _dropdownTriggerTapHandler(e) {
- e.preventDefault();
- e.stopPropagation();
- if (this.$.dropdown.opened) {
- this._close();
- } else {
- this._open();
- }
- }
-
- /**
- * Open the dropdown and initialize the cursor.
- */
- _open() {
- this.$.dropdown.open();
- this._resetCursorStops();
- this.$.cursor.setCursorAtIndex(0);
- this.$.cursor.target.focus();
- }
-
- _close() {
- // async is needed so that that the click event is fired before the
- // dropdown closes (This was a bug for touch devices).
- this.async(() => {
- this.$.dropdown.close();
- }, 1);
- }
-
- /**
- * Get the class for a top-content item based on the given boolean.
- *
- * @param {boolean} bold Whether the item is bold.
- * @return {string} The class for the top-content item.
- */
- _getClassIfBold(bold) {
- return bold ? 'bold-text' : '';
- }
-
- /**
- * Build a URL for the given host and path. The base URL will be only added,
- * if it is not already included in the path.
- *
- * @param {!string} host
- * @param {!string} path
- * @return {!string} The scheme-relative URL.
- */
- _computeURLHelper(host, path) {
- const base = path.startsWith(this.getBaseUrl()) ?
- '' : this.getBaseUrl();
- return '//' + host + base + path;
- }
-
- /**
- * Build a scheme-relative URL for the current host. Will include the base
- * URL if one is present. Note: the URL will be scheme-relative but absolute
- * with regard to the host.
- *
- * @param {!string} path The path for the URL.
- * @return {!string} The scheme-relative URL.
- */
- _computeRelativeURL(path) {
- const host = window.location.host;
- return this._computeURLHelper(host, path);
- }
-
- /**
- * Compute the URL for a link object.
- *
- * @param {!Object} link The object describing the link.
- * @return {!string} The URL.
- */
- _computeLinkURL(link) {
- if (typeof link.url === 'undefined') {
- return '';
- }
- if (link.target || !link.url.startsWith('/')) {
- return link.url;
- }
- return this._computeRelativeURL(link.url);
- }
-
- /**
- * Compute the value for the rel attribute of an anchor for the given link
- * object. If the link has a target value, then the rel must be "noopener"
- * for security reasons.
- *
- * @param {!Object} link The object describing the link.
- * @return {?string} The rel value for the link.
- */
- _computeLinkRel(link) {
- // Note: noopener takes precedence over external.
- if (link.target) { return REL_NOOPENER; }
- if (link.external) { return REL_EXTERNAL; }
- return null;
- }
-
- /**
- * Handle a click on an item of the dropdown.
- *
- * @param {!Event} e
- */
- _handleItemTap(e) {
- const id = e.target.getAttribute('data-id');
- const item = this.items.find(item => item.id === id);
- if (id && !this.disabledIds.includes(id)) {
- if (item) {
- this.dispatchEvent(new CustomEvent('tap-item', {detail: item}));
- }
- this.dispatchEvent(new CustomEvent('tap-item-' + id));
- }
- }
-
- /**
- * If a dropdown item is shown as a button, get the class for the button.
- *
- * @param {string} id
- * @param {!Object} disabledIdsRecord The change record for the disabled IDs
- * list.
- * @return {!string} The class for the item button.
- */
- _computeDisabledClass(id, disabledIdsRecord) {
- return disabledIdsRecord.base.includes(id) ? 'disabled' : '';
- }
-
- /**
- * Recompute the stops for the dropdown item cursor.
- */
- _resetCursorStops() {
- if (this.items && this.items.length > 0 && this.$.dropdown.opened) {
- Polymer.dom.flush();
- this._listElements = Array.from(
- Polymer.dom(this.root).querySelectorAll('li'));
- }
- }
-
- _computeHasTooltip(tooltip) {
- return !!tooltip;
- }
-
- _computeIsDownload(link) {
- return !!link.download;
+ this.$.cursor.previous();
+ } else {
+ this._open();
}
}
- customElements.define(GrDropdown.is, GrDropdown);
-})();
+ /**
+ * Handle the down key.
+ *
+ * @param {!Event} e
+ */
+ _handleDown(e) {
+ if (this.$.dropdown.opened) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.$.cursor.next();
+ } else {
+ this._open();
+ }
+ }
+
+ /**
+ * Handle the tab key.
+ *
+ * @param {!Event} e
+ */
+ _handleTab(e) {
+ if (this.$.dropdown.opened) {
+ // Tab in a native select is a no-op. Emulate this.
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ /**
+ * Handle the enter key.
+ *
+ * @param {!Event} e
+ */
+ _handleEnter(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.$.dropdown.opened) {
+ // TODO(milutin): This solution is not particularly robust in general.
+ // Since gr-tooltip-content click on shadow dom is not propagated down,
+ // we have to target `a` inside it.
+ const el = this.$.cursor.target.querySelector(':not([hidden]) a');
+ if (el) { el.click(); }
+ } else {
+ this._open();
+ }
+ }
+
+ /**
+ * Handle a click on the iron-dropdown element.
+ *
+ * @param {!Event} e
+ */
+ _handleDropdownClick(e) {
+ this._close();
+ }
+
+ /**
+ * Hanlde a click on the button to open the dropdown.
+ *
+ * @param {!Event} e
+ */
+ _dropdownTriggerTapHandler(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this.$.dropdown.opened) {
+ this._close();
+ } else {
+ this._open();
+ }
+ }
+
+ /**
+ * Open the dropdown and initialize the cursor.
+ */
+ _open() {
+ this.$.dropdown.open();
+ this._resetCursorStops();
+ this.$.cursor.setCursorAtIndex(0);
+ this.$.cursor.target.focus();
+ }
+
+ _close() {
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(() => {
+ this.$.dropdown.close();
+ }, 1);
+ }
+
+ /**
+ * Get the class for a top-content item based on the given boolean.
+ *
+ * @param {boolean} bold Whether the item is bold.
+ * @return {string} The class for the top-content item.
+ */
+ _getClassIfBold(bold) {
+ return bold ? 'bold-text' : '';
+ }
+
+ /**
+ * Build a URL for the given host and path. The base URL will be only added,
+ * if it is not already included in the path.
+ *
+ * @param {!string} host
+ * @param {!string} path
+ * @return {!string} The scheme-relative URL.
+ */
+ _computeURLHelper(host, path) {
+ const base = path.startsWith(this.getBaseUrl()) ?
+ '' : this.getBaseUrl();
+ return '//' + host + base + path;
+ }
+
+ /**
+ * Build a scheme-relative URL for the current host. Will include the base
+ * URL if one is present. Note: the URL will be scheme-relative but absolute
+ * with regard to the host.
+ *
+ * @param {!string} path The path for the URL.
+ * @return {!string} The scheme-relative URL.
+ */
+ _computeRelativeURL(path) {
+ const host = window.location.host;
+ return this._computeURLHelper(host, path);
+ }
+
+ /**
+ * Compute the URL for a link object.
+ *
+ * @param {!Object} link The object describing the link.
+ * @return {!string} The URL.
+ */
+ _computeLinkURL(link) {
+ if (typeof link.url === 'undefined') {
+ return '';
+ }
+ if (link.target || !link.url.startsWith('/')) {
+ return link.url;
+ }
+ return this._computeRelativeURL(link.url);
+ }
+
+ /**
+ * Compute the value for the rel attribute of an anchor for the given link
+ * object. If the link has a target value, then the rel must be "noopener"
+ * for security reasons.
+ *
+ * @param {!Object} link The object describing the link.
+ * @return {?string} The rel value for the link.
+ */
+ _computeLinkRel(link) {
+ // Note: noopener takes precedence over external.
+ if (link.target) { return REL_NOOPENER; }
+ if (link.external) { return REL_EXTERNAL; }
+ return null;
+ }
+
+ /**
+ * Handle a click on an item of the dropdown.
+ *
+ * @param {!Event} e
+ */
+ _handleItemTap(e) {
+ const id = e.target.getAttribute('data-id');
+ const item = this.items.find(item => item.id === id);
+ if (id && !this.disabledIds.includes(id)) {
+ if (item) {
+ this.dispatchEvent(new CustomEvent('tap-item', {detail: item}));
+ }
+ this.dispatchEvent(new CustomEvent('tap-item-' + id));
+ }
+ }
+
+ /**
+ * If a dropdown item is shown as a button, get the class for the button.
+ *
+ * @param {string} id
+ * @param {!Object} disabledIdsRecord The change record for the disabled IDs
+ * list.
+ * @return {!string} The class for the item button.
+ */
+ _computeDisabledClass(id, disabledIdsRecord) {
+ return disabledIdsRecord.base.includes(id) ? 'disabled' : '';
+ }
+
+ /**
+ * Recompute the stops for the dropdown item cursor.
+ */
+ _resetCursorStops() {
+ if (this.items && this.items.length > 0 && this.$.dropdown.opened) {
+ flush();
+ this._listElements = Array.from(
+ dom(this.root).querySelectorAll('li'));
+ }
+ }
+
+ _computeHasTooltip(tooltip) {
+ return !!tooltip;
+ }
+
+ _computeIsDownload(link) {
+ return !!link.download;
+ }
+}
+
+customElements.define(GrDropdown.is, GrDropdown);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
index 5d28390..99028af 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
@@ -1,32 +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="../../../behaviors/base-url-behavior/base-url-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="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-dropdown">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: inline-block;
@@ -97,72 +87,32 @@
font-weight: var(--font-weight-bold);
}
</style>
- <gr-button
- link="[[link]]"
- class="dropdown-trigger" id="trigger"
- down-arrow="[[downArrow]]"
- on-click="_dropdownTriggerTapHandler">
+ <gr-button link="[[link]]" class="dropdown-trigger" id="trigger" down-arrow="[[downArrow]]" on-click="_dropdownTriggerTapHandler">
<slot></slot>
</gr-button>
- <iron-dropdown id="dropdown"
- vertical-align="top"
- vertical-offset="[[verticalOffset]]"
- allow-outside-scroll="true"
- horizontal-align="[[horizontalAlign]]"
- on-click="_handleDropdownClick">
+ <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="[[verticalOffset]]" allow-outside-scroll="true" horizontal-align="[[horizontalAlign]]" on-click="_handleDropdownClick">
<div class="dropdown-content" slot="dropdown-content">
<ul>
<template is="dom-if" if="[[topContent]]">
<div class="topContent">
- <template
- is="dom-repeat"
- items="[[topContent]]"
- as="item"
- initial-count="75">
- <div
- class$="[[_getClassIfBold(item.bold)]] top-item"
- tabindex="-1">
+ <template is="dom-repeat" items="[[topContent]]" as="item" initial-count="75">
+ <div class\$="[[_getClassIfBold(item.bold)]] top-item" tabindex="-1">
[[item.text]]
</div>
</template>
</div>
</template>
- <template
- is="dom-repeat"
- items="[[items]]"
- as="link"
- initial-count="75">
+ <template is="dom-repeat" items="[[items]]" as="link" initial-count="75">
<li tabindex="-1">
- <gr-tooltip-content
- has-tooltip="[[_computeHasTooltip(link.tooltip)]]"
- title$="[[link.tooltip]]">
- <span
- class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]"
- data-id$="[[link.id]]"
- on-click="_handleItemTap"
- hidden$="[[link.url]]"
- tabindex="-1">[[link.name]]</span>
- <a
- class="itemAction"
- href$="[[_computeLinkURL(link)]]"
- download$="[[_computeIsDownload(link)]]"
- rel$="[[_computeLinkRel(link)]]"
- target$="[[link.target]]"
- hidden$="[[!link.url]]"
- tabindex="-1">[[link.name]]</a>
+ <gr-tooltip-content has-tooltip="[[_computeHasTooltip(link.tooltip)]]" title\$="[[link.tooltip]]">
+ <span class\$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]" data-id\$="[[link.id]]" on-click="_handleItemTap" hidden\$="[[link.url]]" tabindex="-1">[[link.name]]</span>
+ <a class="itemAction" href\$="[[_computeLinkURL(link)]]" download\$="[[_computeIsDownload(link)]]" rel\$="[[_computeLinkRel(link)]]" target\$="[[link.target]]" hidden\$="[[!link.url]]" tabindex="-1">[[link.name]]</a>
</gr-tooltip-content>
</li>
</template>
</ul>
</div>
</iron-dropdown>
- <gr-cursor-manager
- id="cursor"
- cursor-target-class="selected"
- scroll-behavior="never"
- focus-on-move
- stops="[[_listElements]]"></gr-cursor-manager>
+ <gr-cursor-manager id="cursor" cursor-target-class="selected" scroll-behavior="never" focus-on-move="" stops="[[_listElements]]"></gr-cursor-manager>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-dropdown.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 2d7f090..dcbab4d 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-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-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-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-dropdown.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-dropdown.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,176 +40,179 @@
</template>
</test-fixture>
-<script>
- suite('gr-dropdown 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-dropdown.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-dropdown tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ });
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeIsDownload', () => {
+ assert.isTrue(element._computeIsDownload({download: true}));
+ assert.isFalse(element._computeIsDownload({download: false}));
+ });
+
+ test('tap on trigger opens menu, then closes', () => {
+ sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+ sandbox.stub(element, '_close', () => { element.$.dropdown.close(); });
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isFalse(element.$.dropdown.opened);
+ });
+
+ test('_computeURLHelper', () => {
+ const path = '/test';
+ const host = 'http://www.testsite.com';
+ const computedPath = element._computeURLHelper(host, path);
+ assert.equal(computedPath, '//http://www.testsite.com/test');
+ });
+
+ test('link URLs', () => {
+ assert.equal(
+ element._computeLinkURL({url: 'http://example.com/test'}),
+ 'http://example.com/test');
+ assert.equal(
+ element._computeLinkURL({url: 'https://example.com/test'}),
+ 'https://example.com/test');
+ assert.equal(
+ element._computeLinkURL({url: '/test'}),
+ '//' + window.location.host + '/test');
+ assert.equal(
+ element._computeLinkURL({url: '/test', target: '_blank'}),
+ '/test');
+ });
+
+ test('link rel', () => {
+ let link = {url: '/test'};
+ assert.isNull(element._computeLinkRel(link));
+
+ link = {url: '/test', target: '_blank'};
+ assert.equal(element._computeLinkRel(link), 'noopener');
+
+ link = {url: '/test', external: true};
+ assert.equal(element._computeLinkRel(link), 'external');
+
+ link = {url: '/test', target: '_blank', external: true};
+ assert.equal(element._computeLinkRel(link), 'noopener');
+ });
+
+ test('_getClassIfBold', () => {
+ let bold = true;
+ assert.equal(element._getClassIfBold(bold), 'bold-text');
+
+ bold = false;
+ assert.equal(element._getClassIfBold(bold), '');
+ });
+
+ test('Top text exists and is bolded correctly', () => {
+ element.topContent = [{text: 'User', bold: true}, {text: 'email'}];
+ flushAsynchronousOperations();
+ const topItems = dom(element.root).querySelectorAll('.top-item');
+ assert.equal(topItems.length, 2);
+ assert.isTrue(topItems[0].classList.contains('bold-text'));
+ assert.isFalse(topItems[1].classList.contains('bold-text'));
+ });
+
+ test('non link items', () => {
+ const item0 = {name: 'item one', id: 'foo'};
+ element.items = [item0, {name: 'item two', id: 'bar'}];
+ const fooTapped = sandbox.stub();
+ const tapped = sandbox.stub();
+ element.addEventListener('tap-item-foo', fooTapped);
+ element.addEventListener('tap-item', tapped);
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.itemAction'));
+ assert.isTrue(fooTapped.called);
+ assert.isTrue(tapped.called);
+ assert.deepEqual(tapped.lastCall.args[0].detail, item0);
+ });
+
+ test('disabled non link item', () => {
+ element.items = [{name: 'item one', id: 'foo'}];
+ element.disabledIds = ['foo'];
+
+ const stub = sandbox.stub();
+ const tapped = sandbox.stub();
+ element.addEventListener('tap-item-foo', stub);
+ element.addEventListener('tap-item', tapped);
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('.itemAction'));
+ assert.isFalse(stub.called);
+ assert.isFalse(tapped.called);
+ });
+
+ test('properly sets tooltips', () => {
+ element.items = [
+ {name: 'item one', id: 'foo', tooltip: 'hello'},
+ {name: 'item two', id: 'bar'},
+ ];
+ element.disabledIds = [];
+ flushAsynchronousOperations();
+ const tooltipContents = dom(element.root)
+ .querySelectorAll('iron-dropdown li gr-tooltip-content');
+ assert.equal(tooltipContents.length, 2);
+ assert.isTrue(tooltipContents[0].hasTooltip);
+ assert.equal(tooltipContents[0].getAttribute('title'), 'hello');
+ assert.isFalse(tooltipContents[1].hasTooltip);
+ });
+
+ suite('keyboard navigation', () => {
setup(() => {
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_computeIsDownload', () => {
- assert.isTrue(element._computeIsDownload({download: true}));
- assert.isFalse(element._computeIsDownload({download: false}));
- });
-
- test('tap on trigger opens menu, then closes', () => {
- sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
- sandbox.stub(element, '_close', () => { element.$.dropdown.close(); });
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.tap(element.$.trigger);
- assert.isTrue(element.$.dropdown.opened);
- MockInteractions.tap(element.$.trigger);
- assert.isFalse(element.$.dropdown.opened);
- });
-
- test('_computeURLHelper', () => {
- const path = '/test';
- const host = 'http://www.testsite.com';
- const computedPath = element._computeURLHelper(host, path);
- assert.equal(computedPath, '//http://www.testsite.com/test');
- });
-
- test('link URLs', () => {
- assert.equal(
- element._computeLinkURL({url: 'http://example.com/test'}),
- 'http://example.com/test');
- assert.equal(
- element._computeLinkURL({url: 'https://example.com/test'}),
- 'https://example.com/test');
- assert.equal(
- element._computeLinkURL({url: '/test'}),
- '//' + window.location.host + '/test');
- assert.equal(
- element._computeLinkURL({url: '/test', target: '_blank'}),
- '/test');
- });
-
- test('link rel', () => {
- let link = {url: '/test'};
- assert.isNull(element._computeLinkRel(link));
-
- link = {url: '/test', target: '_blank'};
- assert.equal(element._computeLinkRel(link), 'noopener');
-
- link = {url: '/test', external: true};
- assert.equal(element._computeLinkRel(link), 'external');
-
- link = {url: '/test', target: '_blank', external: true};
- assert.equal(element._computeLinkRel(link), 'noopener');
- });
-
- test('_getClassIfBold', () => {
- let bold = true;
- assert.equal(element._getClassIfBold(bold), 'bold-text');
-
- bold = false;
- assert.equal(element._getClassIfBold(bold), '');
- });
-
- test('Top text exists and is bolded correctly', () => {
- element.topContent = [{text: 'User', bold: true}, {text: 'email'}];
- flushAsynchronousOperations();
- const topItems = Polymer.dom(element.root).querySelectorAll('.top-item');
- assert.equal(topItems.length, 2);
- assert.isTrue(topItems[0].classList.contains('bold-text'));
- assert.isFalse(topItems[1].classList.contains('bold-text'));
- });
-
- test('non link items', () => {
- const item0 = {name: 'item one', id: 'foo'};
- element.items = [item0, {name: 'item two', id: 'bar'}];
- const fooTapped = sandbox.stub();
- const tapped = sandbox.stub();
- element.addEventListener('tap-item-foo', fooTapped);
- element.addEventListener('tap-item', tapped);
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot
- .querySelector('.itemAction'));
- assert.isTrue(fooTapped.called);
- assert.isTrue(tapped.called);
- assert.deepEqual(tapped.lastCall.args[0].detail, item0);
- });
-
- test('disabled non link item', () => {
- element.items = [{name: 'item one', id: 'foo'}];
- element.disabledIds = ['foo'];
-
- const stub = sandbox.stub();
- const tapped = sandbox.stub();
- element.addEventListener('tap-item-foo', stub);
- element.addEventListener('tap-item', tapped);
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot
- .querySelector('.itemAction'));
- assert.isFalse(stub.called);
- assert.isFalse(tapped.called);
- });
-
- test('properly sets tooltips', () => {
element.items = [
- {name: 'item one', id: 'foo', tooltip: 'hello'},
+ {name: 'item one', id: 'foo'},
{name: 'item two', id: 'bar'},
];
- element.disabledIds = [];
flushAsynchronousOperations();
- const tooltipContents = Polymer.dom(element.root)
- .querySelectorAll('iron-dropdown li gr-tooltip-content');
- assert.equal(tooltipContents.length, 2);
- assert.isTrue(tooltipContents[0].hasTooltip);
- assert.equal(tooltipContents[0].getAttribute('title'), 'hello');
- assert.isFalse(tooltipContents[1].hasTooltip);
});
- suite('keyboard navigation', () => {
- setup(() => {
- element.items = [
- {name: 'item one', id: 'foo'},
- {name: 'item two', id: 'bar'},
- ];
- flushAsynchronousOperations();
- });
+ test('down', () => {
+ const stub = sandbox.stub(element.$.cursor, 'next');
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isTrue(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 40);
+ assert.isTrue(stub.called);
+ });
- test('down', () => {
- const stub = sandbox.stub(element.$.cursor, 'next');
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isTrue(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 40);
- assert.isTrue(stub.called);
- });
+ test('up', () => {
+ const stub = sandbox.stub(element.$.cursor, 'previous');
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isTrue(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 38);
+ assert.isTrue(stub.called);
+ });
- test('up', () => {
- const stub = sandbox.stub(element.$.cursor, 'previous');
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isTrue(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 38);
- assert.isTrue(stub.called);
- });
+ test('enter/space', () => {
+ // Because enter and space are handled by the same fn, we need only to
+ // test one.
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+ assert.isTrue(element.$.dropdown.opened);
- test('enter/space', () => {
- // Because enter and space are handled by the same fn, we need only to
- // test one.
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
- assert.isTrue(element.$.dropdown.opened);
-
- const el = element.$.cursor.target.querySelector(':not([hidden]) a');
- const stub = sandbox.stub(el, 'click');
- MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
- assert.isTrue(stub.called);
- });
+ const el = element.$.cursor.target.querySelector(':not([hidden]) a');
+ const stub = sandbox.stub(el, 'click');
+ MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
+ assert.isTrue(stub.called);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 4510d3f..a417e3f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -14,152 +14,163 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const RESTORED_MESSAGE = 'Content restored from a previous edit.';
- const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-storage/gr-storage.js';
+import '../gr-button/gr-button.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-editable-content_html.js';
+
+const RESTORED_MESSAGE = 'Content restored from a previous edit.';
+const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrEditableContent extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-editable-content'; }
+ /**
+ * Fired when the save button is pressed.
+ *
+ * @event editable-content-save
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when the cancel button is pressed.
+ *
+ * @event editable-content-cancel
*/
- class GrEditableContent extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-editable-content'; }
- /**
- * Fired when the save button is pressed.
- *
- * @event editable-content-save
- */
- /**
- * Fired when the cancel button is pressed.
- *
- * @event editable-content-cancel
- */
+ /**
+ * Fired when content is restored from storage.
+ *
+ * @event show-alert
+ */
- /**
- * Fired when content is restored from storage.
- *
- * @event show-alert
- */
-
- static get properties() {
- return {
- content: {
- notify: true,
- type: String,
- },
- disabled: {
- reflectToAttribute: true,
- type: Boolean,
- value: false,
- },
- editing: {
- observer: '_editingChanged',
- type: Boolean,
- value: false,
- },
- removeZeroWidthSpace: Boolean,
- // If no storage key is provided, content is not stored.
- storageKey: String,
- _saveDisabled: {
- computed: '_computeSaveDisabled(disabled, content, _newContent)',
- type: Boolean,
- value: true,
- },
- _newContent: {
- type: String,
- observer: '_newContentChanged',
- },
- };
- }
-
- focusTextarea() {
- this.shadowRoot.querySelector('iron-autogrow-textarea').textarea.focus();
- }
-
- _newContentChanged(newContent, oldContent) {
- if (!this.storageKey) { return; }
-
- this.debounce('store', () => {
- if (newContent.length) {
- this.$.storage.setEditableContentItem(this.storageKey, newContent);
- } else {
- // This does not really happen, because we don't clear newContent
- // after saving (see below). So this only occurs when the user clears
- // all the content in the editable textarea. But <gr-storage> cleans
- // up itself after one day, so we are not so concerned about leaving
- // some garbage behind.
- this.$.storage.eraseEditableContentItem(this.storageKey);
- }
- }, STORAGE_DEBOUNCE_INTERVAL_MS);
- }
-
- _editingChanged(editing) {
- // This method is for initializing _newContent when you start editing.
- // Restoring content from local storage is not perfect and has
- // some issues:
- //
- // 1. When you start editing in multiple tabs, then we are vulnerable to
- // race conditions between the tabs.
- // 2. The stored content is keyed by revision, so when you upload a new
- // patchset and click "reload" and then click "cancel" on the content-
- // editable, then you won't be able to recover the content anymore.
- //
- // Because of these issues we believe that it is better to only recover
- // content from local storage when you enter editing mode for the first
- // time. Otherwise it is better to just keep the last editing state from
- // the same session.
- if (!editing || this._newContent) {
- return;
- }
-
- let content;
- if (this.storageKey) {
- const storedContent =
- this.$.storage.getEditableContentItem(this.storageKey);
- if (storedContent && storedContent.message) {
- content = storedContent.message;
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {message: RESTORED_MESSAGE},
- bubbles: true,
- composed: true,
- }));
- }
- }
- if (!content) {
- content = this.content || '';
- }
-
- // TODO(wyatta) switch linkify sequence, see issue 5526.
- this._newContent = this.removeZeroWidthSpace ?
- content.replace(/^R=\u200B/gm, 'R=') :
- content;
- }
-
- _computeSaveDisabled(disabled, content, newContent) {
- return disabled || !newContent || content === newContent;
- }
-
- _handleSave(e) {
- e.preventDefault();
- this.fire('editable-content-save', {content: this._newContent});
- // It would be nice, if we would set this._newContent = undefined here,
- // but we can only do that when we are sure that the save operation has
- // succeeded.
- }
-
- _handleCancel(e) {
- e.preventDefault();
- this.editing = false;
- this.fire('editable-content-cancel');
- }
+ static get properties() {
+ return {
+ content: {
+ notify: true,
+ type: String,
+ },
+ disabled: {
+ reflectToAttribute: true,
+ type: Boolean,
+ value: false,
+ },
+ editing: {
+ observer: '_editingChanged',
+ type: Boolean,
+ value: false,
+ },
+ removeZeroWidthSpace: Boolean,
+ // If no storage key is provided, content is not stored.
+ storageKey: String,
+ _saveDisabled: {
+ computed: '_computeSaveDisabled(disabled, content, _newContent)',
+ type: Boolean,
+ value: true,
+ },
+ _newContent: {
+ type: String,
+ observer: '_newContentChanged',
+ },
+ };
}
- customElements.define(GrEditableContent.is, GrEditableContent);
-})();
+ focusTextarea() {
+ this.shadowRoot.querySelector('iron-autogrow-textarea').textarea.focus();
+ }
+
+ _newContentChanged(newContent, oldContent) {
+ if (!this.storageKey) { return; }
+
+ this.debounce('store', () => {
+ if (newContent.length) {
+ this.$.storage.setEditableContentItem(this.storageKey, newContent);
+ } else {
+ // This does not really happen, because we don't clear newContent
+ // after saving (see below). So this only occurs when the user clears
+ // all the content in the editable textarea. But <gr-storage> cleans
+ // up itself after one day, so we are not so concerned about leaving
+ // some garbage behind.
+ this.$.storage.eraseEditableContentItem(this.storageKey);
+ }
+ }, STORAGE_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _editingChanged(editing) {
+ // This method is for initializing _newContent when you start editing.
+ // Restoring content from local storage is not perfect and has
+ // some issues:
+ //
+ // 1. When you start editing in multiple tabs, then we are vulnerable to
+ // race conditions between the tabs.
+ // 2. The stored content is keyed by revision, so when you upload a new
+ // patchset and click "reload" and then click "cancel" on the content-
+ // editable, then you won't be able to recover the content anymore.
+ //
+ // Because of these issues we believe that it is better to only recover
+ // content from local storage when you enter editing mode for the first
+ // time. Otherwise it is better to just keep the last editing state from
+ // the same session.
+ if (!editing || this._newContent) {
+ return;
+ }
+
+ let content;
+ if (this.storageKey) {
+ const storedContent =
+ this.$.storage.getEditableContentItem(this.storageKey);
+ if (storedContent && storedContent.message) {
+ content = storedContent.message;
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: RESTORED_MESSAGE},
+ bubbles: true,
+ composed: true,
+ }));
+ }
+ }
+ if (!content) {
+ content = this.content || '';
+ }
+
+ // TODO(wyatta) switch linkify sequence, see issue 5526.
+ this._newContent = this.removeZeroWidthSpace ?
+ content.replace(/^R=\u200B/gm, 'R=') :
+ content;
+ }
+
+ _computeSaveDisabled(disabled, content, newContent) {
+ return disabled || !newContent || content === newContent;
+ }
+
+ _handleSave(e) {
+ e.preventDefault();
+ this.fire('editable-content-save', {content: this._newContent});
+ // It would be nice, if we would set this._newContent = undefined here,
+ // but we can only do that when we are sure that the save operation has
+ // succeeded.
+ }
+
+ _handleCancel(e) {
+ e.preventDefault();
+ this.editing = false;
+ this.fire('editable-content-cancel');
+ }
+}
+
+customElements.define(GrEditableContent.is, GrEditableContent);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js
index 627f948..e0e5047 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_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="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-storage/gr-storage.html">
-<link rel="import" href="../gr-button/gr-button.html">
-
-<dom-module id="gr-editable-content">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -60,24 +53,15 @@
justify-content: space-between;
}
</style>
- <div class="viewer" hidden$="[[editing]]">
+ <div class="viewer" hidden\$="[[editing]]">
<slot></slot>
</div>
- <div class="editor" hidden$="[[!editing]]">
- <iron-autogrow-textarea
- autocomplete="on"
- bind-value="{{_newContent}}"
- disabled="[[disabled]]"></iron-autogrow-textarea>
+ <div class="editor" hidden\$="[[!editing]]">
+ <iron-autogrow-textarea autocomplete="on" bind-value="{{_newContent}}" disabled="[[disabled]]"></iron-autogrow-textarea>
<div class="editButtons">
- <gr-button primary
- on-click="_handleSave"
- disabled="[[_saveDisabled]]">Save</gr-button>
- <gr-button
- on-click="_handleCancel"
- disabled="[[disabled]]">Cancel</gr-button>
+ <gr-button primary="" on-click="_handleSave" disabled="[[_saveDisabled]]">Save</gr-button>
+ <gr-button on-click="_handleCancel" disabled="[[disabled]]">Cancel</gr-button>
</div>
</div>
<gr-storage id="storage"></gr-storage>
- </template>
- <script src="gr-editable-content.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
index d34ff78..8f3b4d3 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -19,12 +19,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-editable-content</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>
<!-- Can't use absolute path below for mock-interaction.js.
Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
actually /node_modules directory). Also, wct patches some files to load modules from /components.
@@ -33,9 +33,14 @@
-->
<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-<link rel="import" href="gr-editable-content.html">
+<script type="module" src="./gr-editable-content.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-editable-content.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -43,128 +48,130 @@
</template>
</test-fixture>
-<script>
- suite('gr-editable-content 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-editable-content.js';
+suite('gr-editable-content tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('save event', done => {
+ element.content = '';
+ element._newContent = 'foo';
+ element.addEventListener('editable-content-save', e => {
+ assert.equal(e.detail.content, 'foo');
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button[primary]'));
+ });
+
+ test('cancel event', done => {
+ element.addEventListener('editable-content-cancel', () => {
+ done();
+ });
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('gr-button:not([primary])'));
+ });
+
+ test('enabling editing keeps old content', () => {
+ element.content = 'current content';
+ element._newContent = 'old content';
+ element.editing = true;
+ assert.equal(element._newContent, 'old content');
+ });
+
+ test('disabling editing does not update edit field contents', () => {
+ element.content = 'current content';
+ element.editing = true;
+ element._newContent = 'stale content';
+ element.editing = false;
+ assert.equal(element._newContent, 'stale content');
+ });
+
+ test('zero width spaces are removed properly', () => {
+ element.removeZeroWidthSpace = true;
+ element.content = 'R=\u200Btest@google.com';
+ element.editing = true;
+ assert.equal(element._newContent, 'R=test@google.com');
+ });
+
+ suite('editing', () => {
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('save event', done => {
- element.content = '';
- element._newContent = 'foo';
- element.addEventListener('editable-content-save', e => {
- assert.equal(e.detail.content, 'foo');
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button[primary]'));
- });
-
- test('cancel event', done => {
- element.addEventListener('editable-content-cancel', () => {
- done();
- });
- MockInteractions.tap(element.shadowRoot
- .querySelector('gr-button:not([primary])'));
- });
-
- test('enabling editing keeps old content', () => {
- element.content = 'current content';
- element._newContent = 'old content';
- element.editing = true;
- assert.equal(element._newContent, 'old content');
- });
-
- test('disabling editing does not update edit field contents', () => {
element.content = 'current content';
element.editing = true;
- element._newContent = 'stale content';
- element.editing = false;
- assert.equal(element._newContent, 'stale content');
});
- test('zero width spaces are removed properly', () => {
- element.removeZeroWidthSpace = true;
- element.content = 'R=\u200Btest@google.com';
- element.editing = true;
- assert.equal(element._newContent, 'R=test@google.com');
+ test('save button is disabled initially', () => {
+ assert.isTrue(element.shadowRoot
+ .querySelector('gr-button[primary]').disabled);
});
- suite('editing', () => {
- setup(() => {
- element.content = 'current content';
- element.editing = true;
- });
-
- test('save button is disabled initially', () => {
- assert.isTrue(element.shadowRoot
- .querySelector('gr-button[primary]').disabled);
- });
-
- test('save button is enabled when content changes', () => {
- element._newContent = 'new content';
- assert.isFalse(element.shadowRoot
- .querySelector('gr-button[primary]').disabled);
- });
- });
-
- suite('storageKey and related behavior', () => {
- let dispatchSpy;
- setup(() => {
- element.content = 'current content';
- element.storageKey = 'test';
- dispatchSpy = sandbox.spy(element, 'dispatchEvent');
- });
-
- test('editing toggled to true, has stored data', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({message: 'stored content'});
- element.editing = true;
-
- assert.equal(element._newContent, 'stored content');
- assert.isTrue(dispatchSpy.called);
- assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
- });
-
- test('editing toggled to true, has no stored data', () => {
- sandbox.stub(element.$.storage, 'getEditableContentItem')
- .returns({});
- element.editing = true;
-
- assert.equal(element._newContent, 'current content');
- assert.isFalse(dispatchSpy.called);
- });
-
- test('edits are cached', () => {
- const storeStub =
- sandbox.stub(element.$.storage, 'setEditableContentItem');
- const eraseStub =
- sandbox.stub(element.$.storage, 'eraseEditableContentItem');
- element.editing = true;
-
- element._newContent = 'new content';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isTrue(storeStub.called);
- assert.deepEqual(
- [element.storageKey, element._newContent],
- storeStub.lastCall.args);
-
- element._newContent = '';
- flushAsynchronousOperations();
- element.flushDebouncer('store');
-
- assert.isTrue(eraseStub.called);
- assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
- });
+ test('save button is enabled when content changes', () => {
+ element._newContent = 'new content';
+ assert.isFalse(element.shadowRoot
+ .querySelector('gr-button[primary]').disabled);
});
});
+
+ suite('storageKey and related behavior', () => {
+ let dispatchSpy;
+ setup(() => {
+ element.content = 'current content';
+ element.storageKey = 'test';
+ dispatchSpy = sandbox.spy(element, 'dispatchEvent');
+ });
+
+ test('editing toggled to true, has stored data', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({message: 'stored content'});
+ element.editing = true;
+
+ assert.equal(element._newContent, 'stored content');
+ assert.isTrue(dispatchSpy.called);
+ assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert');
+ });
+
+ test('editing toggled to true, has no stored data', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({});
+ element.editing = true;
+
+ assert.equal(element._newContent, 'current content');
+ assert.isFalse(dispatchSpy.called);
+ });
+
+ test('edits are cached', () => {
+ const storeStub =
+ sandbox.stub(element.$.storage, 'setEditableContentItem');
+ const eraseStub =
+ sandbox.stub(element.$.storage, 'eraseEditableContentItem');
+ element.editing = true;
+
+ element._newContent = 'new content';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isTrue(storeStub.called);
+ assert.deepEqual(
+ [element.storageKey, element._newContent],
+ storeStub.lastCall.args);
+
+ element._newContent = '';
+ flushAsynchronousOperations();
+ element.flushDebouncer('store');
+
+ assert.isTrue(eraseStub.called);
+ assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index ef5bb8c..3de5a64 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -14,193 +14,207 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const AWAIT_MAX_ITERS = 10;
- const AWAIT_STEP = 5;
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {IronOverlayBehaviorImpl} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
+import '@polymer/iron-dropdown/iron-dropdown.js';
+import '@polymer/paper-input/paper-input.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.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-editable-label_html.js';
+
+const AWAIT_MAX_ITERS = 10;
+const AWAIT_STEP = 5;
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrEditableLabel extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-editable-label'; }
+ /**
+ * Fired when the value is changed.
+ *
+ * @event changed
+ */
+
+ static get properties() {
+ return {
+ labelText: String,
+ editing: {
+ type: Boolean,
+ value: false,
+ },
+ value: {
+ type: String,
+ notify: true,
+ value: '',
+ observer: '_updateTitle',
+ },
+ placeholder: {
+ type: String,
+ value: '',
+ },
+ readOnly: {
+ type: Boolean,
+ value: false,
+ },
+ uppercase: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false,
+ },
+ maxLength: Number,
+ _inputText: String,
+ // This is used to push the iron-input element up on the page, so
+ // the input is placed in approximately the same position as the
+ // trigger.
+ _verticalOffset: {
+ type: Number,
+ readOnly: true,
+ value: -30,
+ },
+ };
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('tabindex', '0');
+ }
+
+ get keyBindings() {
+ return {
+ enter: '_handleEnter',
+ esc: '_handleEsc',
+ };
+ }
+
+ _usePlaceholder(value, placeholder) {
+ return (!value || !value.length) && placeholder;
+ }
+
+ _computeLabel(value, placeholder) {
+ if (this._usePlaceholder(value, placeholder)) {
+ return placeholder;
+ }
+ return value;
+ }
+
+ _showDropdown() {
+ if (this.readOnly || this.editing) { return; }
+ return this._open().then(() => {
+ this._nativeInput.focus();
+ if (!this.$.input.value) { return; }
+ this._nativeInput.setSelectionRange(0, this.$.input.value.length);
+ });
+ }
+
+ open() {
+ return this._open().then(() => {
+ this._nativeInput.focus();
+ });
+ }
+
+ _open(...args) {
+ this.$.dropdown.open();
+ this._inputText = this.value;
+ this.editing = true;
+
+ return new Promise(resolve => {
+ IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args);
+ this._awaitOpen(resolve);
+ });
+ }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
+ * opening. Eventually replace with a direct way to listen to the overlay.
*/
- class GrEditableLabel extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-editable-label'; }
- /**
- * Fired when the value is changed.
- *
- * @event changed
- */
+ _awaitOpen(fn) {
+ let iters = 0;
+ const step = () => {
+ this.async(() => {
+ if (this.$.dropdown.style.display !== 'none') {
+ fn.call(this);
+ } else if (iters++ < AWAIT_MAX_ITERS) {
+ step.call(this);
+ }
+ }, AWAIT_STEP);
+ };
+ step.call(this);
+ }
- static get properties() {
- return {
- labelText: String,
- editing: {
- type: Boolean,
- value: false,
- },
- value: {
- type: String,
- notify: true,
- value: '',
- observer: '_updateTitle',
- },
- placeholder: {
- type: String,
- value: '',
- },
- readOnly: {
- type: Boolean,
- value: false,
- },
- uppercase: {
- type: Boolean,
- reflectToAttribute: true,
- value: false,
- },
- maxLength: Number,
- _inputText: String,
- // This is used to push the iron-input element up on the page, so
- // the input is placed in approximately the same position as the
- // trigger.
- _verticalOffset: {
- type: Number,
- readOnly: true,
- value: -30,
- },
- };
- }
+ _id() {
+ return this.getAttribute('id') || 'global';
+ }
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('tabindex', '0');
- }
+ _save() {
+ if (!this.editing) { return; }
+ this.$.dropdown.close();
+ this.value = this._inputText;
+ this.editing = false;
+ this.fire('changed', this.value);
+ }
- get keyBindings() {
- return {
- enter: '_handleEnter',
- esc: '_handleEsc',
- };
- }
+ _cancel() {
+ if (!this.editing) { return; }
+ this.$.dropdown.close();
+ this.editing = false;
+ this._inputText = this.value;
+ }
- _usePlaceholder(value, placeholder) {
- return (!value || !value.length) && placeholder;
- }
+ get _nativeInput() {
+ // In Polymer 2, the namespace of nativeInput
+ // changed from input to nativeInput
+ return this.$.input.$.nativeInput || this.$.input.$.input;
+ }
- _computeLabel(value, placeholder) {
- if (this._usePlaceholder(value, placeholder)) {
- return placeholder;
- }
- return value;
- }
-
- _showDropdown() {
- if (this.readOnly || this.editing) { return; }
- return this._open().then(() => {
- this._nativeInput.focus();
- if (!this.$.input.value) { return; }
- this._nativeInput.setSelectionRange(0, this.$.input.value.length);
- });
- }
-
- open() {
- return this._open().then(() => {
- this._nativeInput.focus();
- });
- }
-
- _open(...args) {
- this.$.dropdown.open();
- this._inputText = this.value;
- this.editing = true;
-
- return new Promise(resolve => {
- Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args);
- this._awaitOpen(resolve);
- });
- }
-
- /**
- * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
- * opening. Eventually replace with a direct way to listen to the overlay.
- */
- _awaitOpen(fn) {
- let iters = 0;
- const step = () => {
- this.async(() => {
- if (this.$.dropdown.style.display !== 'none') {
- fn.call(this);
- } else if (iters++ < AWAIT_MAX_ITERS) {
- step.call(this);
- }
- }, AWAIT_STEP);
- };
- step.call(this);
- }
-
- _id() {
- return this.getAttribute('id') || 'global';
- }
-
- _save() {
- if (!this.editing) { return; }
- this.$.dropdown.close();
- this.value = this._inputText;
- this.editing = false;
- this.fire('changed', this.value);
- }
-
- _cancel() {
- if (!this.editing) { return; }
- this.$.dropdown.close();
- this.editing = false;
- this._inputText = this.value;
- }
-
- get _nativeInput() {
- // In Polymer 2, the namespace of nativeInput
- // changed from input to nativeInput
- return this.$.input.$.nativeInput || this.$.input.$.input;
- }
-
- _handleEnter(e) {
- e = this.getKeyboardEvent(e);
- const target = Polymer.dom(e).rootTarget;
- if (target === this._nativeInput) {
- e.preventDefault();
- this._save();
- }
- }
-
- _handleEsc(e) {
- e = this.getKeyboardEvent(e);
- const target = Polymer.dom(e).rootTarget;
- if (target === this._nativeInput) {
- e.preventDefault();
- this._cancel();
- }
- }
-
- _computeLabelClass(readOnly, value, placeholder) {
- const classes = [];
- if (!readOnly) { classes.push('editable'); }
- if (this._usePlaceholder(value, placeholder)) {
- classes.push('placeholder');
- }
- return classes.join(' ');
- }
-
- _updateTitle(value) {
- this.setAttribute('title', this._computeLabel(value, this.placeholder));
+ _handleEnter(e) {
+ e = this.getKeyboardEvent(e);
+ const target = dom(e).rootTarget;
+ if (target === this._nativeInput) {
+ e.preventDefault();
+ this._save();
}
}
- customElements.define(GrEditableLabel.is, GrEditableLabel);
-})();
+ _handleEsc(e) {
+ e = this.getKeyboardEvent(e);
+ const target = dom(e).rootTarget;
+ if (target === this._nativeInput) {
+ e.preventDefault();
+ this._cancel();
+ }
+ }
+
+ _computeLabelClass(readOnly, value, placeholder) {
+ const classes = [];
+ if (!readOnly) { classes.push('editable'); }
+ if (this._usePlaceholder(value, placeholder)) {
+ classes.push('placeholder');
+ }
+ return classes.join(' ');
+ }
+
+ _updateTitle(value) {
+ this.setAttribute('title', this._computeLabel(value, this.placeholder));
+ }
+}
+
+customElements.define(GrEditableLabel.is, GrEditableLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
index 8d0d1c37..9bc31a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
@@ -1,31 +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/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
-<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html">
-<link rel="import" href="/bower_components/paper-input/paper-input.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-button/gr-button.html">
-
-<dom-module id="gr-editable-label">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
align-items: center;
@@ -78,30 +69,16 @@
--paper-input-container-focus-color: var(--link-color);
}
</style>
- <label
- class$="[[_computeLabelClass(readOnly, value, placeholder)]]"
- title$="[[_computeLabel(value, placeholder)]]"
- on-click="_showDropdown">[[_computeLabel(value, placeholder)]]</label>
- <iron-dropdown id="dropdown"
- vertical-align="auto"
- horizontal-align="auto"
- vertical-offset="[[_verticalOffset]]"
- allow-outside-scroll="true"
- on-iron-overlay-canceled="_cancel">
+ <label class\$="[[_computeLabelClass(readOnly, value, placeholder)]]" title\$="[[_computeLabel(value, placeholder)]]" on-click="_showDropdown">[[_computeLabel(value, placeholder)]]</label>
+ <iron-dropdown id="dropdown" vertical-align="auto" horizontal-align="auto" vertical-offset="[[_verticalOffset]]" allow-outside-scroll="true" on-iron-overlay-canceled="_cancel">
<div class="dropdown-content" slot="dropdown-content">
<div class="inputContainer">
- <paper-input
- id="input"
- label="[[labelText]]"
- maxlength="[[maxLength]]"
- value="{{_inputText}}"></paper-input>
+ <paper-input id="input" label="[[labelText]]" maxlength="[[maxLength]]" value="{{_inputText}}"></paper-input>
<div class="buttons">
- <gr-button link id="cancelBtn" on-click="_cancel">cancel</gr-button>
- <gr-button link id="saveBtn" on-click="_save">save</gr-button>
+ <gr-button link="" id="cancelBtn" on-click="_cancel">cancel</gr-button>
+ <gr-button link="" id="saveBtn" on-click="_save">save</gr-button>
</div>
</div>
</div>
</iron-dropdown>
- </template>
- <script src="gr-editable-label.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index ccf5e3b..29e6619 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -19,12 +19,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-editable-label</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>
<!-- Can't use absolute path below for mock-interaction.js.
Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
actually /node_modules directory). Also, wct patches some files to load modules from /components.
@@ -33,9 +33,14 @@
-->
<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-<link rel="import" href="gr-editable-label.html">
+<script type="module" src="./gr-editable-label.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-editable-label.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -60,197 +65,200 @@
</template>
</test-fixture>
-<script>
- suite('gr-editable-label tests', async () => {
- await readyToTest();
- let element;
- let elementNoPlaceholder;
- let input;
- let label;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-editable-label.js';
+import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-editable-label tests', () => {
+ let element;
+ let elementNoPlaceholder;
+ let input;
+ let label;
+ let sandbox;
- setup(done => {
- element = fixture('basic');
- elementNoPlaceholder = fixture('no-placeholder');
+ setup(done => {
+ element = fixture('basic');
+ elementNoPlaceholder = fixture('no-placeholder');
- label = element.shadowRoot
- .querySelector('label');
- sandbox = sinon.sandbox.create();
- flush(() => {
- // In Polymer 2 inputElement isn't nativeInput anymore
- input = element.$.input.$.nativeInput || element.$.input.inputElement;
- done();
- });
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('element render', () => {
- // The dropdown is closed and the label is visible:
- assert.isFalse(element.$.dropdown.opened);
- assert.isTrue(label.classList.contains('editable'));
- assert.equal(label.textContent, 'value text');
- const focusSpy = sandbox.spy(input, 'focus');
- const showSpy = sandbox.spy(element, '_showDropdown');
-
- MockInteractions.tap(label);
-
- return showSpy.lastCall.returnValue.then(() => {
- // The dropdown is open (which covers up the label):
- assert.isTrue(element.$.dropdown.opened);
- assert.isTrue(focusSpy.called);
- assert.equal(input.value, 'value text');
- });
- });
-
- test('title with placeholder', done => {
- assert.equal(element.title, 'value text');
- element.value = '';
-
- element.async(() => {
- assert.equal(element.title, 'label text');
- done();
- });
- });
-
- test('title without placeholder', done => {
- assert.equal(elementNoPlaceholder.title, '');
- element.value = 'value text';
-
- element.async(() => {
- assert.equal(element.title, 'value text');
- done();
- });
- });
-
- test('edit value', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isTrue(editedStub.called);
- assert.equal(input.value, 'new text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press enter:
- MockInteractions.keyDownOn(input, 13);
- });
-
- test('save button', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isTrue(editedStub.called);
- assert.equal(input.value, 'new text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press enter:
- MockInteractions.tap(element.$.saveBtn, 13);
- });
-
- test('edit and then escape key', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isFalse(editedStub.called);
- // Text changes sould be discarded.
- assert.equal(input.value, 'value text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press escape:
- MockInteractions.keyDownOn(input, 27);
- });
-
- test('cancel button', done => {
- const editedStub = sandbox.stub();
- element.addEventListener('changed', editedStub);
- assert.isFalse(element.editing);
-
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- assert.isTrue(element.editing);
- element._inputText = 'new text';
-
- assert.isFalse(editedStub.called);
-
- element.async(() => {
- assert.isFalse(editedStub.called);
- // Text changes sould be discarded.
- assert.equal(input.value, 'value text');
- assert.isFalse(element.editing);
- done();
- });
-
- // Press escape:
- MockInteractions.tap(element.$.cancelBtn);
- });
-
- suite('gr-editable-label read-only tests', () => {
- let element;
- let label;
-
- setup(() => {
- element = fixture('read-only');
- label = element.shadowRoot
- .querySelector('label');
- });
-
- test('disallows edit when read-only', () => {
- // The dropdown is closed.
- assert.isFalse(element.$.dropdown.opened);
- MockInteractions.tap(label);
-
- Polymer.dom.flush();
-
- // The dropdown is still closed.
- assert.isFalse(element.$.dropdown.opened);
- });
-
- test('label is not marked as editable', () => {
- assert.isFalse(label.classList.contains('editable'));
- });
+ label = element.shadowRoot
+ .querySelector('label');
+ sandbox = sinon.sandbox.create();
+ flush(() => {
+ // In Polymer 2 inputElement isn't nativeInput anymore
+ input = element.$.input.$.nativeInput || element.$.input.inputElement;
+ done();
});
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('element render', () => {
+ // The dropdown is closed and the label is visible:
+ assert.isFalse(element.$.dropdown.opened);
+ assert.isTrue(label.classList.contains('editable'));
+ assert.equal(label.textContent, 'value text');
+ const focusSpy = sandbox.spy(input, 'focus');
+ const showSpy = sandbox.spy(element, '_showDropdown');
+
+ MockInteractions.tap(label);
+
+ return showSpy.lastCall.returnValue.then(() => {
+ // The dropdown is open (which covers up the label):
+ assert.isTrue(element.$.dropdown.opened);
+ assert.isTrue(focusSpy.called);
+ assert.equal(input.value, 'value text');
+ });
+ });
+
+ test('title with placeholder', done => {
+ assert.equal(element.title, 'value text');
+ element.value = '';
+
+ element.async(() => {
+ assert.equal(element.title, 'label text');
+ done();
+ });
+ });
+
+ test('title without placeholder', done => {
+ assert.equal(elementNoPlaceholder.title, '');
+ element.value = 'value text';
+
+ element.async(() => {
+ assert.equal(element.title, 'value text');
+ done();
+ });
+ });
+
+ test('edit value', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isTrue(editedStub.called);
+ assert.equal(input.value, 'new text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press enter:
+ MockInteractions.keyDownOn(input, 13);
+ });
+
+ test('save button', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isTrue(editedStub.called);
+ assert.equal(input.value, 'new text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press enter:
+ MockInteractions.tap(element.$.saveBtn, 13);
+ });
+
+ test('edit and then escape key', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isFalse(editedStub.called);
+ // Text changes sould be discarded.
+ assert.equal(input.value, 'value text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press escape:
+ MockInteractions.keyDownOn(input, 27);
+ });
+
+ test('cancel button', done => {
+ const editedStub = sandbox.stub();
+ element.addEventListener('changed', editedStub);
+ assert.isFalse(element.editing);
+
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ assert.isTrue(element.editing);
+ element._inputText = 'new text';
+
+ assert.isFalse(editedStub.called);
+
+ element.async(() => {
+ assert.isFalse(editedStub.called);
+ // Text changes sould be discarded.
+ assert.equal(input.value, 'value text');
+ assert.isFalse(element.editing);
+ done();
+ });
+
+ // Press escape:
+ MockInteractions.tap(element.$.cancelBtn);
+ });
+
+ suite('gr-editable-label read-only tests', () => {
+ let element;
+ let label;
+
+ setup(() => {
+ element = fixture('read-only');
+ label = element.shadowRoot
+ .querySelector('label');
+ });
+
+ test('disallows edit when read-only', () => {
+ // The dropdown is closed.
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(label);
+
+ flush$0();
+
+ // The dropdown is still closed.
+ assert.isFalse(element.$.dropdown.opened);
+ });
+
+ test('label is not marked as editable', () => {
+ assert.isFalse(label.classList.contains('editable'));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
index 305702a..a58df2e 100644
--- a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
@@ -19,13 +19,18 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</title>
-<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-js-api-interface/gr-js-api-interface.html">
+<script src="../../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.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-js-api-interface/gr-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-js-api-interface/gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -33,118 +38,120 @@
</template>
</test-fixture>
-<script>
- suite('gr-event-interface tests', async () => {
- await readyToTest();
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../gr-js-api-interface/gr-js-api-interface.js';
+suite('gr-event-interface tests', () => {
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('test on Gerrit', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
+ fixture('basic');
+ Gerrit.removeAllListeners();
});
- teardown(() => {
- sandbox.restore();
+ test('communicate between plugin and Gerrit', done => {
+ const eventName = 'test-plugin-event';
+ let p;
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ assert.equal(e.plugin, p);
+ done();
+ });
+ Gerrit.install(plugin => {
+ p = plugin;
+ Gerrit.emit(eventName, {value: 'test', plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
});
- suite('test on Gerrit', () => {
- setup(() => {
- fixture('basic');
- Gerrit.removeAllListeners();
+ test('listen on events from core', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.on(eventName, e => {
+ assert.equal(e.value, 'test');
+ done();
});
- test('communicate between plugin and Gerrit', done => {
- const eventName = 'test-plugin-event';
- let p;
+ Gerrit.emit(eventName, {value: 'test'});
+ });
+
+ test('communicate across plugins', done => {
+ const eventName = 'test-plugin-event';
+ Gerrit.install(plugin => {
Gerrit.on(eventName, e => {
- assert.equal(e.value, 'test');
- assert.equal(e.plugin, p);
+ assert.equal(e.plugin.getPluginName(), 'testB');
done();
});
- Gerrit.install(plugin => {
- p = plugin;
- Gerrit.emit(eventName, {value: 'test', plugin});
- }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- });
+ }, '0.1',
+ 'http://test.com/plugins/testA/static/testA.js');
- test('listen on events from core', done => {
- const eventName = 'test-plugin-event';
- Gerrit.on(eventName, e => {
- assert.equal(e.value, 'test');
- done();
- });
-
- Gerrit.emit(eventName, {value: 'test'});
- });
-
- test('communicate across plugins', done => {
- const eventName = 'test-plugin-event';
- Gerrit.install(plugin => {
- Gerrit.on(eventName, e => {
- assert.equal(e.plugin.getPluginName(), 'testB');
- done();
- });
- }, '0.1',
- 'http://test.com/plugins/testA/static/testA.js');
-
- Gerrit.install(plugin => {
- Gerrit.emit(eventName, {plugin});
- }, '0.1',
- 'http://test.com/plugins/testB/static/testB.js');
- });
- });
-
- suite('test on interfaces', () => {
- let testObj;
-
- class TestClass extends EventEmitter {
- }
-
- setup(() => {
- testObj = new TestClass();
- });
-
- test('on', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.emit('test');
- testObj.emit('test');
- assert.isTrue(cbStub.calledTwice);
- });
-
- test('once', () => {
- const cbStub = sinon.stub();
- testObj.once('test', cbStub);
- testObj.emit('test');
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('unsubscribe', () => {
- const cbStub = sinon.stub();
- const unsubscribe = testObj.on('test', cbStub);
- testObj.emit('test');
- unsubscribe();
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('off', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.emit('test');
- testObj.off('test', cbStub);
- testObj.emit('test');
- assert.isTrue(cbStub.calledOnce);
- });
-
- test('removeAllListeners', () => {
- const cbStub = sinon.stub();
- testObj.on('test', cbStub);
- testObj.removeAllListeners('test');
- testObj.emit('test');
- assert.isTrue(cbStub.notCalled);
- });
+ Gerrit.install(plugin => {
+ Gerrit.emit(eventName, {plugin});
+ }, '0.1',
+ 'http://test.com/plugins/testB/static/testB.js');
});
});
+
+ suite('test on interfaces', () => {
+ let testObj;
+
+ class TestClass extends EventEmitter {
+ }
+
+ setup(() => {
+ testObj = new TestClass();
+ });
+
+ test('on', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledTwice);
+ });
+
+ test('once', () => {
+ const cbStub = sinon.stub();
+ testObj.once('test', cbStub);
+ testObj.emit('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('unsubscribe', () => {
+ const cbStub = sinon.stub();
+ const unsubscribe = testObj.on('test', cbStub);
+ testObj.emit('test');
+ unsubscribe();
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('off', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.emit('test');
+ testObj.off('test', cbStub);
+ testObj.emit('test');
+ assert.isTrue(cbStub.calledOnce);
+ });
+
+ test('removeAllListeners', () => {
+ const cbStub = sinon.stub();
+ testObj.on('test', cbStub);
+ testObj.removeAllListeners('test');
+ testObj.emit('test');
+ assert.isTrue(cbStub.notCalled);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index d4995cc..0d19f00 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -14,225 +14,231 @@
* 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 GrFixedPanel extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-fixed-panel'; }
+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-fixed-panel_html.js';
- static get properties() {
- return {
- floatingDisabled: {
- type: Boolean,
- value: false,
- },
- readyForMeasure: {
- type: Boolean,
- observer: '_readyForMeasureObserver',
- },
- keepOnScroll: {
- type: Boolean,
- value: false,
- },
- _isMeasured: {
- type: Boolean,
- value: false,
- },
+/** @extends Polymer.Element */
+class GrFixedPanel extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * Initial offset from the top of the document, in pixels.
- */
- _topInitial: Number,
+ static get is() { return 'gr-fixed-panel'; }
- /**
- * Current offset from the top of the window, in pixels.
- */
- _topLast: Number,
+ static get properties() {
+ return {
+ floatingDisabled: {
+ type: Boolean,
+ value: false,
+ },
+ readyForMeasure: {
+ type: Boolean,
+ observer: '_readyForMeasureObserver',
+ },
+ keepOnScroll: {
+ type: Boolean,
+ value: false,
+ },
+ _isMeasured: {
+ type: Boolean,
+ value: false,
+ },
- _headerHeight: Number,
- _headerFloating: {
- type: Boolean,
- value: false,
- },
- _observer: {
- type: Object,
- value: null,
- },
- /**
- * If place before any other content defines how much
- * of the content below it is covered by this panel
- */
- floatingHeight: {
- type: Number,
- value: 0,
- notify: true,
- },
+ /**
+ * Initial offset from the top of the document, in pixels.
+ */
+ _topInitial: Number,
- _webComponentsReady: Boolean,
- };
+ /**
+ * Current offset from the top of the window, in pixels.
+ */
+ _topLast: Number,
+
+ _headerHeight: Number,
+ _headerFloating: {
+ type: Boolean,
+ value: false,
+ },
+ _observer: {
+ type: Object,
+ value: null,
+ },
+ /**
+ * If place before any other content defines how much
+ * of the content below it is covered by this panel
+ */
+ floatingHeight: {
+ type: Number,
+ value: 0,
+ notify: true,
+ },
+
+ _webComponentsReady: Boolean,
+ };
+ }
+
+ static get observers() {
+ return [
+ '_updateFloatingHeight(floatingDisabled, _isMeasured, _headerHeight)',
+ ];
+ }
+
+ _updateFloatingHeight(floatingDisabled, isMeasured, headerHeight) {
+ if ([
+ floatingDisabled,
+ isMeasured,
+ headerHeight,
+ ].some(arg => arg === undefined)) {
+ return;
}
+ this.floatingHeight =
+ (!floatingDisabled && isMeasured) ? headerHeight : 0;
+ }
- static get observers() {
- return [
- '_updateFloatingHeight(floatingDisabled, _isMeasured, _headerHeight)',
- ];
+ /** @override */
+ attached() {
+ super.attached();
+ if (this.floatingDisabled) {
+ return;
}
-
- _updateFloatingHeight(floatingDisabled, isMeasured, headerHeight) {
- if ([
- floatingDisabled,
- isMeasured,
- headerHeight,
- ].some(arg => arg === undefined)) {
- return;
- }
- this.floatingHeight =
- (!floatingDisabled && isMeasured) ? headerHeight : 0;
+ // Enable content measure unless blocked by param.
+ if (this.readyForMeasure !== false) {
+ this.readyForMeasure = true;
}
+ this.listen(window, 'resize', 'update');
+ this.listen(window, 'scroll', '_updateOnScroll');
+ this._observer = new MutationObserver(this.update.bind(this));
+ this._observer.observe(this.$.header, {childList: true, subtree: true});
+ }
- /** @override */
- attached() {
- super.attached();
- if (this.floatingDisabled) {
- return;
- }
- // Enable content measure unless blocked by param.
- if (this.readyForMeasure !== false) {
- this.readyForMeasure = true;
- }
- this.listen(window, 'resize', 'update');
- this.listen(window, 'scroll', '_updateOnScroll');
- this._observer = new MutationObserver(this.update.bind(this));
- this._observer.observe(this.$.header, {childList: true, subtree: true});
- }
-
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_updateOnScroll');
- this.unlisten(window, 'resize', 'update');
- if (this._observer) {
- this._observer.disconnect();
- }
- }
-
- _readyForMeasureObserver(readyForMeasure) {
- if (readyForMeasure) {
- this.update();
- }
- }
-
- _computeHeaderClass(headerFloating, topLast) {
- const fixedAtTop = this.keepOnScroll && topLast === 0;
- return [
- headerFloating ? 'floating' : '',
- fixedAtTop ? 'fixedAtTop' : '',
- ].join(' ');
- }
-
- unfloat() {
- if (this.floatingDisabled) {
- return;
- }
- this.$.header.style.top = '';
- this._headerFloating = false;
- this.updateStyles({'--header-height': ''});
- }
-
- update() {
- this.debounce('update', () => {
- this._updateDebounced();
- }, 100);
- }
-
- _updateOnScroll() {
- this.debounce('update', () => {
- this._updateDebounced();
- });
- }
-
- _updateDebounced() {
- if (this.floatingDisabled) {
- return;
- }
- this._isMeasured = false;
- this._maybeFloatHeader();
- this._reposition();
- }
-
- _getElementTop() {
- return this.getBoundingClientRect().top;
- }
-
- _reposition() {
- if (!this._headerFloating) {
- return;
- }
- const header = this.$.header;
- // Since the outer element is relative positioned, can use its top
- // to determine how to position the inner header element.
- const elemTop = this._getElementTop();
- let newTop;
- if (this.keepOnScroll && elemTop < 0) {
- // Should stick to the top.
- newTop = 0;
- } else {
- // Keep in line with the outer element.
- newTop = elemTop;
- }
- // Initialize top style if it doesn't exist yet.
- if (!header.style.top && this._topLast === newTop) {
- header.style.top = newTop;
- }
- if (this._topLast !== newTop) {
- if (newTop === undefined) {
- header.style.top = '';
- } else {
- header.style.top = newTop + 'px';
- }
- this._topLast = newTop;
- }
- }
-
- _measure() {
- if (this._isMeasured) {
- return; // Already measured.
- }
- const rect = this.$.header.getBoundingClientRect();
- if (rect.height === 0 && rect.width === 0) {
- return; // Not ready for measurement yet.
- }
- const top = document.body.scrollTop + rect.top;
- this._topLast = top;
- this._headerHeight = rect.height;
- this._topInitial =
- this.getBoundingClientRect().top + document.body.scrollTop;
- this._isMeasured = true;
- }
-
- _isFloatingNeeded() {
- return this.keepOnScroll ||
- document.body.scrollWidth > document.body.clientWidth;
- }
-
- _maybeFloatHeader() {
- if (!this._isFloatingNeeded()) {
- return;
- }
- this._measure();
- if (this._isMeasured) {
- this._floatHeader();
- }
- }
-
- _floatHeader() {
- this.updateStyles({'--header-height': this._headerHeight + 'px'});
- this._headerFloating = true;
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_updateOnScroll');
+ this.unlisten(window, 'resize', 'update');
+ if (this._observer) {
+ this._observer.disconnect();
}
}
- customElements.define(GrFixedPanel.is, GrFixedPanel);
-})();
+ _readyForMeasureObserver(readyForMeasure) {
+ if (readyForMeasure) {
+ this.update();
+ }
+ }
+
+ _computeHeaderClass(headerFloating, topLast) {
+ const fixedAtTop = this.keepOnScroll && topLast === 0;
+ return [
+ headerFloating ? 'floating' : '',
+ fixedAtTop ? 'fixedAtTop' : '',
+ ].join(' ');
+ }
+
+ unfloat() {
+ if (this.floatingDisabled) {
+ return;
+ }
+ this.$.header.style.top = '';
+ this._headerFloating = false;
+ this.updateStyles({'--header-height': ''});
+ }
+
+ update() {
+ this.debounce('update', () => {
+ this._updateDebounced();
+ }, 100);
+ }
+
+ _updateOnScroll() {
+ this.debounce('update', () => {
+ this._updateDebounced();
+ });
+ }
+
+ _updateDebounced() {
+ if (this.floatingDisabled) {
+ return;
+ }
+ this._isMeasured = false;
+ this._maybeFloatHeader();
+ this._reposition();
+ }
+
+ _getElementTop() {
+ return this.getBoundingClientRect().top;
+ }
+
+ _reposition() {
+ if (!this._headerFloating) {
+ return;
+ }
+ const header = this.$.header;
+ // Since the outer element is relative positioned, can use its top
+ // to determine how to position the inner header element.
+ const elemTop = this._getElementTop();
+ let newTop;
+ if (this.keepOnScroll && elemTop < 0) {
+ // Should stick to the top.
+ newTop = 0;
+ } else {
+ // Keep in line with the outer element.
+ newTop = elemTop;
+ }
+ // Initialize top style if it doesn't exist yet.
+ if (!header.style.top && this._topLast === newTop) {
+ header.style.top = newTop;
+ }
+ if (this._topLast !== newTop) {
+ if (newTop === undefined) {
+ header.style.top = '';
+ } else {
+ header.style.top = newTop + 'px';
+ }
+ this._topLast = newTop;
+ }
+ }
+
+ _measure() {
+ if (this._isMeasured) {
+ return; // Already measured.
+ }
+ const rect = this.$.header.getBoundingClientRect();
+ if (rect.height === 0 && rect.width === 0) {
+ return; // Not ready for measurement yet.
+ }
+ const top = document.body.scrollTop + rect.top;
+ this._topLast = top;
+ this._headerHeight = rect.height;
+ this._topInitial =
+ this.getBoundingClientRect().top + document.body.scrollTop;
+ this._isMeasured = true;
+ }
+
+ _isFloatingNeeded() {
+ return this.keepOnScroll ||
+ document.body.scrollWidth > document.body.clientWidth;
+ }
+
+ _maybeFloatHeader() {
+ if (!this._isFloatingNeeded()) {
+ return;
+ }
+ this._measure();
+ if (this._isMeasured) {
+ this._floatHeader();
+ }
+ }
+
+ _floatHeader() {
+ this.updateStyles({'--header-height': this._headerHeight + 'px'});
+ this._headerFloating = true;
+ }
+}
+
+customElements.define(GrFixedPanel.is, GrFixedPanel);
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
index 14285b4..69ae735 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
@@ -1,25 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-fixed-panel">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
box-sizing: border-box;
@@ -44,9 +41,7 @@
box-shadow: var(--elevation-level-2);
}
</style>
- <header id="header" class$="[[_computeHeaderClass(_headerFloating, _topLast)]]">
+ <header id="header" class\$="[[_computeHeaderClass(_headerFloating, _topLast)]]">
<slot></slot>
</header>
- </template>
- <script src="gr-fixed-panel.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
index 7ae0265..e2f77c5 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-fixed-panel</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-fixed-panel.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-fixed-panel.js"></script>
<style>
/* Prevent horizontal scrolling on page.
New version of web-component-tester creates body with margins */
@@ -35,7 +35,12 @@
}
</style>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-fixed-panel.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -45,83 +50,85 @@
</template>
</test-fixture>
-<script>
- suite('gr-fixed-panel', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-fixed-panel.js';
+suite('gr-fixed-panel', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.readyForMeasure = true;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('can be disabled with floatingDisabled', () => {
+ element.floatingDisabled = true;
+ sandbox.stub(element, '_reposition');
+ window.dispatchEvent(new CustomEvent('resize'));
+ element.flushDebouncer('update');
+ assert.isFalse(element._reposition.called);
+ });
+
+ test('header is the height of the content', () => {
+ assert.equal(element.getBoundingClientRect().height, 100);
+ });
+
+ test('scroll triggers _reposition', () => {
+ sandbox.stub(element, '_reposition');
+ window.dispatchEvent(new CustomEvent('scroll'));
+ element.flushDebouncer('update');
+ assert.isTrue(element._reposition.called);
+ });
+
+ suite('_reposition', () => {
+ const getHeaderTop = function() {
+ return element.$.header.style.top;
+ };
+
+ const emulateScrollY = function(distance) {
+ element._getElementTop.returns(element._headerTopInitial - distance);
+ element._updateDebounced();
+ element.flushDebouncer('scroll');
+ };
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.readyForMeasure = true;
+ element._headerTopInitial = 10;
+ sandbox.stub(element, '_getElementTop')
+ .returns(element._headerTopInitial);
});
- teardown(() => {
- sandbox.restore();
+ test('scrolls header along with document', () => {
+ emulateScrollY(20);
+ // No top property is set when !_headerFloating.
+ assert.equal(getHeaderTop(), '');
});
- test('can be disabled with floatingDisabled', () => {
- element.floatingDisabled = true;
- sandbox.stub(element, '_reposition');
- window.dispatchEvent(new CustomEvent('resize'));
- element.flushDebouncer('update');
- assert.isFalse(element._reposition.called);
+ test('does not stick to the top by default', () => {
+ emulateScrollY(150);
+ // No top property is set when !_headerFloating.
+ assert.equal(getHeaderTop(), '');
});
- test('header is the height of the content', () => {
- assert.equal(element.getBoundingClientRect().height, 100);
+ test('sticks to the top if enabled', () => {
+ element.keepOnScroll = true;
+ emulateScrollY(120);
+ assert.equal(getHeaderTop(), '0px');
});
- test('scroll triggers _reposition', () => {
- sandbox.stub(element, '_reposition');
- window.dispatchEvent(new CustomEvent('scroll'));
- element.flushDebouncer('update');
- assert.isTrue(element._reposition.called);
- });
-
- suite('_reposition', () => {
- const getHeaderTop = function() {
- return element.$.header.style.top;
- };
-
- const emulateScrollY = function(distance) {
- element._getElementTop.returns(element._headerTopInitial - distance);
- element._updateDebounced();
- element.flushDebouncer('scroll');
- };
-
- setup(() => {
- element._headerTopInitial = 10;
- sandbox.stub(element, '_getElementTop')
- .returns(element._headerTopInitial);
- });
-
- test('scrolls header along with document', () => {
- emulateScrollY(20);
- // No top property is set when !_headerFloating.
- assert.equal(getHeaderTop(), '');
- });
-
- test('does not stick to the top by default', () => {
- emulateScrollY(150);
- // No top property is set when !_headerFloating.
- assert.equal(getHeaderTop(), '');
- });
-
- test('sticks to the top if enabled', () => {
- element.keepOnScroll = true;
- emulateScrollY(120);
- assert.equal(getHeaderTop(), '0px');
- });
-
- test('drops a shadow when fixed to the top', () => {
- element.keepOnScroll = true;
- emulateScrollY(5);
- assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
- emulateScrollY(120);
- assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
- });
+ test('drops a shadow when fixed to the top', () => {
+ element.keepOnScroll = true;
+ emulateScrollY(5);
+ assert.isFalse(element.$.header.classList.contains('fixedAtTop'));
+ emulateScrollY(120);
+ assert.isTrue(element.$.header.classList.contains('fixedAtTop'));
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
index e62fcc8..139e09c 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -14,288 +14,296 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // eslint-disable-next-line no-unused-vars
- const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
- const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
+import '../gr-linked-text/gr-linked-text.js';
+import '../../../styles/shared-styles.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-formatted-text_html.js';
- /** @extends Polymer.Element */
- class GrFormattedText extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-formatted-text'; }
+// eslint-disable-next-line no-unused-vars
+const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
+const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
- static get properties() {
- return {
- content: {
- type: String,
- observer: '_contentChanged',
- },
- config: Object,
- noTrailingMargin: {
- type: Boolean,
- value: false,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrFormattedText extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_contentOrConfigChanged(content, config)',
- ];
- }
+ static get is() { return 'gr-formatted-text'; }
- /** @override */
- ready() {
- super.ready();
- if (this.noTrailingMargin) {
- this.classList.add('noTrailingMargin');
- }
- }
+ static get properties() {
+ return {
+ content: {
+ type: String,
+ observer: '_contentChanged',
+ },
+ config: Object,
+ noTrailingMargin: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- _contentChanged(content) {
- // In the case where the config may not be set (perhaps due to the
- // request for it still being in flight), set the content anyway to
- // prevent waiting on the config to display the text.
- if (this.config) { return; }
- this._contentOrConfigChanged(content);
- }
+ static get observers() {
+ return [
+ '_contentOrConfigChanged(content, config)',
+ ];
+ }
- /**
- * Given a source string, update the DOM inside #container.
- */
- _contentOrConfigChanged(content) {
- const container = Polymer.dom(this.$.container);
-
- // Remove existing content.
- while (container.firstChild) {
- container.removeChild(container.firstChild);
- }
-
- // Add new content.
- for (const node of this._computeNodes(this._computeBlocks(content))) {
- container.appendChild(node);
- }
- }
-
- /**
- * Given a source string, parse into an array of block objects. Each block
- * has a `type` property which takes any of the follwoing values.
- * * 'paragraph'
- * * 'quote' (Block quote.)
- * * 'pre' (Pre-formatted text.)
- * * 'list' (Unordered list.)
- * * 'code' (code blocks.)
- *
- * For blocks of type 'paragraph', 'pre' and 'code' there is a `text`
- * property that maps to a string of the block's content.
- *
- * For blocks of type 'list', there is an `items` property that maps to a
- * list of strings representing the list items.
- *
- * For blocks of type 'quote', there is a `blocks` property that maps to a
- * list of blocks contained in the quote.
- *
- * NOTE: Strings appearing in all block objects are NOT escaped.
- *
- * @param {string} content
- * @return {!Array<!Object>}
- */
- _computeBlocks(content) {
- if (!content) { return []; }
-
- const result = [];
- const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
-
- for (let i = 0; i < lines.length; i++) {
- if (!lines[i].length) {
- continue;
- }
-
- if (this._isCodeMarkLine(lines[i])) {
- // handle multi-line code
- let nextI = i+1;
- while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
- nextI++;
- }
-
- if (this._isCodeMarkLine(lines[nextI])) {
- result.push({
- type: 'code',
- text: lines.slice(i+1, nextI).join('\n'),
- });
- i = nextI;
- continue;
- }
-
- // otherwise treat it as regular line and continue
- // check for other cases
- }
-
- if (this._isSingleLineCode(lines[i])) {
- // no guard check as _isSingleLineCode tested on the pattern
- const codeContent = lines[i].match(CODE_MARKER_PATTERN)[2];
- result.push({type: 'code', text: codeContent});
- } else if (this._isList(lines[i])) {
- let nextI = i + 1;
- while (this._isList(lines[nextI])) {
- nextI++;
- }
- result.push(this._makeList(lines.slice(i, nextI)));
- i = nextI - 1;
- } else if (this._isQuote(lines[i])) {
- let nextI = i + 1;
- while (this._isQuote(lines[nextI])) {
- nextI++;
- }
- const blockLines = lines.slice(i, nextI)
- .map(l => l.replace(/^[ ]?>[ ]?/, ''));
- result.push({
- type: 'quote',
- blocks: this._computeBlocks(blockLines.join('\n')),
- });
- i = nextI - 1;
- } else if (this._isPreFormat(lines[i])) {
- let nextI = i + 1;
- // include pre or all regular lines but stop at next new line
- while (this._isPreFormat(lines[nextI])
- || (this._isRegularLine(lines[nextI]) && lines[nextI].length)) {
- nextI++;
- }
- result.push({
- type: 'pre',
- text: lines.slice(i, nextI).join('\n'),
- });
- i = nextI - 1;
- } else {
- let nextI = i + 1;
- while (this._isRegularLine(lines[nextI])) {
- nextI++;
- }
- result.push({
- type: 'paragraph',
- text: lines.slice(i, nextI).join('\n'),
- });
- i = nextI - 1;
- }
- }
-
- return result;
- }
-
- /**
- * Take a block of comment text that contains a list, generate appropriate
- * block objects and append them to the output list.
- *
- * * Item one.
- * * Item two.
- * * item three.
- *
- * TODO(taoalpha): maybe we should also support nested list
- *
- * @param {!Array<string>} lines The block containing the list.
- */
- _makeList(lines) {
- const block = {type: 'list', items: []};
- let line;
-
- for (let i = 0; i < lines.length; i++) {
- line = lines[i];
- line = line.substring(1).trim();
- block.items.push(line);
- }
- return block;
- }
-
- _isRegularLine(line) {
- // line can not be recognized by existing patterns
- if (line === undefined) return false;
- return !this._isQuote(line) && !this._isCodeMarkLine(line)
- && !this._isSingleLineCode(line) && !this._isList(line) &&
- !this._isPreFormat(line);
- }
-
- _isQuote(line) {
- return line && (line.startsWith('> ') || line.startsWith(' > '));
- }
-
- _isCodeMarkLine(line) {
- return line && line.trim() === '```';
- }
-
- _isSingleLineCode(line) {
- return line && CODE_MARKER_PATTERN.test(line);
- }
-
- _isPreFormat(line) {
- return line && /^[ \t]/.test(line);
- }
-
- _isList(line) {
- return line && /^[-*] /.test(line);
- }
-
- /**
- * @param {string} content
- * @param {boolean=} opt_isPre
- */
- _makeLinkedText(content, opt_isPre) {
- const text = document.createElement('gr-linked-text');
- text.config = this.config;
- text.content = content;
- text.pre = true;
- if (opt_isPre) {
- text.classList.add('pre');
- }
- return text;
- }
-
- /**
- * Map an array of block objects to an array of DOM nodes.
- *
- * @param {!Array<!Object>} blocks
- * @return {!Array<!HTMLElement>}
- */
- _computeNodes(blocks) {
- return blocks.map(block => {
- if (block.type === 'paragraph') {
- const p = document.createElement('p');
- p.appendChild(this._makeLinkedText(block.text));
- return p;
- }
-
- if (block.type === 'quote') {
- const bq = document.createElement('blockquote');
- for (const node of this._computeNodes(block.blocks)) {
- bq.appendChild(node);
- }
- return bq;
- }
-
- if (block.type === 'code') {
- const code = document.createElement('code');
- code.textContent = block.text;
- return code;
- }
-
- if (block.type === 'pre') {
- return this._makeLinkedText(block.text, true);
- }
-
- if (block.type === 'list') {
- const ul = document.createElement('ul');
- for (const item of block.items) {
- const li = document.createElement('li');
- li.appendChild(this._makeLinkedText(item));
- ul.appendChild(li);
- }
- return ul;
- }
- });
+ /** @override */
+ ready() {
+ super.ready();
+ if (this.noTrailingMargin) {
+ this.classList.add('noTrailingMargin');
}
}
- customElements.define(GrFormattedText.is, GrFormattedText);
-})();
+ _contentChanged(content) {
+ // In the case where the config may not be set (perhaps due to the
+ // request for it still being in flight), set the content anyway to
+ // prevent waiting on the config to display the text.
+ if (this.config) { return; }
+ this._contentOrConfigChanged(content);
+ }
+
+ /**
+ * Given a source string, update the DOM inside #container.
+ */
+ _contentOrConfigChanged(content) {
+ const container = dom(this.$.container);
+
+ // Remove existing content.
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+
+ // Add new content.
+ for (const node of this._computeNodes(this._computeBlocks(content))) {
+ container.appendChild(node);
+ }
+ }
+
+ /**
+ * Given a source string, parse into an array of block objects. Each block
+ * has a `type` property which takes any of the follwoing values.
+ * * 'paragraph'
+ * * 'quote' (Block quote.)
+ * * 'pre' (Pre-formatted text.)
+ * * 'list' (Unordered list.)
+ * * 'code' (code blocks.)
+ *
+ * For blocks of type 'paragraph', 'pre' and 'code' there is a `text`
+ * property that maps to a string of the block's content.
+ *
+ * For blocks of type 'list', there is an `items` property that maps to a
+ * list of strings representing the list items.
+ *
+ * For blocks of type 'quote', there is a `blocks` property that maps to a
+ * list of blocks contained in the quote.
+ *
+ * NOTE: Strings appearing in all block objects are NOT escaped.
+ *
+ * @param {string} content
+ * @return {!Array<!Object>}
+ */
+ _computeBlocks(content) {
+ if (!content) { return []; }
+
+ const result = [];
+ const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ if (!lines[i].length) {
+ continue;
+ }
+
+ if (this._isCodeMarkLine(lines[i])) {
+ // handle multi-line code
+ let nextI = i+1;
+ while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
+ nextI++;
+ }
+
+ if (this._isCodeMarkLine(lines[nextI])) {
+ result.push({
+ type: 'code',
+ text: lines.slice(i+1, nextI).join('\n'),
+ });
+ i = nextI;
+ continue;
+ }
+
+ // otherwise treat it as regular line and continue
+ // check for other cases
+ }
+
+ if (this._isSingleLineCode(lines[i])) {
+ // no guard check as _isSingleLineCode tested on the pattern
+ const codeContent = lines[i].match(CODE_MARKER_PATTERN)[2];
+ result.push({type: 'code', text: codeContent});
+ } else if (this._isList(lines[i])) {
+ let nextI = i + 1;
+ while (this._isList(lines[nextI])) {
+ nextI++;
+ }
+ result.push(this._makeList(lines.slice(i, nextI)));
+ i = nextI - 1;
+ } else if (this._isQuote(lines[i])) {
+ let nextI = i + 1;
+ while (this._isQuote(lines[nextI])) {
+ nextI++;
+ }
+ const blockLines = lines.slice(i, nextI)
+ .map(l => l.replace(/^[ ]?>[ ]?/, ''));
+ result.push({
+ type: 'quote',
+ blocks: this._computeBlocks(blockLines.join('\n')),
+ });
+ i = nextI - 1;
+ } else if (this._isPreFormat(lines[i])) {
+ let nextI = i + 1;
+ // include pre or all regular lines but stop at next new line
+ while (this._isPreFormat(lines[nextI])
+ || (this._isRegularLine(lines[nextI]) && lines[nextI].length)) {
+ nextI++;
+ }
+ result.push({
+ type: 'pre',
+ text: lines.slice(i, nextI).join('\n'),
+ });
+ i = nextI - 1;
+ } else {
+ let nextI = i + 1;
+ while (this._isRegularLine(lines[nextI])) {
+ nextI++;
+ }
+ result.push({
+ type: 'paragraph',
+ text: lines.slice(i, nextI).join('\n'),
+ });
+ i = nextI - 1;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Take a block of comment text that contains a list, generate appropriate
+ * block objects and append them to the output list.
+ *
+ * * Item one.
+ * * Item two.
+ * * item three.
+ *
+ * TODO(taoalpha): maybe we should also support nested list
+ *
+ * @param {!Array<string>} lines The block containing the list.
+ */
+ _makeList(lines) {
+ const block = {type: 'list', items: []};
+ let line;
+
+ for (let i = 0; i < lines.length; i++) {
+ line = lines[i];
+ line = line.substring(1).trim();
+ block.items.push(line);
+ }
+ return block;
+ }
+
+ _isRegularLine(line) {
+ // line can not be recognized by existing patterns
+ if (line === undefined) return false;
+ return !this._isQuote(line) && !this._isCodeMarkLine(line)
+ && !this._isSingleLineCode(line) && !this._isList(line) &&
+ !this._isPreFormat(line);
+ }
+
+ _isQuote(line) {
+ return line && (line.startsWith('> ') || line.startsWith(' > '));
+ }
+
+ _isCodeMarkLine(line) {
+ return line && line.trim() === '```';
+ }
+
+ _isSingleLineCode(line) {
+ return line && CODE_MARKER_PATTERN.test(line);
+ }
+
+ _isPreFormat(line) {
+ return line && /^[ \t]/.test(line);
+ }
+
+ _isList(line) {
+ return line && /^[-*] /.test(line);
+ }
+
+ /**
+ * @param {string} content
+ * @param {boolean=} opt_isPre
+ */
+ _makeLinkedText(content, opt_isPre) {
+ const text = document.createElement('gr-linked-text');
+ text.config = this.config;
+ text.content = content;
+ text.pre = true;
+ if (opt_isPre) {
+ text.classList.add('pre');
+ }
+ return text;
+ }
+
+ /**
+ * Map an array of block objects to an array of DOM nodes.
+ *
+ * @param {!Array<!Object>} blocks
+ * @return {!Array<!HTMLElement>}
+ */
+ _computeNodes(blocks) {
+ return blocks.map(block => {
+ if (block.type === 'paragraph') {
+ const p = document.createElement('p');
+ p.appendChild(this._makeLinkedText(block.text));
+ return p;
+ }
+
+ if (block.type === 'quote') {
+ const bq = document.createElement('blockquote');
+ for (const node of this._computeNodes(block.blocks)) {
+ bq.appendChild(node);
+ }
+ return bq;
+ }
+
+ if (block.type === 'code') {
+ const code = document.createElement('code');
+ code.textContent = block.text;
+ return code;
+ }
+
+ if (block.type === 'pre') {
+ return this._makeLinkedText(block.text, true);
+ }
+
+ if (block.type === 'list') {
+ const ul = document.createElement('ul');
+ for (const item of block.items) {
+ const li = document.createElement('li');
+ li.appendChild(this._makeLinkedText(item));
+ ul.appendChild(li);
+ }
+ return ul;
+ }
+ });
+ }
+}
+
+customElements.define(GrFormattedText.is, GrFormattedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
index 0c254ee..a30b65a 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
@@ -1,25 +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="../gr-linked-text/gr-linked-text.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-formatted-text">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -66,6 +63,4 @@
</style>
<div id="container"></div>
- </template>
- <script src="gr-formatted-text.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
index a6d8524..56bd44ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_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-editable-label</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-formatted-text.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-formatted-text.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-formatted-text.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,395 +40,397 @@
</template>
</test-fixture>
-<script>
- suite('gr-formatted-text 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-formatted-text.js';
+suite('gr-formatted-text tests', () => {
+ let element;
+ let sandbox;
- function assertBlock(result, index, type, text) {
- assert.equal(result[index].type, type);
- assert.equal(result[index].text, text);
- }
+ function assertBlock(result, index, type, text) {
+ assert.equal(result[index].type, type);
+ assert.equal(result[index].text, text);
+ }
- function assertListBlock(result, resultIndex, itemIndex, text) {
- assert.equal(result[resultIndex].type, 'list');
- assert.equal(result[resultIndex].items[itemIndex], text);
- }
+ function assertListBlock(result, resultIndex, itemIndex, text) {
+ assert.equal(result[resultIndex].type, 'list');
+ assert.equal(result[resultIndex].items[itemIndex], text);
+ }
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('parse null undefined and empty', () => {
- assert.lengthOf(element._computeBlocks(null), 0);
- assert.lengthOf(element._computeBlocks(undefined), 0);
- assert.lengthOf(element._computeBlocks(''), 0);
- });
-
- test('parse simple', () => {
- const comment = 'Para1';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'paragraph', comment);
- });
-
- test('parse multiline para', () => {
- const comment = 'Para 1\nStill para 1';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'paragraph', comment);
- });
-
- test('parse para break without special blocks', () => {
- const comment = 'Para 1\n\nPara 2\n\nPara 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'paragraph', comment);
- });
-
- test('parse quote', () => {
- const comment = '> Quote text';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
- });
-
- test('parse quote lead space', () => {
- const comment = ' > Quote text';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
- });
-
- test('parse multiline quote', () => {
- const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph',
- 'Quote line 1\nQuote line 2\nQuote line 3');
- });
-
- test('parse pre', () => {
- const comment = ' Four space indent.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'pre', comment);
- });
-
- test('parse one space pre', () => {
- const comment = ' One space indent.\n Another line.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'pre', comment);
- });
-
- test('parse tab pre', () => {
- const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertBlock(result, 0, 'pre', comment);
- });
-
- test('parse star list', () => {
- const comment = '* Item 1\n* Item 2\n* Item 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result, 0, 0, 'Item 1');
- assertListBlock(result, 0, 1, 'Item 2');
- assertListBlock(result, 0, 2, 'Item 3');
- });
-
- test('parse dash list', () => {
- const comment = '- Item 1\n- Item 2\n- Item 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result, 0, 0, 'Item 1');
- assertListBlock(result, 0, 1, 'Item 2');
- assertListBlock(result, 0, 2, 'Item 3');
- });
-
- test('parse mixed list', () => {
- const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result, 0, 0, 'Item 1');
- assertListBlock(result, 0, 1, 'Item 2');
- assertListBlock(result, 0, 2, 'Item 3');
- assertListBlock(result, 0, 3, 'Item 4');
- });
-
- test('parse mixed block types', () => {
- const comment = 'Paragraph\nacross\na\nfew\nlines.' +
- '\n\n' +
- '> Quote\n> across\n> not many lines.' +
- '\n\n' +
- 'Another paragraph' +
- '\n\n' +
- '* Series\n* of\n* list\n* items' +
- '\n\n' +
- 'Yet another paragraph' +
- '\n\n' +
- '\tPreformatted text.' +
- '\n\n' +
- 'Parting words.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 7);
- assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.\n');
-
- assert.equal(result[1].type, 'quote');
- assert.lengthOf(result[1].blocks, 1);
- assertBlock(result[1].blocks, 0, 'paragraph',
- 'Quote\nacross\nnot many lines.');
-
- assertBlock(result, 2, 'paragraph', 'Another paragraph\n');
- assertListBlock(result, 3, 0, 'Series');
- assertListBlock(result, 3, 1, 'of');
- assertListBlock(result, 3, 2, 'list');
- assertListBlock(result, 3, 3, 'items');
- assertBlock(result, 4, 'paragraph', 'Yet another paragraph\n');
- assertBlock(result, 5, 'pre', '\tPreformatted text.');
- assertBlock(result, 6, 'paragraph', 'Parting words.');
- });
-
- test('bullet list 1', () => {
- const comment = 'A\n\n* line 1\n* 2nd line';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'A\n');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- });
-
- test('bullet list 2', () => {
- const comment = 'A\n* line 1\n* 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- assertBlock(result, 2, 'paragraph', 'B');
- });
-
- test('bullet list 3', () => {
- const comment = '* line 1\n* 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertListBlock(result, 0, 0, 'line 1');
- assertListBlock(result, 0, 1, '2nd line');
- assertBlock(result, 1, 'paragraph', 'B');
- });
-
- test('bullet list 4', () => {
- const comment = 'To see this bug, you have to:\n' +
- '* Be on IMAP or EAS (not on POP)\n' +
- '* Be very unlucky\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:');
- assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
- assertListBlock(result, 1, 1, 'Be very unlucky');
- });
-
- test('bullet list 5', () => {
- const comment = 'To see this bug,\n' +
- 'you have to:\n' +
- '* Be on IMAP or EAS (not on POP)\n' +
- '* Be very unlucky\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'To see this bug,\nyou have to:');
- assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
- assertListBlock(result, 1, 1, 'Be very unlucky');
- });
-
- test('dash list 1', () => {
- const comment = 'A\n- line 1\n- 2nd line';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'A');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- });
-
- test('dash list 2', () => {
- const comment = 'A\n- line 1\n- 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertListBlock(result, 1, 0, 'line 1');
- assertListBlock(result, 1, 1, '2nd line');
- assertBlock(result, 2, 'paragraph', 'B');
- });
-
- test('dash list 3', () => {
- const comment = '- line 1\n- 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertListBlock(result, 0, 0, 'line 1');
- assertListBlock(result, 0, 1, '2nd line');
- assertBlock(result, 1, 'paragraph', 'B');
- });
-
- test('nested list will NOT be recognized', () => {
- // will be rendered as two separate lists
- const comment = '- line 1\n - line with indentation\n- line 2';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertListBlock(result, 0, 0, 'line 1');
- assert.equal(result[1].type, 'pre');
- assertListBlock(result, 2, 0, 'line 2');
- });
-
- test('pre format 1', () => {
- const comment = 'A\n This is pre\n formatted';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'paragraph', 'A');
- assertBlock(result, 1, 'pre', ' This is pre\n formatted');
- });
-
- test('pre format 2', () => {
- const comment = 'A\n This is pre\n formatted\n\nbut this is not';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertBlock(result, 1, 'pre', ' This is pre\n formatted');
- assertBlock(result, 2, 'paragraph', 'but this is not');
- });
-
- test('pre format 3', () => {
- const comment = 'A\n Q\n <R>\n S\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'A');
- assertBlock(result, 1, 'pre', ' Q\n <R>\n S');
- assertBlock(result, 2, 'paragraph', 'B');
- });
-
- test('pre format 4', () => {
- const comment = ' Q\n <R>\n S\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertBlock(result, 0, 'pre', ' Q\n <R>\n S');
- assertBlock(result, 1, 'paragraph', 'B');
- });
-
- test('quote 1', () => {
- const comment = '> I\'m happy\n > with quotes!\n\nSee above.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 1);
- assertBlock(result[0].blocks, 0, 'paragraph', 'I\'m happy\nwith quotes!');
- assertBlock(result, 1, 'paragraph', 'See above.');
- });
-
- test('quote 2', () => {
- const comment = 'See this said:\n > a quoted\n > string block\n\nOK?';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertBlock(result, 0, 'paragraph', 'See this said:');
- assert.equal(result[1].type, 'quote');
- assert.lengthOf(result[1].blocks, 1);
- assertBlock(result[1].blocks, 0, 'paragraph', 'a quoted\nstring block');
- assertBlock(result, 2, 'paragraph', 'OK?');
- });
-
- test('nested quotes', () => {
- const comment = ' > > prior\n > \n > next\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'quote');
- assert.lengthOf(result[0].blocks, 2);
- assert.equal(result[0].blocks[0].type, 'quote');
- assert.lengthOf(result[0].blocks[0].blocks, 1);
- assertBlock(result[0].blocks[0].blocks, 0, 'paragraph', 'prior');
- assertBlock(result[0].blocks, 1, 'paragraph', 'next');
- });
-
- test('code 1', () => {
- const comment = '```\n// test code\n```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'code');
- assert.equal(result[0].text, '// test code');
- });
-
- test('code 2', () => {
- const comment = 'test code\n```// test code```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code');
- assert.equal(result[1].type, 'code');
- assert.equal(result[1].text, '// test code');
- });
-
- test('code 3', () => {
- const comment = 'test code\n```// test code```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code');
- assert.equal(result[1].type, 'code');
- assert.equal(result[1].text, '// test code');
- });
-
- test('not a code', () => {
- const comment = 'test code\n```// test code';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code\n```// test code');
- });
-
- test('not a code 2', () => {
- const comment = 'test code\n```\n// test code';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assert.equal(result[0].type, 'paragraph');
- assert.equal(result[0].text, 'test code');
- assert.equal(result[1].type, 'paragraph');
- assert.equal(result[1].text, '```\n// test code');
- });
-
- test('mix all 1', () => {
- const comment = ' bullets:\n- bullet 1\n- bullet 2\n\ncode example:\n' +
- '```// test code```\n\n> reference is here';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 5);
- assert.equal(result[0].type, 'pre');
- assert.equal(result[1].type, 'list');
- assert.equal(result[2].type, 'paragraph');
- assert.equal(result[3].type, 'code');
- assert.equal(result[4].type, 'quote');
- });
-
- test('_computeNodes called without config', () => {
- const computeNodesSpy = sandbox.spy(element, '_computeNodes');
- element.content = 'some text';
- assert.isTrue(computeNodesSpy.called);
- });
-
- test('_contentOrConfigChanged called with config', () => {
- const contentStub = sandbox.stub(element, '_contentChanged');
- const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
- element.content = 'some text';
- element.config = {};
- assert.isTrue(contentStub.called);
- assert.isTrue(contentConfigStub.called);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('parse null undefined and empty', () => {
+ assert.lengthOf(element._computeBlocks(null), 0);
+ assert.lengthOf(element._computeBlocks(undefined), 0);
+ assert.lengthOf(element._computeBlocks(''), 0);
+ });
+
+ test('parse simple', () => {
+ const comment = 'Para1';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'paragraph', comment);
+ });
+
+ test('parse multiline para', () => {
+ const comment = 'Para 1\nStill para 1';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'paragraph', comment);
+ });
+
+ test('parse para break without special blocks', () => {
+ const comment = 'Para 1\n\nPara 2\n\nPara 3';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'paragraph', comment);
+ });
+
+ test('parse quote', () => {
+ const comment = '> Quote text';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
+ });
+
+ test('parse quote lead space', () => {
+ const comment = ' > Quote text';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text');
+ });
+
+ test('parse multiline quote', () => {
+ const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph',
+ 'Quote line 1\nQuote line 2\nQuote line 3');
+ });
+
+ test('parse pre', () => {
+ const comment = ' Four space indent.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'pre', comment);
+ });
+
+ test('parse one space pre', () => {
+ const comment = ' One space indent.\n Another line.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'pre', comment);
+ });
+
+ test('parse tab pre', () => {
+ const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertBlock(result, 0, 'pre', comment);
+ });
+
+ test('parse star list', () => {
+ const comment = '* Item 1\n* Item 2\n* Item 3';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertListBlock(result, 0, 0, 'Item 1');
+ assertListBlock(result, 0, 1, 'Item 2');
+ assertListBlock(result, 0, 2, 'Item 3');
+ });
+
+ test('parse dash list', () => {
+ const comment = '- Item 1\n- Item 2\n- Item 3';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertListBlock(result, 0, 0, 'Item 1');
+ assertListBlock(result, 0, 1, 'Item 2');
+ assertListBlock(result, 0, 2, 'Item 3');
+ });
+
+ test('parse mixed list', () => {
+ const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assertListBlock(result, 0, 0, 'Item 1');
+ assertListBlock(result, 0, 1, 'Item 2');
+ assertListBlock(result, 0, 2, 'Item 3');
+ assertListBlock(result, 0, 3, 'Item 4');
+ });
+
+ test('parse mixed block types', () => {
+ const comment = 'Paragraph\nacross\na\nfew\nlines.' +
+ '\n\n' +
+ '> Quote\n> across\n> not many lines.' +
+ '\n\n' +
+ 'Another paragraph' +
+ '\n\n' +
+ '* Series\n* of\n* list\n* items' +
+ '\n\n' +
+ 'Yet another paragraph' +
+ '\n\n' +
+ '\tPreformatted text.' +
+ '\n\n' +
+ 'Parting words.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 7);
+ assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.\n');
+
+ assert.equal(result[1].type, 'quote');
+ assert.lengthOf(result[1].blocks, 1);
+ assertBlock(result[1].blocks, 0, 'paragraph',
+ 'Quote\nacross\nnot many lines.');
+
+ assertBlock(result, 2, 'paragraph', 'Another paragraph\n');
+ assertListBlock(result, 3, 0, 'Series');
+ assertListBlock(result, 3, 1, 'of');
+ assertListBlock(result, 3, 2, 'list');
+ assertListBlock(result, 3, 3, 'items');
+ assertBlock(result, 4, 'paragraph', 'Yet another paragraph\n');
+ assertBlock(result, 5, 'pre', '\tPreformatted text.');
+ assertBlock(result, 6, 'paragraph', 'Parting words.');
+ });
+
+ test('bullet list 1', () => {
+ const comment = 'A\n\n* line 1\n* 2nd line';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'A\n');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ });
+
+ test('bullet list 2', () => {
+ const comment = 'A\n* line 1\n* 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ assertBlock(result, 2, 'paragraph', 'B');
+ });
+
+ test('bullet list 3', () => {
+ const comment = '* line 1\n* 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertListBlock(result, 0, 0, 'line 1');
+ assertListBlock(result, 0, 1, '2nd line');
+ assertBlock(result, 1, 'paragraph', 'B');
+ });
+
+ test('bullet list 4', () => {
+ const comment = 'To see this bug, you have to:\n' +
+ '* Be on IMAP or EAS (not on POP)\n' +
+ '* Be very unlucky\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:');
+ assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
+ assertListBlock(result, 1, 1, 'Be very unlucky');
+ });
+
+ test('bullet list 5', () => {
+ const comment = 'To see this bug,\n' +
+ 'you have to:\n' +
+ '* Be on IMAP or EAS (not on POP)\n' +
+ '* Be very unlucky\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'To see this bug,\nyou have to:');
+ assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
+ assertListBlock(result, 1, 1, 'Be very unlucky');
+ });
+
+ test('dash list 1', () => {
+ const comment = 'A\n- line 1\n- 2nd line';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ });
+
+ test('dash list 2', () => {
+ const comment = 'A\n- line 1\n- 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertListBlock(result, 1, 0, 'line 1');
+ assertListBlock(result, 1, 1, '2nd line');
+ assertBlock(result, 2, 'paragraph', 'B');
+ });
+
+ test('dash list 3', () => {
+ const comment = '- line 1\n- 2nd line\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertListBlock(result, 0, 0, 'line 1');
+ assertListBlock(result, 0, 1, '2nd line');
+ assertBlock(result, 1, 'paragraph', 'B');
+ });
+
+ test('nested list will NOT be recognized', () => {
+ // will be rendered as two separate lists
+ const comment = '- line 1\n - line with indentation\n- line 2';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertListBlock(result, 0, 0, 'line 1');
+ assert.equal(result[1].type, 'pre');
+ assertListBlock(result, 2, 0, 'line 2');
+ });
+
+ test('pre format 1', () => {
+ const comment = 'A\n This is pre\n formatted';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertBlock(result, 1, 'pre', ' This is pre\n formatted');
+ });
+
+ test('pre format 2', () => {
+ const comment = 'A\n This is pre\n formatted\n\nbut this is not';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertBlock(result, 1, 'pre', ' This is pre\n formatted');
+ assertBlock(result, 2, 'paragraph', 'but this is not');
+ });
+
+ test('pre format 3', () => {
+ const comment = 'A\n Q\n <R>\n S\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'A');
+ assertBlock(result, 1, 'pre', ' Q\n <R>\n S');
+ assertBlock(result, 2, 'paragraph', 'B');
+ });
+
+ test('pre format 4', () => {
+ const comment = ' Q\n <R>\n S\n\nB';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assertBlock(result, 0, 'pre', ' Q\n <R>\n S');
+ assertBlock(result, 1, 'paragraph', 'B');
+ });
+
+ test('quote 1', () => {
+ const comment = '> I\'m happy\n > with quotes!\n\nSee above.';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 1);
+ assertBlock(result[0].blocks, 0, 'paragraph', 'I\'m happy\nwith quotes!');
+ assertBlock(result, 1, 'paragraph', 'See above.');
+ });
+
+ test('quote 2', () => {
+ const comment = 'See this said:\n > a quoted\n > string block\n\nOK?';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 3);
+ assertBlock(result, 0, 'paragraph', 'See this said:');
+ assert.equal(result[1].type, 'quote');
+ assert.lengthOf(result[1].blocks, 1);
+ assertBlock(result[1].blocks, 0, 'paragraph', 'a quoted\nstring block');
+ assertBlock(result, 2, 'paragraph', 'OK?');
+ });
+
+ test('nested quotes', () => {
+ const comment = ' > > prior\n > \n > next\n';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'quote');
+ assert.lengthOf(result[0].blocks, 2);
+ assert.equal(result[0].blocks[0].type, 'quote');
+ assert.lengthOf(result[0].blocks[0].blocks, 1);
+ assertBlock(result[0].blocks[0].blocks, 0, 'paragraph', 'prior');
+ assertBlock(result[0].blocks, 1, 'paragraph', 'next');
+ });
+
+ test('code 1', () => {
+ const comment = '```\n// test code\n```';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'code');
+ assert.equal(result[0].text, '// test code');
+ });
+
+ test('code 2', () => {
+ const comment = 'test code\n```// test code```';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code');
+ assert.equal(result[1].type, 'code');
+ assert.equal(result[1].text, '// test code');
+ });
+
+ test('code 3', () => {
+ const comment = 'test code\n```// test code```';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code');
+ assert.equal(result[1].type, 'code');
+ assert.equal(result[1].text, '// test code');
+ });
+
+ test('not a code', () => {
+ const comment = 'test code\n```// test code';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 1);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code\n```// test code');
+ });
+
+ test('not a code 2', () => {
+ const comment = 'test code\n```\n// test code';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 2);
+ assert.equal(result[0].type, 'paragraph');
+ assert.equal(result[0].text, 'test code');
+ assert.equal(result[1].type, 'paragraph');
+ assert.equal(result[1].text, '```\n// test code');
+ });
+
+ test('mix all 1', () => {
+ const comment = ' bullets:\n- bullet 1\n- bullet 2\n\ncode example:\n' +
+ '```// test code```\n\n> reference is here';
+ const result = element._computeBlocks(comment);
+ assert.lengthOf(result, 5);
+ assert.equal(result[0].type, 'pre');
+ assert.equal(result[1].type, 'list');
+ assert.equal(result[2].type, 'paragraph');
+ assert.equal(result[3].type, 'code');
+ assert.equal(result[4].type, 'quote');
+ });
+
+ test('_computeNodes called without config', () => {
+ const computeNodesSpy = sandbox.spy(element, '_computeNodes');
+ element.content = 'some text';
+ assert.isTrue(computeNodesSpy.called);
+ });
+
+ test('_contentOrConfigChanged called with config', () => {
+ const contentStub = sandbox.stub(element, '_contentChanged');
+ const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+ element.content = 'some text';
+ element.config = {};
+ assert.isTrue(contentStub.called);
+ assert.isTrue(contentConfigStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index ce34d3a..ce2303f 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -14,322 +14,331 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
- const HOVER_CLASS = 'hovered';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../styles/shared-styles.js';
+import '../../../scripts/rootElement.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-hovercard_html.js';
+
+const HOVER_CLASS = 'hovered';
+
+/**
+ * When the hovercard is positioned diagonally (bottom-left, bottom-right,
+ * top-left, or top-right), we add additional (invisible) padding so that the
+ * area that a user can hover over to access the hovercard is larger.
+ */
+const DIAGONAL_OVERFLOW = 15;
+
+/** @extends Polymer.Element */
+class GrHovercard extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-hovercard'; }
+
+ static get properties() {
+ return {
+ /**
+ * @type {?}
+ */
+ _target: Object,
+
+ /**
+ * Determines whether or not the hovercard is visible.
+ *
+ * @type {boolean}
+ */
+ _isShowing: {
+ type: Boolean,
+ value: false,
+ },
+ /**
+ * The `id` of the element that the hovercard is anchored to.
+ *
+ * @type {string}
+ */
+ for: {
+ type: String,
+ observer: '_forChanged',
+ },
+
+ /**
+ * The spacing between the top of the hovercard and the element it is
+ * anchored to.
+ *
+ * @type {number}
+ */
+ offset: {
+ type: Number,
+ value: 14,
+ },
+
+ /**
+ * Positions the hovercard to the top, right, bottom, left, bottom-left,
+ * bottom-right, top-left, or top-right of its content.
+ *
+ * @type {string}
+ */
+ position: {
+ type: String,
+ value: 'bottom',
+ },
+
+ container: Object,
+ /**
+ * ID for the container element.
+ *
+ * @type {string}
+ */
+ containerId: {
+ type: String,
+ value: 'gr-hovercard-container',
+ },
+ };
+ }
+
+ /** @override */
+ attached() {
+ super.attached();
+ if (!this._target) { this._target = this.target; }
+ this.listen(this._target, 'mouseenter', 'show');
+ this.listen(this._target, 'focus', 'show');
+ this.listen(this._target, 'mouseleave', 'hide');
+ this.listen(this._target, 'blur', 'hide');
+ this.listen(this._target, 'click', 'hide');
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('mouseleave',
+ e => this.hide(e));
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ // First, check to see if the container has already been created.
+ this.container = Gerrit.getRootElement()
+ .querySelector('#' + this.containerId);
+
+ if (this.container) { return; }
+
+ // If it does not exist, create and initialize the hovercard container.
+ this.container = document.createElement('div');
+ this.container.setAttribute('id', this.containerId);
+ Gerrit.getRootElement().appendChild(this.container);
+ }
+
+ removeListeners() {
+ this.unlisten(this._target, 'mouseenter', 'show');
+ this.unlisten(this._target, 'focus', 'show');
+ this.unlisten(this._target, 'mouseleave', 'hide');
+ this.unlisten(this._target, 'blur', 'hide');
+ this.unlisten(this._target, 'click', 'hide');
+ }
/**
- * When the hovercard is positioned diagonally (bottom-left, bottom-right,
- * top-left, or top-right), we add additional (invisible) padding so that the
- * area that a user can hover over to access the hovercard is larger.
+ * Returns the target element that the hovercard is anchored to (the `id` of
+ * the `for` property).
+ *
+ * @type {HTMLElement}
*/
- const DIAGONAL_OVERFLOW = 15;
+ get target() {
+ const parentNode = dom(this).parentNode;
+ // If the parentNode is a document fragment, then we need to use the host.
+ const ownerRoot = dom(this).getOwnerRoot();
+ let target;
+ if (this.for) {
+ target = dom(ownerRoot).querySelector('#' + this.for);
+ } else {
+ target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
+ ownerRoot.host :
+ parentNode;
+ }
+ return target;
+ }
- /** @extends Polymer.Element */
- class GrHovercard extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-hovercard'; }
-
- static get properties() {
- return {
- /**
- * @type {?}
- */
- _target: Object,
-
- /**
- * Determines whether or not the hovercard is visible.
- *
- * @type {boolean}
- */
- _isShowing: {
- type: Boolean,
- value: false,
- },
- /**
- * The `id` of the element that the hovercard is anchored to.
- *
- * @type {string}
- */
- for: {
- type: String,
- observer: '_forChanged',
- },
-
- /**
- * The spacing between the top of the hovercard and the element it is
- * anchored to.
- *
- * @type {number}
- */
- offset: {
- type: Number,
- value: 14,
- },
-
- /**
- * Positions the hovercard to the top, right, bottom, left, bottom-left,
- * bottom-right, top-left, or top-right of its content.
- *
- * @type {string}
- */
- position: {
- type: String,
- value: 'bottom',
- },
-
- container: Object,
- /**
- * ID for the container element.
- *
- * @type {string}
- */
- containerId: {
- type: String,
- value: 'gr-hovercard-container',
- },
- };
+ /**
+ * Hides/closes the hovercard. This occurs when the user triggers the
+ * `mouseleave` event on the hovercard's `target` element (as long as the
+ * user is not hovering over the hovercard).
+ *
+ * @param {Event} e DOM Event (e.g. `mouseleave` event)
+ */
+ hide(e) {
+ const targetRect = this._target.getBoundingClientRect();
+ const x = e.clientX;
+ const y = e.clientY;
+ if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
+ y < targetRect.bottom) {
+ // Sometimes the hovercard itself obscures the mouse pointer, and
+ // that generates a mouseleave event. We don't want to hide the hovercard
+ // in that situation.
+ return;
}
- /** @override */
- attached() {
- super.attached();
- if (!this._target) { this._target = this.target; }
- this.listen(this._target, 'mouseenter', 'show');
- this.listen(this._target, 'focus', 'show');
- this.listen(this._target, 'mouseleave', 'hide');
- this.listen(this._target, 'blur', 'hide');
- this.listen(this._target, 'click', 'hide');
+ // If the hovercard is already hidden or the user is now hovering over the
+ // hovercard or the user is returning from the hovercard but now hovering
+ // over the target (to stop an annoying flicker effect), just return.
+ if (!this._isShowing || e.toElement === this ||
+ (e.fromElement === this && e.toElement === this._target)) {
+ return;
}
- /** @override */
- created() {
- super.created();
- this.addEventListener('mouseleave',
- e => this.hide(e));
- }
+ // Mark that the hovercard is not visible and do not allow focusing
+ this._isShowing = false;
- /** @override */
- ready() {
- super.ready();
- // First, check to see if the container has already been created.
- this.container = Gerrit.getRootElement()
- .querySelector('#' + this.containerId);
+ // Clear styles in preparation for the next time we need to show the card
+ this.classList.remove(HOVER_CLASS);
- if (this.container) { return; }
+ // Reset and remove the hovercard from the DOM
+ this.style.cssText = '';
+ this.$.hovercard.setAttribute('tabindex', -1);
- // If it does not exist, create and initialize the hovercard container.
- this.container = document.createElement('div');
- this.container.setAttribute('id', this.containerId);
- Gerrit.getRootElement().appendChild(this.container);
- }
-
- removeListeners() {
- this.unlisten(this._target, 'mouseenter', 'show');
- this.unlisten(this._target, 'focus', 'show');
- this.unlisten(this._target, 'mouseleave', 'hide');
- this.unlisten(this._target, 'blur', 'hide');
- this.unlisten(this._target, 'click', 'hide');
- }
-
- /**
- * Returns the target element that the hovercard is anchored to (the `id` of
- * the `for` property).
- *
- * @type {HTMLElement}
- */
- get target() {
- const parentNode = Polymer.dom(this).parentNode;
- // If the parentNode is a document fragment, then we need to use the host.
- const ownerRoot = Polymer.dom(this).getOwnerRoot();
- let target;
- if (this.for) {
- target = Polymer.dom(ownerRoot).querySelector('#' + this.for);
- } else {
- target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
- ownerRoot.host :
- parentNode;
- }
- return target;
- }
-
- /**
- * Hides/closes the hovercard. This occurs when the user triggers the
- * `mouseleave` event on the hovercard's `target` element (as long as the
- * user is not hovering over the hovercard).
- *
- * @param {Event} e DOM Event (e.g. `mouseleave` event)
- */
- hide(e) {
- const targetRect = this._target.getBoundingClientRect();
- const x = e.clientX;
- const y = e.clientY;
- if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
- y < targetRect.bottom) {
- // Sometimes the hovercard itself obscures the mouse pointer, and
- // that generates a mouseleave event. We don't want to hide the hovercard
- // in that situation.
- return;
- }
-
- // If the hovercard is already hidden or the user is now hovering over the
- // hovercard or the user is returning from the hovercard but now hovering
- // over the target (to stop an annoying flicker effect), just return.
- if (!this._isShowing || e.toElement === this ||
- (e.fromElement === this && e.toElement === this._target)) {
- return;
- }
-
- // Mark that the hovercard is not visible and do not allow focusing
- this._isShowing = false;
-
- // Clear styles in preparation for the next time we need to show the card
- this.classList.remove(HOVER_CLASS);
-
- // Reset and remove the hovercard from the DOM
- this.style.cssText = '';
- this.$.hovercard.setAttribute('tabindex', -1);
-
- // Remove the hovercard from the container, given that it is still a child
- // of the container.
- if (this.container.contains(this)) {
- this.container.removeChild(this);
- }
- }
-
- /**
- * Shows/opens the hovercard. This occurs when the user triggers the
- * `mousenter` event on the hovercard's `target` element.
- *
- * @param {Event} e DOM Event (e.g., `mouseenter` event)
- */
- show(e) {
- if (this._isShowing) {
- return;
- }
-
- // Mark that the hovercard is now visible
- this._isShowing = true;
- this.setAttribute('tabindex', 0);
-
- // Add it to the DOM and calculate its position
- this.container.appendChild(this);
- this.updatePosition();
-
- // Trigger the transition
- this.classList.add(HOVER_CLASS);
- }
-
- /**
- * Updates the hovercard's position based on the `position` attribute
- * and the current position of the `target` element.
- *
- * The hovercard is supposed to stay open if the user hovers over it.
- * To keep it open when the user moves away from the target, the bounding
- * rects of the target and hovercard must touch or overlap.
- *
- * NOTE: You do not need to directly call this method unless you need to
- * update the position of the tooltip while it is already visible (the
- * target element has moved and the tooltip is still open).
- */
- updatePosition() {
- if (!this._target) { return; }
-
- // Calculate the necessary measurements and positions
- const parentRect = document.documentElement.getBoundingClientRect();
- const targetRect = this._target.getBoundingClientRect();
- const thisRect = this.getBoundingClientRect();
-
- const targetLeft = targetRect.left - parentRect.left;
- const targetTop = targetRect.top - parentRect.top;
-
- let hovercardLeft;
- let hovercardTop;
- const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
- let cssText = '';
-
- // Find the top and left position values based on the position attribute
- // of the hovercard.
- switch (this.position) {
- case 'top':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop - thisRect.height - this.offset;
- cssText += `padding-bottom:${this.offset
- }px; margin-bottom:-${this.offset}px;`;
- break;
- case 'bottom':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop + targetRect.height + this.offset;
- cssText +=
- `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
- break;
- case 'left':
- hovercardLeft = targetLeft - thisRect.width - this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- cssText +=
- `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
- break;
- case 'right':
- hovercardLeft = targetRect.right + this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- cssText +=
- `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
- break;
- case 'bottom-right':
- hovercardLeft = targetRect.left + targetRect.width + this.offset;
- hovercardTop = targetRect.top + targetRect.height + this.offset;
- cssText += `padding-top:${diagonalPadding}px;`;
- cssText += `padding-left:${diagonalPadding}px;`;
- cssText += `margin-left:-${diagonalPadding}px;`;
- cssText += `margin-top:-${diagonalPadding}px;`;
- break;
- case 'bottom-left':
- hovercardLeft = targetRect.left - thisRect.width - this.offset;
- hovercardTop = targetRect.top + targetRect.height + this.offset;
- cssText += `padding-top:${diagonalPadding}px;`;
- cssText += `padding-right:${diagonalPadding}px;`;
- cssText += `margin-right:-${diagonalPadding}px;`;
- cssText += `margin-top:-${diagonalPadding}px;`;
- break;
- case 'top-left':
- hovercardLeft = targetRect.left - thisRect.width - this.offset;
- hovercardTop = targetRect.top - thisRect.height - this.offset;
- cssText += `padding-bottom:${diagonalPadding}px;`;
- cssText += `padding-right:${diagonalPadding}px;`;
- cssText += `margin-bottom:-${diagonalPadding}px;`;
- cssText += `margin-right:-${diagonalPadding}px;`;
- break;
- case 'top-right':
- hovercardLeft = targetRect.left + targetRect.width + this.offset;
- hovercardTop = targetRect.top - thisRect.height - this.offset;
- cssText += `padding-bottom:${diagonalPadding}px;`;
- cssText += `padding-left:${diagonalPadding}px;`;
- cssText += `margin-bottom:-${diagonalPadding}px;`;
- cssText += `margin-left:-${diagonalPadding}px;`;
- break;
- }
-
- // Prevent hovercard from appearing outside the viewport.
- // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
- // right.
- if (hovercardLeft < 0) { hovercardLeft = 0; }
- if (hovercardTop < 0) { hovercardTop = 0; }
- // Set the hovercard's position
- cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
- this.style.cssText = cssText;
- }
-
- /**
- * Responds to a change in the `for` value and gets the updated `target`
- * element for the hovercard.
- *
- * @private
- */
- _forChanged() {
- this._target = this.target;
+ // Remove the hovercard from the container, given that it is still a child
+ // of the container.
+ if (this.container.contains(this)) {
+ this.container.removeChild(this);
}
}
- customElements.define(GrHovercard.is, GrHovercard);
-})();
+ /**
+ * Shows/opens the hovercard. This occurs when the user triggers the
+ * `mousenter` event on the hovercard's `target` element.
+ *
+ * @param {Event} e DOM Event (e.g., `mouseenter` event)
+ */
+ show(e) {
+ if (this._isShowing) {
+ return;
+ }
+
+ // Mark that the hovercard is now visible
+ this._isShowing = true;
+ this.setAttribute('tabindex', 0);
+
+ // Add it to the DOM and calculate its position
+ this.container.appendChild(this);
+ this.updatePosition();
+
+ // Trigger the transition
+ this.classList.add(HOVER_CLASS);
+ }
+
+ /**
+ * Updates the hovercard's position based on the `position` attribute
+ * and the current position of the `target` element.
+ *
+ * The hovercard is supposed to stay open if the user hovers over it.
+ * To keep it open when the user moves away from the target, the bounding
+ * rects of the target and hovercard must touch or overlap.
+ *
+ * NOTE: You do not need to directly call this method unless you need to
+ * update the position of the tooltip while it is already visible (the
+ * target element has moved and the tooltip is still open).
+ */
+ updatePosition() {
+ if (!this._target) { return; }
+
+ // Calculate the necessary measurements and positions
+ const parentRect = document.documentElement.getBoundingClientRect();
+ const targetRect = this._target.getBoundingClientRect();
+ const thisRect = this.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - parentRect.left;
+ const targetTop = targetRect.top - parentRect.top;
+
+ let hovercardLeft;
+ let hovercardTop;
+ const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
+ let cssText = '';
+
+ // Find the top and left position values based on the position attribute
+ // of the hovercard.
+ switch (this.position) {
+ case 'top':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop - thisRect.height - this.offset;
+ cssText += `padding-bottom:${this.offset
+ }px; margin-bottom:-${this.offset}px;`;
+ break;
+ case 'bottom':
+ hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+ hovercardTop = targetTop + targetRect.height + this.offset;
+ cssText +=
+ `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
+ break;
+ case 'left':
+ hovercardLeft = targetLeft - thisRect.width - this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ cssText +=
+ `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
+ break;
+ case 'right':
+ hovercardLeft = targetRect.right + this.offset;
+ hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+ cssText +=
+ `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
+ break;
+ case 'bottom-right':
+ hovercardLeft = targetRect.left + targetRect.width + this.offset;
+ hovercardTop = targetRect.top + targetRect.height + this.offset;
+ cssText += `padding-top:${diagonalPadding}px;`;
+ cssText += `padding-left:${diagonalPadding}px;`;
+ cssText += `margin-left:-${diagonalPadding}px;`;
+ cssText += `margin-top:-${diagonalPadding}px;`;
+ break;
+ case 'bottom-left':
+ hovercardLeft = targetRect.left - thisRect.width - this.offset;
+ hovercardTop = targetRect.top + targetRect.height + this.offset;
+ cssText += `padding-top:${diagonalPadding}px;`;
+ cssText += `padding-right:${diagonalPadding}px;`;
+ cssText += `margin-right:-${diagonalPadding}px;`;
+ cssText += `margin-top:-${diagonalPadding}px;`;
+ break;
+ case 'top-left':
+ hovercardLeft = targetRect.left - thisRect.width - this.offset;
+ hovercardTop = targetRect.top - thisRect.height - this.offset;
+ cssText += `padding-bottom:${diagonalPadding}px;`;
+ cssText += `padding-right:${diagonalPadding}px;`;
+ cssText += `margin-bottom:-${diagonalPadding}px;`;
+ cssText += `margin-right:-${diagonalPadding}px;`;
+ break;
+ case 'top-right':
+ hovercardLeft = targetRect.left + targetRect.width + this.offset;
+ hovercardTop = targetRect.top - thisRect.height - this.offset;
+ cssText += `padding-bottom:${diagonalPadding}px;`;
+ cssText += `padding-left:${diagonalPadding}px;`;
+ cssText += `margin-bottom:-${diagonalPadding}px;`;
+ cssText += `margin-left:-${diagonalPadding}px;`;
+ break;
+ }
+
+ // Prevent hovercard from appearing outside the viewport.
+ // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
+ // right.
+ if (hovercardLeft < 0) { hovercardLeft = 0; }
+ if (hovercardTop < 0) { hovercardTop = 0; }
+ // Set the hovercard's position
+ cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
+ this.style.cssText = cssText;
+ }
+
+ /**
+ * Responds to a change in the `for` value and gets the updated `target`
+ * element for the hovercard.
+ *
+ * @private
+ */
+ _forChanged() {
+ this._target = this.target;
+ }
+}
+
+customElements.define(GrHovercard.is, GrHovercard);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
index c666227..2969bdb 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
@@ -1,27 +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">
-<script src="../../../scripts/rootElement.js"></script>
-
-<dom-module id="gr-hovercard">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
box-sizing: border-box;
@@ -44,6 +39,4 @@
<div id="hovercard" role="tooltip" tabindex="-1">
<slot></slot>
</div>
- </template>
- <script src="gr-hovercard.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
index e091fcf..ed087ca 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -19,12 +19,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-hovercard</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>
<!-- Can't use absolute path below for mock-interaction.js.
Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
actually /node_modules directory). Also, wct patches some files to load modules from /components.
@@ -33,9 +33,14 @@
-->
<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-<link rel="import" href="gr-hovercard.html">
+<script type="module" src="./gr-hovercard.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-hovercard.js';
+void(0);
+</script>
<button id="foo">Hello</button>
<test-fixture id="basic">
@@ -44,87 +49,90 @@
</template>
</test-fixture>
-<script>
- suite('gr-hovercard tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- // For css animations
- const TRANSITION_TIME = 500;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-hovercard.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-hovercard tests', () => {
+ let element;
+ let sandbox;
+ // For css animations
+ const TRANSITION_TIME = 500;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('updatePosition', () => {
- // Test that the correct style properties have at least been set.
- element.position = 'bottom';
- element.updatePosition();
- assert.typeOf(element.style.getPropertyValue('left'), 'string');
- assert.typeOf(element.style.getPropertyValue('top'), 'string');
- assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
- assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
-
- const parentRect = document.documentElement.getBoundingClientRect();
- const targetRect = element._target.getBoundingClientRect();
- const thisRect = element.getBoundingClientRect();
-
- const targetLeft = targetRect.left - parentRect.left;
- const targetTop = targetRect.top - parentRect.top;
-
- const pixelCompare = pixel =>
- Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10);
-
- assert.equal(
- pixelCompare(element.style.left),
- pixelCompare(
- (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px'));
- assert.equal(
- pixelCompare(element.style.top),
- pixelCompare(
- (targetTop + targetRect.height + element.offset) + 'px'));
- });
-
- test('hide', done => {
- element.hide({});
- setTimeout(() => {
- const style = getComputedStyle(element);
- assert.isFalse(element._isShowing);
- assert.isFalse(element.classList.contains('hovered'));
- assert.equal(style.opacity, '0');
- assert.equal(style.visibility, 'hidden');
- assert.notEqual(element.container, Polymer.dom(element).parentNode);
- done();
- }, TRANSITION_TIME);
- });
-
- test('show', done => {
- element.show({});
- setTimeout(() => {
- const style = getComputedStyle(element);
- assert.isTrue(element._isShowing);
- assert.isTrue(element.classList.contains('hovered'));
- assert.equal(style.opacity, '1');
- assert.equal(style.visibility, 'visible');
- done();
- }, TRANSITION_TIME);
- });
-
- test('card shows on enter and hides on leave', done => {
- const button = Polymer.dom(document).querySelector('button');
- assert.isFalse(element._isShowing);
- button.addEventListener('mouseenter', event => {
- assert.isTrue(element._isShowing);
- button.dispatchEvent(new CustomEvent('mouseleave'));
- });
- button.addEventListener('mouseleave', event => {
- assert.isFalse(element._isShowing);
- done();
- });
- button.dispatchEvent(new CustomEvent('mouseenter'));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('updatePosition', () => {
+ // Test that the correct style properties have at least been set.
+ element.position = 'bottom';
+ element.updatePosition();
+ assert.typeOf(element.style.getPropertyValue('left'), 'string');
+ assert.typeOf(element.style.getPropertyValue('top'), 'string');
+ assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string');
+ assert.typeOf(element.style.getPropertyValue('marginTop'), 'string');
+
+ const parentRect = document.documentElement.getBoundingClientRect();
+ const targetRect = element._target.getBoundingClientRect();
+ const thisRect = element.getBoundingClientRect();
+
+ const targetLeft = targetRect.left - parentRect.left;
+ const targetTop = targetRect.top - parentRect.top;
+
+ const pixelCompare = pixel =>
+ Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10);
+
+ assert.equal(
+ pixelCompare(element.style.left),
+ pixelCompare(
+ (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px'));
+ assert.equal(
+ pixelCompare(element.style.top),
+ pixelCompare(
+ (targetTop + targetRect.height + element.offset) + 'px'));
+ });
+
+ test('hide', done => {
+ element.hide({});
+ setTimeout(() => {
+ const style = getComputedStyle(element);
+ assert.isFalse(element._isShowing);
+ assert.isFalse(element.classList.contains('hovered'));
+ assert.equal(style.opacity, '0');
+ assert.equal(style.visibility, 'hidden');
+ assert.notEqual(element.container, dom(element).parentNode);
+ done();
+ }, TRANSITION_TIME);
+ });
+
+ test('show', done => {
+ element.show({});
+ setTimeout(() => {
+ const style = getComputedStyle(element);
+ assert.isTrue(element._isShowing);
+ assert.isTrue(element.classList.contains('hovered'));
+ assert.equal(style.opacity, '1');
+ assert.equal(style.visibility, 'visible');
+ done();
+ }, TRANSITION_TIME);
+ });
+
+ test('card shows on enter and hides on leave', done => {
+ const button = dom(document).querySelector('button');
+ assert.isFalse(element._isShowing);
+ button.addEventListener('mouseenter', event => {
+ assert.isTrue(element._isShowing);
+ button.dispatchEvent(new CustomEvent('mouseleave'));
+ });
+ button.addEventListener('mouseleave', event => {
+ assert.isFalse(element._isShowing);
+ done();
+ });
+ button.dispatchEvent(new CustomEvent('mouseenter'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index 2245b98..5d7da6c 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -1,75 +1,76 @@
-<!--
-@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.
+ */
+import '@polymer/iron-icon/iron-icon.js';
+import '@polymer/iron-iconset-svg/iron-iconset-svg.js';
+const $_documentContainer = document.createElement('template');
-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/iron-icon/iron-icon.html">
-<link rel="import" href="/bower_components/iron-iconset-svg/iron-iconset-svg.html">
-
-<iron-iconset-svg name="gr-icons" size="24">
+$_documentContainer.innerHTML = `<iron-iconset-svg name="gr-icons" size="24">
<svg>
<defs>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></g>
+ <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></g>
+ <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#unfold_more -->
- <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"/></g>
+ <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"></path><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></g>
+ <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></g>
+ <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></g>
+ <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></g>
+ <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></g>
+ <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></g>
+ <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></g>
+ <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></g>
+ <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
+ <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g>
+ <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
- <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"/></g>
+ <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html -->
- <g id="delete"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g>
+ <g id="delete"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="help"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="info"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full-->
- <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"/><path d="M0 0h24v24H0V0z" fill="none"/></g>
+ <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#mode_comment-->
- <g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/><path d="M0 0h24v24H0z" fill="none"/></g>
+ <g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="error"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="lightbulb-outline"><path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"/></g>
+ <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"/></g>
+ <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
- <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></g>
+ <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g>
+ <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="robot"><path d="M4.137453,5.61015591 L4.54835569,1.5340419 C4.5717665,1.30180904 4.76724872,1.12504213 5.00065859,1.12504213 C5.23327176,1.12504213 5.42730868,1.30282046 5.44761309,1.53454578 L5.76084628,5.10933916 C6.16304484,5.03749412 6.57714381,5 7,5 L17,5 C20.8659932,5 24,8.13400675 24,12 L24,15.1250421 C24,18.9910354 20.8659932,22.1250421 17,22.1250421 L7,22.1250421 C3.13400675,22.1250421 2.19029351e-15,18.9910354 0,15.1250421 L0,12 C-3.48556243e-16,9.15382228 1.69864167,6.70438358 4.137453,5.61015591 Z M5.77553049,6.12504213 C3.04904264,6.69038358 1,9.10590202 1,12 L1,15.1250421 C1,18.4387506 3.6862915,21.1250421 7,21.1250421 L17,21.1250421 C20.3137085,21.1250421 23,18.4387506 23,15.1250421 L23,12 C23,8.6862915 20.3137085,6 17,6 L7,6 C6.60617231,6 6.2212068,6.03794347 5.84855971,6.11037415 L5.84984496,6.12504213 L5.77553049,6.12504213 Z M6.93003717,6.95027711 L17.1232083,6.95027711 C19.8638332,6.95027711 22.0855486,9.17199258 22.0855486,11.9126175 C22.0855486,14.6532424 19.8638332,16.8749579 17.1232083,16.8749579 L6.93003717,16.8749579 C4.18941226,16.8749579 1.9676968,14.6532424 1.9676968,11.9126175 C1.9676968,9.17199258 4.18941226,6.95027711 6.93003717,6.95027711 Z M7.60124392,14.0779303 C9.03787127,14.0779303 10.2024878,12.9691885 10.2024878,11.6014862 C10.2024878,10.2337839 9.03787127,9.12504213 7.60124392,9.12504213 C6.16461657,9.12504213 5,10.2337839 5,11.6014862 C5,12.9691885 6.16461657,14.0779303 7.60124392,14.0779303 Z M16.617997,14.1098288 C18.0638768,14.1098288 19.2359939,12.9939463 19.2359939,11.6174355 C19.2359939,10.2409246 18.0638768,9.12504213 16.617997,9.12504213 C15.1721172,9.12504213 14,10.2409246 14,11.6174355 C14,12.9939463 15.1721172,14.1098288 16.617997,14.1098288 Z M9.79751216,18.1250421 L15,18.1250421 L15,19.1250421 C15,19.6773269 14.5522847,20.1250421 14,20.1250421 L10.7975122,20.1250421 C10.2452274,20.1250421 9.79751216,19.6773269 9.79751216,19.1250421 L9.79751216,18.1250421 Z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
@@ -90,9 +91,54 @@
<!-- This is a custom PolyGerrit SVG -->
<g id="submit"><path d="M22.23,5 L11.65,15.58 L7.47000001,11.41 L6.06000001,12.82 L11.65,18.41 L23.649,6.41 L22.23,5 Z M16.58,5 L10.239,11.34 L11.65,12.75 L17.989,6.41 L16.58,5 Z M0.400000006,12.82 L5.99000001,18.41 L7.40000001,17 L1.82000001,11.41 L0.400000006,12.82 Z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g>
+ <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
- <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"/></g>
+ <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
</defs>
</svg>
-</iron-iconset-svg>
+</iron-iconset-svg>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from material.io https://material.io/icons/#unfold_more */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full*/
+/* This SVG is a copy from material.io https://material.io/icons/#mode_comment*/
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/* This is a custom PolyGerrit SVG */
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
index ea5740f..c044327 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-annotation-actions-context</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="../../diff/gr-diff-highlight/gr-annotation.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="../../diff/gr-diff-highlight/gr-annotation.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-js-api-interface.html"/>
+<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-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../diff/gr-diff-highlight/gr-annotation.js';
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,68 +43,71 @@
</template>
</test-fixture>
-<script>
- suite('gr-annotation-actions-context tests', async () => {
- await readyToTest();
- let instance;
- let sandbox;
- let el;
- let lineNumberEl;
- let plugin;
+<script type="module">
+import '../../diff/gr-diff-highlight/gr-annotation.js';
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-annotation-actions-context tests', () => {
+ let instance;
+ let sandbox;
+ let el;
+ let lineNumberEl;
+ let plugin;
- setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- el = document.createElement('div');
- el.textContent = str;
- el.setAttribute('data-side', 'right');
- lineNumberEl = document.createElement('td');
- lineNumberEl.classList.add('right');
- document.body.appendChild(el);
- instance = new GrAnnotationActionsContext(
- el, lineNumberEl, line, 'dummy/path', '123', '1');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('test annotateRange', () => {
- const annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
- const start = 0;
- const end = 100;
- const cssStyleObject = plugin.styles().css('background-color: #000000');
-
- // Assert annotateElement is not called when side is different.
- instance.annotateRange(start, end, cssStyleObject, 'left');
- assert.equal(annotateElementSpy.callCount, 0);
-
- // Assert annotateElement is called once when side is the same.
- instance.annotateRange(start, end, cssStyleObject, 'right');
- assert.equal(annotateElementSpy.callCount, 1);
- const args = annotateElementSpy.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], start);
- assert.equal(args[2], end);
- assert.equal(args[3], cssStyleObject.getClassName(el));
- });
-
- test('test annotateLineNumber', () => {
- const cssStyleObject = plugin.styles().css('background-color: #000000');
-
- const className = cssStyleObject.getClassName(lineNumberEl);
-
- // Assert that css class is *not* applied when side is different.
- instance.annotateLineNumber(cssStyleObject, 'left');
- assert.isFalse(lineNumberEl.classList.contains(className));
-
- // Assert that css class is applied when side is the same.
- instance.annotateLineNumber(cssStyleObject, 'right');
- assert.isTrue(lineNumberEl.classList.contains(className));
- });
+ const str = 'lorem ipsum blah blah';
+ const line = {text: str};
+ el = document.createElement('div');
+ el.textContent = str;
+ el.setAttribute('data-side', 'right');
+ lineNumberEl = document.createElement('td');
+ lineNumberEl.classList.add('right');
+ document.body.appendChild(el);
+ instance = new GrAnnotationActionsContext(
+ el, lineNumberEl, line, 'dummy/path', '123', '1');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('test annotateRange', () => {
+ const annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
+ const start = 0;
+ const end = 100;
+ const cssStyleObject = plugin.styles().css('background-color: #000000');
+
+ // Assert annotateElement is not called when side is different.
+ instance.annotateRange(start, end, cssStyleObject, 'left');
+ assert.equal(annotateElementSpy.callCount, 0);
+
+ // Assert annotateElement is called once when side is the same.
+ instance.annotateRange(start, end, cssStyleObject, 'right');
+ assert.equal(annotateElementSpy.callCount, 1);
+ const args = annotateElementSpy.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], start);
+ assert.equal(args[2], end);
+ assert.equal(args[3], cssStyleObject.getClassName(el));
+ });
+
+ test('test annotateLineNumber', () => {
+ const cssStyleObject = plugin.styles().css('background-color: #000000');
+
+ const className = cssStyleObject.getClassName(lineNumberEl);
+
+ // Assert that css class is *not* applied when side is different.
+ instance.annotateLineNumber(cssStyleObject, 'left');
+ assert.isFalse(lineNumberEl.classList.contains(className));
+
+ // Assert that css class is applied when side is the same.
+ instance.annotateLineNumber(cssStyleObject, 'right');
+ assert.isTrue(lineNumberEl.classList.contains(className));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
index b8c4f83..061f22c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -19,13 +19,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-annotation-actions-js-api-js-api</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="../../change/gr-change-actions/gr-change-actions.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="../../change/gr-change-actions/gr-change-actions.js"></script>
<test-fixture id="basic">
<template>
@@ -38,153 +38,155 @@
</template>
</test-fixture>
-<script>
- suite('gr-annotation-actions-js-api tests', async () => {
- await readyToTest();
- let annotationActions;
- let sandbox;
- let plugin;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../change/gr-change-actions/gr-change-actions.js';
+suite('gr-annotation-actions-js-api tests', () => {
+ let annotationActions;
+ let sandbox;
+ let plugin;
- setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- annotationActions = plugin.annotationApi();
- });
-
- teardown(() => {
- annotationActions = null;
- sandbox.restore();
- });
-
- test('add/get layer', () => {
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const changeNum = 1234;
- const patchNum = 2;
- let testLayerFuncCalled = false;
-
- const testLayerFunc = context => {
- testLayerFuncCalled = true;
- assert.equal(context.line, line);
- assert.equal(context.changeNum, changeNum);
- assert.equal(context.patchNum, 2);
- };
- annotationActions.addLayer(testLayerFunc);
-
- const annotationLayer = annotationActions.getLayer(
- '/dummy/path', changeNum, patchNum);
-
- const lineNumberEl = document.createElement('td');
- annotationLayer.annotate(el, lineNumberEl, line);
- assert.isTrue(testLayerFuncCalled);
- });
-
- test('add notifier', () => {
- const path1 = '/dummy/path1';
- const path2 = '/dummy/path2';
- const annotationLayer1 = annotationActions.getLayer(path1, 1, 2);
- const annotationLayer2 = annotationActions.getLayer(path2, 1, 2);
- const layer1Spy = sandbox.spy(annotationLayer1, 'notifyListeners');
- const layer2Spy = sandbox.spy(annotationLayer2, 'notifyListeners');
-
- let notify;
- let notifyFuncCalled;
- const notifyFunc = n => {
- notifyFuncCalled = true;
- notify = n;
- };
- annotationActions.addNotifier(notifyFunc);
- assert.isTrue(notifyFuncCalled);
-
- // Assert that no layers are invoked with a different path.
- notify('/dummy/path3', 0, 10, 'right');
- assert.isFalse(layer1Spy.called);
- assert.isFalse(layer2Spy.called);
-
- // Assert that only the 1st layer is invoked with path1.
- notify(path1, 0, 10, 'right');
- assert.isTrue(layer1Spy.called);
- assert.isFalse(layer2Spy.called);
-
- // Reset spies.
- layer1Spy.reset();
- layer2Spy.reset();
-
- // Assert that only the 2nd layer is invoked with path2.
- notify(path2, 0, 20, 'left');
- assert.isFalse(layer1Spy.called);
- assert.isTrue(layer2Spy.called);
- });
-
- test('toggle checkbox', () => {
- const fakeEl = {content: fixture('basic')};
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
-
- let checkbox;
- let onAttachedFuncCalled = false;
- const onAttachedFunc = c => {
- checkbox = c;
- onAttachedFuncCalled = true;
- };
- annotationActions.enableToggleCheckbox('test label', onAttachedFunc);
- const emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
- emulateAttached();
-
- // Assert that onAttachedFunc is called and HTML elements have the
- // expected state.
- assert.isTrue(onAttachedFuncCalled);
- assert.equal(checkbox.id, 'annotation-checkbox');
- assert.isTrue(checkbox.disabled);
- assert.equal(document.getElementById('annotation-label').textContent,
- 'test label');
- assert.isFalse(document.getElementById('annotation-span').hidden);
-
- // Assert that error is shown if we try to enable checkbox again.
- onAttachedFuncCalled = false;
- annotationActions.enableToggleCheckbox('test label2', onAttachedFunc);
- const errorStub = sandbox.stub(
- console, 'error', (msg, err) => undefined);
- emulateAttached();
- assert.isTrue(
- errorStub.calledWith(
- 'annotation-span is already enabled. Cannot re-enable.'));
- // Assert that onAttachedFunc is not called and the label has not changed.
- assert.isFalse(onAttachedFuncCalled);
- assert.equal(document.getElementById('annotation-label').textContent,
- 'test label');
- });
-
- test('layer notify listeners', () => {
- const annotationLayer = annotationActions.getLayer(
- '/dummy/path', 1, 2);
- let listenerCalledTimes = 0;
- const startRange = 10;
- const endRange = 20;
- const side = 'right';
- const listener = (st, end, s) => {
- listenerCalledTimes++;
- assert.equal(st, startRange);
- assert.equal(end, endRange);
- assert.equal(s, side);
- };
-
- // Notify with 0 listeners added.
- annotationLayer.notifyListeners(startRange, endRange, side);
- assert.equal(listenerCalledTimes, 0);
-
- // Add 1 listener.
- annotationLayer.addListener(listener);
- annotationLayer.notifyListeners(startRange, endRange, side);
- assert.equal(listenerCalledTimes, 1);
-
- // Add 1 more listener. Total 2 listeners.
- annotationLayer.addListener(listener);
- annotationLayer.notifyListeners(startRange, endRange, side);
- assert.equal(listenerCalledTimes, 3);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ annotationActions = plugin.annotationApi();
});
+
+ teardown(() => {
+ annotationActions = null;
+ sandbox.restore();
+ });
+
+ test('add/get layer', () => {
+ const str = 'lorem ipsum blah blah';
+ const line = {text: str};
+ const el = document.createElement('div');
+ el.textContent = str;
+ const changeNum = 1234;
+ const patchNum = 2;
+ let testLayerFuncCalled = false;
+
+ const testLayerFunc = context => {
+ testLayerFuncCalled = true;
+ assert.equal(context.line, line);
+ assert.equal(context.changeNum, changeNum);
+ assert.equal(context.patchNum, 2);
+ };
+ annotationActions.addLayer(testLayerFunc);
+
+ const annotationLayer = annotationActions.getLayer(
+ '/dummy/path', changeNum, patchNum);
+
+ const lineNumberEl = document.createElement('td');
+ annotationLayer.annotate(el, lineNumberEl, line);
+ assert.isTrue(testLayerFuncCalled);
+ });
+
+ test('add notifier', () => {
+ const path1 = '/dummy/path1';
+ const path2 = '/dummy/path2';
+ const annotationLayer1 = annotationActions.getLayer(path1, 1, 2);
+ const annotationLayer2 = annotationActions.getLayer(path2, 1, 2);
+ const layer1Spy = sandbox.spy(annotationLayer1, 'notifyListeners');
+ const layer2Spy = sandbox.spy(annotationLayer2, 'notifyListeners');
+
+ let notify;
+ let notifyFuncCalled;
+ const notifyFunc = n => {
+ notifyFuncCalled = true;
+ notify = n;
+ };
+ annotationActions.addNotifier(notifyFunc);
+ assert.isTrue(notifyFuncCalled);
+
+ // Assert that no layers are invoked with a different path.
+ notify('/dummy/path3', 0, 10, 'right');
+ assert.isFalse(layer1Spy.called);
+ assert.isFalse(layer2Spy.called);
+
+ // Assert that only the 1st layer is invoked with path1.
+ notify(path1, 0, 10, 'right');
+ assert.isTrue(layer1Spy.called);
+ assert.isFalse(layer2Spy.called);
+
+ // Reset spies.
+ layer1Spy.reset();
+ layer2Spy.reset();
+
+ // Assert that only the 2nd layer is invoked with path2.
+ notify(path2, 0, 20, 'left');
+ assert.isFalse(layer1Spy.called);
+ assert.isTrue(layer2Spy.called);
+ });
+
+ test('toggle checkbox', () => {
+ const fakeEl = {content: fixture('basic')};
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+
+ let checkbox;
+ let onAttachedFuncCalled = false;
+ const onAttachedFunc = c => {
+ checkbox = c;
+ onAttachedFuncCalled = true;
+ };
+ annotationActions.enableToggleCheckbox('test label', onAttachedFunc);
+ const emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
+ emulateAttached();
+
+ // Assert that onAttachedFunc is called and HTML elements have the
+ // expected state.
+ assert.isTrue(onAttachedFuncCalled);
+ assert.equal(checkbox.id, 'annotation-checkbox');
+ assert.isTrue(checkbox.disabled);
+ assert.equal(document.getElementById('annotation-label').textContent,
+ 'test label');
+ assert.isFalse(document.getElementById('annotation-span').hidden);
+
+ // Assert that error is shown if we try to enable checkbox again.
+ onAttachedFuncCalled = false;
+ annotationActions.enableToggleCheckbox('test label2', onAttachedFunc);
+ const errorStub = sandbox.stub(
+ console, 'error', (msg, err) => undefined);
+ emulateAttached();
+ assert.isTrue(
+ errorStub.calledWith(
+ 'annotation-span is already enabled. Cannot re-enable.'));
+ // Assert that onAttachedFunc is not called and the label has not changed.
+ assert.isFalse(onAttachedFuncCalled);
+ assert.equal(document.getElementById('annotation-label').textContent,
+ 'test label');
+ });
+
+ test('layer notify listeners', () => {
+ const annotationLayer = annotationActions.getLayer(
+ '/dummy/path', 1, 2);
+ let listenerCalledTimes = 0;
+ const startRange = 10;
+ const endRange = 20;
+ const side = 'right';
+ const listener = (st, end, s) => {
+ listenerCalledTimes++;
+ assert.equal(st, startRange);
+ assert.equal(end, endRange);
+ assert.equal(s, side);
+ };
+
+ // Notify with 0 listeners added.
+ annotationLayer.notifyListeners(startRange, endRange, side);
+ assert.equal(listenerCalledTimes, 0);
+
+ // Add 1 listener.
+ annotationLayer.addListener(listener);
+ annotationLayer.notifyListeners(startRange, endRange, side);
+ assert.equal(listenerCalledTimes, 1);
+
+ // Add 1 more listener. Total 2 listeners.
+ annotationLayer.addListener(listener);
+ annotationLayer.notifyListeners(startRange, endRange, side);
+ assert.equal(listenerCalledTimes, 3);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
index d70a8d2..154d287 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
@@ -19,70 +19,77 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</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-js-api-interface.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-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+void(0);
+</script>
-<script>
- const PRELOADED_PROTOCOL = 'preloaded:';
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+const PRELOADED_PROTOCOL = 'preloaded:';
- suite('gr-api-utils tests', async () => {
- await readyToTest();
- suite('test getPluginNameFromUrl', () => {
- const {getPluginNameFromUrl} = window._apiUtils;
+suite('gr-api-utils tests', () => {
+ suite('test getPluginNameFromUrl', () => {
+ const {getPluginNameFromUrl} = window._apiUtils;
- test('with empty string', () => {
- assert.equal(getPluginNameFromUrl(''), null);
- });
+ test('with empty string', () => {
+ assert.equal(getPluginNameFromUrl(''), null);
+ });
- test('with invalid url', () => {
- assert.equal(getPluginNameFromUrl('test'), null);
- });
+ test('with invalid url', () => {
+ assert.equal(getPluginNameFromUrl('test'), null);
+ });
- test('with random invalid url', () => {
- assert.equal(getPluginNameFromUrl('http://example.com'), null);
- assert.equal(
- getPluginNameFromUrl('http://example.com/static/a.html'),
- null
- );
- });
+ test('with random invalid url', () => {
+ assert.equal(getPluginNameFromUrl('http://example.com'), null);
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/static/a.html'),
+ null
+ );
+ });
- test('with valid urls', () => {
- assert.equal(
- getPluginNameFromUrl('http://example.com/plugins/a.html'),
- 'a'
- );
- assert.equal(
- getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'),
- 'a'
- );
- });
+ test('with valid urls', () => {
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/plugins/a.html'),
+ 'a'
+ );
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'),
+ 'a'
+ );
+ });
- test('with preloaded urls', () => {
- assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a');
- });
+ test('with preloaded urls', () => {
+ assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a');
+ });
- test('with gerrit-theme override', () => {
- assert.equal(
- getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'),
- 'gerrit-theme'
- );
- });
+ test('with gerrit-theme override', () => {
+ assert.equal(
+ getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'),
+ 'gerrit-theme'
+ );
+ });
- test('with ASSETS_PATH', () => {
- window.ASSETS_PATH = 'http://cdn.com/2';
- assert.equal(
- getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
- 'a'
- );
- window.ASSETS_PATH = undefined;
- });
+ test('with ASSETS_PATH', () => {
+ window.ASSETS_PATH = 'http://cdn.com/2';
+ assert.equal(
+ getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
+ 'a'
+ );
+ window.ASSETS_PATH = undefined;
});
});
+});
</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 91e1a49..1425f71 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -19,19 +19,24 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions-js-api</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>
<!--
This must refer to the element this interface is wrapping around. Otherwise
breaking changes to gr-change-actions won’t be noticed.
-->
-<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
+<script type="module" src="../../change/gr-change-actions/gr-change-actions.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../change/gr-change-actions/gr-change-actions.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -39,191 +44,194 @@
</template>
</test-fixture>
-<script>
- suite('gr-js-api-interface tests', async () => {
- await readyToTest();
- let element;
- let changeActions;
- let plugin;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../change/gr-change-actions/gr-change-actions.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-js-api-interface tests', () => {
+ let element;
+ let changeActions;
+ let plugin;
- // Because deepEqual doesn’t behave in Safari.
- function assertArraysEqual(actual, expected) {
- assert.equal(actual.length, expected.length);
- for (let i = 0; i < actual.length; i++) {
- assert.equal(actual[i], expected[i]);
- }
+ // Because deepEqual doesn’t behave in Safari.
+ function assertArraysEqual(actual, expected) {
+ assert.equal(actual.length, expected.length);
+ for (let i = 0; i < actual.length; i++) {
+ assert.equal(actual[i], expected[i]);
}
+ }
- suite('early init', () => {
- setup(() => {
- Gerrit._testOnly_resetPlugins();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- // Mimic all plugins loaded.
- Gerrit._loadPlugins([]);
- changeActions = plugin.changeActions();
- element = fixture('basic');
+ suite('early init', () => {
+ setup(() => {
+ Gerrit._testOnly_resetPlugins();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ // Mimic all plugins loaded.
+ Gerrit._loadPlugins([]);
+ changeActions = plugin.changeActions();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ changeActions = null;
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ test('does not throw', ()=> {
+ assert.doesNotThrow(() => {
+ changeActions.add('change', 'foo');
});
+ });
+ });
- teardown(() => {
- changeActions = null;
- Gerrit._testOnly_resetPlugins();
- });
+ suite('normal init', () => {
+ setup(() => {
+ Gerrit._testOnly_resetPlugins();
+ element = fixture('basic');
+ sinon.stub(element, '_editStatusChanged');
+ element.change = {};
+ element._hasKnownChainState = false;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeActions = plugin.changeActions();
+ // Mimic all plugins loaded.
+ Gerrit._loadPlugins([]);
+ });
- test('does not throw', ()=> {
- assert.doesNotThrow(() => {
- changeActions.add('change', 'foo');
+ teardown(() => {
+ changeActions = null;
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ test('property existence', () => {
+ const properties = [
+ 'ActionType',
+ 'ChangeActions',
+ 'RevisionActions',
+ ];
+ for (const p of properties) {
+ assertArraysEqual(changeActions[p], element[p]);
+ }
+ });
+
+ test('add/remove primary action keys', () => {
+ element.primaryActionKeys = [];
+ changeActions.addPrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['foo']);
+ changeActions.addPrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['foo']);
+ changeActions.addPrimaryActionKey('bar');
+ assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
+ changeActions.removePrimaryActionKey('foo');
+ assertArraysEqual(element.primaryActionKeys, ['bar']);
+ changeActions.removePrimaryActionKey('baz');
+ assertArraysEqual(element.primaryActionKeys, ['bar']);
+ changeActions.removePrimaryActionKey('bar');
+ assertArraysEqual(element.primaryActionKeys, []);
+ });
+
+ test('action buttons', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ const handler = sinon.spy();
+ changeActions.addTapListener(key, handler);
+ flush(() => {
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ assert(handler.calledOnce);
+ changeActions.removeTapListener(key, handler);
+ MockInteractions.tap(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ assert(handler.calledOnce);
+ changeActions.remove(key);
+ flush(() => {
+ assert.isNull(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ done();
});
});
});
- suite('normal init', () => {
- setup(() => {
- Gerrit._testOnly_resetPlugins();
- element = fixture('basic');
- sinon.stub(element, '_editStatusChanged');
- element.change = {};
- element._hasKnownChainState = false;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeActions = plugin.changeActions();
- // Mimic all plugins loaded.
- Gerrit._loadPlugins([]);
- });
-
- teardown(() => {
- changeActions = null;
- Gerrit._testOnly_resetPlugins();
- });
-
- test('property existence', () => {
- const properties = [
- 'ActionType',
- 'ChangeActions',
- 'RevisionActions',
- ];
- for (const p of properties) {
- assertArraysEqual(changeActions[p], element[p]);
- }
- });
-
- test('add/remove primary action keys', () => {
- element.primaryActionKeys = [];
- changeActions.addPrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['foo']);
- changeActions.addPrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['foo']);
- changeActions.addPrimaryActionKey('bar');
- assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
- changeActions.removePrimaryActionKey('foo');
- assertArraysEqual(element.primaryActionKeys, ['bar']);
- changeActions.removePrimaryActionKey('baz');
- assertArraysEqual(element.primaryActionKeys, ['bar']);
- changeActions.removePrimaryActionKey('bar');
- assertArraysEqual(element.primaryActionKeys, []);
- });
-
- test('action buttons', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- const handler = sinon.spy();
- changeActions.addTapListener(key, handler);
+ test('action button properties', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(() => {
+ const button = element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]');
+ assert.isOk(button);
+ assert.equal(button.getAttribute('data-label'), 'Bork!');
+ assert.isNotOk(button.disabled);
+ changeActions.setLabel(key, 'Yo');
+ changeActions.setTitle(key, 'Yo hint');
+ changeActions.setEnabled(key, false);
+ changeActions.setIcon(key, 'pupper');
flush(() => {
- MockInteractions.tap(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- assert(handler.calledOnce);
- changeActions.removeTapListener(key, handler);
- MockInteractions.tap(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- assert(handler.calledOnce);
- changeActions.remove(key);
- flush(() => {
- assert.isNull(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- done();
- });
+ assert.equal(button.getAttribute('data-label'), 'Yo');
+ assert.equal(button.getAttribute('title'), 'Yo hint');
+ assert.isTrue(button.disabled);
+ assert.equal(dom(button).querySelector('iron-icon').icon,
+ 'gr-icons:pupper');
+ done();
});
});
+ });
- test('action button properties', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ test('hide action buttons', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(() => {
+ const button = element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]');
+ assert.isOk(button);
+ assert.isFalse(button.hasAttribute('hidden'));
+ changeActions.setActionHidden(
+ changeActions.ActionType.REVISION, key, true);
flush(() => {
const button = element.shadowRoot
.querySelector('[data-action-key="' + key + '"]');
- assert.isOk(button);
- assert.equal(button.getAttribute('data-label'), 'Bork!');
- assert.isNotOk(button.disabled);
- changeActions.setLabel(key, 'Yo');
- changeActions.setTitle(key, 'Yo hint');
- changeActions.setEnabled(key, false);
- changeActions.setIcon(key, 'pupper');
- flush(() => {
- assert.equal(button.getAttribute('data-label'), 'Yo');
- assert.equal(button.getAttribute('title'), 'Yo hint');
- assert.isTrue(button.disabled);
- assert.equal(Polymer.dom(button).querySelector('iron-icon').icon,
- 'gr-icons:pupper');
- done();
- });
+ assert.isNotOk(button);
+ done();
});
});
+ });
- test('hide action buttons', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ test('move action button to overflow', done => {
+ const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(() => {
+ assert.isTrue(element.$.moreActions.hidden);
+ assert.isOk(element.shadowRoot
+ .querySelector('[data-action-key="' + key + '"]'));
+ changeActions.setActionOverflow(
+ changeActions.ActionType.REVISION, key, true);
flush(() => {
- const button = element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]');
- assert.isOk(button);
- assert.isFalse(button.hasAttribute('hidden'));
- changeActions.setActionHidden(
- changeActions.ActionType.REVISION, key, true);
- flush(() => {
- const button = element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]');
- assert.isNotOk(button);
- done();
- });
- });
- });
-
- test('move action button to overflow', done => {
- const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- flush(() => {
- assert.isTrue(element.$.moreActions.hidden);
- assert.isOk(element.shadowRoot
+ assert.isNotOk(element.shadowRoot
.querySelector('[data-action-key="' + key + '"]'));
- changeActions.setActionOverflow(
- changeActions.ActionType.REVISION, key, true);
- flush(() => {
- assert.isNotOk(element.shadowRoot
- .querySelector('[data-action-key="' + key + '"]'));
- assert.isFalse(element.$.moreActions.hidden);
- assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!');
- done();
- });
+ assert.isFalse(element.$.moreActions.hidden);
+ assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!');
+ done();
});
});
+ });
- test('change actions priority', done => {
- const key1 =
- changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
- const key2 =
- changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?');
+ test('change actions priority', done => {
+ const key1 =
+ changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ const key2 =
+ changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?');
+ flush(() => {
+ let buttons =
+ dom(element.root).querySelectorAll('[data-action-key]');
+ assert.equal(buttons[0].getAttribute('data-action-key'), key1);
+ assert.equal(buttons[1].getAttribute('data-action-key'), key2);
+ changeActions.setActionPriority(
+ changeActions.ActionType.REVISION, key1, 10);
flush(() => {
- let buttons =
- Polymer.dom(element.root).querySelectorAll('[data-action-key]');
- assert.equal(buttons[0].getAttribute('data-action-key'), key1);
- assert.equal(buttons[1].getAttribute('data-action-key'), key2);
- changeActions.setActionPriority(
- changeActions.ActionType.REVISION, key1, 10);
- flush(() => {
- buttons =
- Polymer.dom(element.root).querySelectorAll('[data-action-key]');
- assert.equal(buttons[0].getAttribute('data-action-key'), key2);
- assert.equal(buttons[1].getAttribute('data-action-key'), key1);
- done();
- });
+ buttons =
+ dom(element.root).querySelectorAll('[data-action-key]');
+ assert.equal(buttons[0].getAttribute('data-action-key'), key2);
+ assert.equal(buttons[1].getAttribute('data-action-key'), key1);
+ done();
});
});
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
index 3147746..3a1c51c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -19,19 +19,24 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-reply-js-api</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>
<!--
This must refer to the element this interface is wrapping around. Otherwise
breaking changes to gr-reply-dialog won’t be noticed.
-->
-<link rel="import" href="../../change/gr-reply-dialog/gr-reply-dialog.html">
+<script type="module" src="../../change/gr-reply-dialog/gr-reply-dialog.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../change/gr-reply-dialog/gr-reply-dialog.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -39,86 +44,88 @@
</template>
</test-fixture>
-<script>
- suite('gr-change-reply-js-api tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let changeReply;
- let plugin;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../change/gr-reply-dialog/gr-reply-dialog.js';
+suite('gr-change-reply-js-api tests', () => {
+ let element;
+ let sandbox;
+ let changeReply;
+ let plugin;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getAccount() { return Promise.resolve(null); },
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('early init', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- getAccount() { return Promise.resolve(null); },
- });
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeReply = plugin.changeReply();
+ element = fixture('basic');
});
teardown(() => {
- sandbox.restore();
+ changeReply = null;
});
- suite('early init', () => {
- setup(() => {
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeReply = plugin.changeReply();
- element = fixture('basic');
- });
+ test('works', () => {
+ sandbox.stub(element, 'getLabelValue').returns('+123');
+ assert.equal(changeReply.getLabelValue('My-Label'), '+123');
- teardown(() => {
- changeReply = null;
- });
+ sandbox.stub(element, 'setLabelValue');
+ changeReply.setLabelValue('My-Label', '+1337');
+ assert.isTrue(
+ element.setLabelValue.calledWithExactly('My-Label', '+1337'));
- test('works', () => {
- sandbox.stub(element, 'getLabelValue').returns('+123');
- assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+ sandbox.stub(element, 'send');
+ changeReply.send(false);
+ assert.isTrue(element.send.calledWithExactly(false));
- sandbox.stub(element, 'setLabelValue');
- changeReply.setLabelValue('My-Label', '+1337');
- assert.isTrue(
- element.setLabelValue.calledWithExactly('My-Label', '+1337'));
-
- sandbox.stub(element, 'send');
- changeReply.send(false);
- assert.isTrue(element.send.calledWithExactly(false));
-
- sandbox.stub(element, 'setPluginMessage');
- changeReply.showMessage('foobar');
- assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
- });
- });
-
- suite('normal init', () => {
- setup(() => {
- element = fixture('basic');
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- changeReply = plugin.changeReply();
- });
-
- teardown(() => {
- changeReply = null;
- });
-
- test('works', () => {
- sandbox.stub(element, 'getLabelValue').returns('+123');
- assert.equal(changeReply.getLabelValue('My-Label'), '+123');
-
- sandbox.stub(element, 'setLabelValue');
- changeReply.setLabelValue('My-Label', '+1337');
- assert.isTrue(
- element.setLabelValue.calledWithExactly('My-Label', '+1337'));
-
- sandbox.stub(element, 'send');
- changeReply.send(false);
- assert.isTrue(element.send.calledWithExactly(false));
-
- sandbox.stub(element, 'setPluginMessage');
- changeReply.showMessage('foobar');
- assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
- });
+ sandbox.stub(element, 'setPluginMessage');
+ changeReply.showMessage('foobar');
+ assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
});
});
+
+ suite('normal init', () => {
+ setup(() => {
+ element = fixture('basic');
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ changeReply = plugin.changeReply();
+ });
+
+ teardown(() => {
+ changeReply = null;
+ });
+
+ test('works', () => {
+ sandbox.stub(element, 'getLabelValue').returns('+123');
+ assert.equal(changeReply.getLabelValue('My-Label'), '+123');
+
+ sandbox.stub(element, 'setLabelValue');
+ changeReply.setLabelValue('My-Label', '+1337');
+ assert.isTrue(
+ element.setLabelValue.calledWithExactly('My-Label', '+1337'));
+
+ sandbox.stub(element, 'send');
+ changeReply.send(false);
+ assert.isTrue(element.send.calledWithExactly(false));
+
+ sandbox.stub(element, 'setPluginMessage');
+ changeReply.showMessage('foobar');
+ assert.isTrue(element.setPluginMessage.calledWithExactly('foobar'));
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
index ee95a5e..57f0646 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_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-api-interface</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-js-api-interface.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-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,68 +40,70 @@
</template>
</test-fixture>
-<script>
- suite('gr-gerrit tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let sendStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-gerrit tests', () => {
+ let element;
+ let sandbox;
+ let sendStub;
- setup(() => {
- window.clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve({name: 'Judy Hopps'});
- },
- send(...args) {
- return sendStub(...args);
- },
- });
- element = fixture('basic');
+ setup(() => {
+ window.clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve({name: 'Judy Hopps'});
+ },
+ send(...args) {
+ return sendStub(...args);
+ },
+ });
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ window.clock.restore();
+ sandbox.restore();
+ element._removeEventCallbacks();
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ suite('proxy methods', () => {
+ test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginEnabled',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginEnabled('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
- teardown(() => {
- window.clock.restore();
- sandbox.restore();
- element._removeEventCallbacks();
- Gerrit._testOnly_resetPlugins();
+ test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginLoaded',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginLoaded('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
- suite('proxy methods', () => {
- test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
- const stubFn = sandbox.stub();
- sandbox.stub(
- Gerrit._pluginLoader,
- 'isPluginEnabled',
- (...args) => stubFn(...args)
- );
- Gerrit._isPluginEnabled('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
-
- test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
- const stubFn = sandbox.stub();
- sandbox.stub(
- Gerrit._pluginLoader,
- 'isPluginLoaded',
- (...args) => stubFn(...args)
- );
- Gerrit._isPluginLoaded('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
-
- test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
- const stubFn = sandbox.stub();
- sandbox.stub(
- Gerrit._pluginLoader,
- 'isPluginPreloaded',
- (...args) => stubFn(...args)
- );
- Gerrit._isPluginPreloaded('test_plugin');
- assert.isTrue(stubFn.calledWith('test_plugin'));
- });
+ test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginPreloaded',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginPreloaded('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.html
deleted file mode 100644
index 922fa57..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-@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.
--->
-
-<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/gr-patch-set-behavior/gr-patch-set-behavior.html">
-
-<dom-module id="gr-js-api-interface">
- <script src="gr-js-api-interface-element.js"></script>
-</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
index 393dc77..2523d47 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,306 +14,311 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- // Note: for new events, naming convention should be: `a-b`
- const EventType = {
- HISTORY: 'history',
- LABEL_CHANGE: 'labelchange',
- SHOW_CHANGE: 'showchange',
- SUBMIT_CHANGE: 'submitchange',
- SHOW_REVISION_ACTIONS: 'show-revision-actions',
- COMMIT_MSG_EDIT: 'commitmsgedit',
- COMMENT: 'comment',
- REVERT: 'revert',
- REVERT_SUBMISSION: 'revert_submission',
- POST_REVERT: 'postrevert',
- ANNOTATE_DIFF: 'annotatediff',
- ADMIN_MENU_LINKS: 'admin-menu-links',
- HIGHLIGHTJS_LOADED: 'highlightjs-loaded',
- };
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.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';
- const Element = {
- CHANGE_ACTIONS: 'changeactions',
- REPLY_DIALOG: 'replydialog',
- };
+// Note: for new events, naming convention should be: `a-b`
+const EventType = {
+ HISTORY: 'history',
+ LABEL_CHANGE: 'labelchange',
+ SHOW_CHANGE: 'showchange',
+ SUBMIT_CHANGE: 'submitchange',
+ SHOW_REVISION_ACTIONS: 'show-revision-actions',
+ COMMIT_MSG_EDIT: 'commitmsgedit',
+ COMMENT: 'comment',
+ REVERT: 'revert',
+ REVERT_SUBMISSION: 'revert_submission',
+ POST_REVERT: 'postrevert',
+ ANNOTATE_DIFF: 'annotatediff',
+ ADMIN_MENU_LINKS: 'admin-menu-links',
+ HIGHLIGHTJS_LOADED: 'highlightjs-loaded',
+};
- /**
- * @appliesMixin Gerrit.PatchSetMixin
- * @extends Polymer.Element
- */
- class GrJsApiInterface extends Polymer.mixinBehaviors( [
- Gerrit.PatchSetBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-js-api-interface'; }
+const Element = {
+ CHANGE_ACTIONS: 'changeactions',
+ REPLY_DIALOG: 'replydialog',
+};
- constructor() {
- super();
- this.Element = Element;
- this.EventType = EventType;
- }
+/**
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @extends Polymer.Element
+ */
+class GrJsApiInterface extends mixinBehaviors( [
+ Gerrit.PatchSetBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get is() { return 'gr-js-api-interface'; }
- static get properties() {
- return {
- _elements: {
- type: Object,
- value: {}, // Shared across all instances.
- },
- _eventCallbacks: {
- type: Object,
- value: {}, // Shared across all instances.
- },
- };
- }
+ constructor() {
+ super();
+ this.Element = Element;
+ this.EventType = EventType;
+ }
- handleEvent(type, detail) {
- Gerrit.awaitPluginsLoaded().then(() => {
- switch (type) {
- case EventType.HISTORY:
- this._handleHistory(detail);
- break;
- case EventType.SHOW_CHANGE:
- this._handleShowChange(detail);
- break;
- case EventType.COMMENT:
- this._handleComment(detail);
- break;
- case EventType.LABEL_CHANGE:
- this._handleLabelChange(detail);
- break;
- case EventType.SHOW_REVISION_ACTIONS:
- this._handleShowRevisionActions(detail);
- break;
- case EventType.HIGHLIGHTJS_LOADED:
- this._handleHighlightjsLoaded(detail);
- break;
- default:
- console.warn('handleEvent called with unsupported event type:',
- type);
- break;
- }
- });
- }
+ static get properties() {
+ return {
+ _elements: {
+ type: Object,
+ value: {}, // Shared across all instances.
+ },
+ _eventCallbacks: {
+ type: Object,
+ value: {}, // Shared across all instances.
+ },
+ };
+ }
- addElement(key, el) {
- this._elements[key] = el;
- }
-
- getElement(key) {
- return this._elements[key];
- }
-
- addEventCallback(eventName, callback) {
- if (!this._eventCallbacks[eventName]) {
- this._eventCallbacks[eventName] = [];
- }
- this._eventCallbacks[eventName].push(callback);
- }
-
- canSubmitChange(change, revision) {
- const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
- const cancelSubmit = submitCallbacks.some(callback => {
- try {
- return callback(change, revision) === false;
- } catch (err) {
- console.error(err);
- }
- return false;
- });
-
- return !cancelSubmit;
- }
-
- _removeEventCallbacks() {
- for (const k in EventType) {
- if (!EventType.hasOwnProperty(k)) { continue; }
- this._eventCallbacks[EventType[k]] = [];
- }
- }
-
- _handleHistory(detail) {
- for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
- try {
- cb(detail.path);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleShowChange(detail) {
- // Note (issue 8221) Shallow clone the change object and add a mergeable
- // getter with deprecation warning. This makes the change detail appear as
- // though SKIP_MERGEABLE was not set, so that plugins that expect it can
- // still access.
- //
- // This clone and getter can be removed after plugins migrate to use
- // info.mergeable.
- //
- // assign on getter with existing property will report error
- // see Issue: 12286
- const change = Object.assign({}, detail.change, {
- get mergeable() {
- console.warn('Accessing change.mergeable from SHOW_CHANGE is ' +
- 'deprecated! Use info.mergeable instead.');
- return detail.info && detail.info.mergeable;
- },
- });
- const patchNum = detail.patchNum;
- const info = detail.info;
-
- let revision;
- for (const rev of Object.values(change.revisions || {})) {
- if (this.patchNumEquals(rev._number, patchNum)) {
- revision = rev;
+ handleEvent(type, detail) {
+ Gerrit.awaitPluginsLoaded().then(() => {
+ switch (type) {
+ case EventType.HISTORY:
+ this._handleHistory(detail);
break;
- }
+ case EventType.SHOW_CHANGE:
+ this._handleShowChange(detail);
+ break;
+ case EventType.COMMENT:
+ this._handleComment(detail);
+ break;
+ case EventType.LABEL_CHANGE:
+ this._handleLabelChange(detail);
+ break;
+ case EventType.SHOW_REVISION_ACTIONS:
+ this._handleShowRevisionActions(detail);
+ break;
+ case EventType.HIGHLIGHTJS_LOADED:
+ this._handleHighlightjsLoaded(detail);
+ break;
+ default:
+ console.warn('handleEvent called with unsupported event type:',
+ type);
+ break;
}
+ });
+ }
- for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) {
- try {
- cb(change, revision, info);
- } catch (err) {
- console.error(err);
- }
+ addElement(key, el) {
+ this._elements[key] = el;
+ }
+
+ getElement(key) {
+ return this._elements[key];
+ }
+
+ addEventCallback(eventName, callback) {
+ if (!this._eventCallbacks[eventName]) {
+ this._eventCallbacks[eventName] = [];
+ }
+ this._eventCallbacks[eventName].push(callback);
+ }
+
+ canSubmitChange(change, revision) {
+ const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
+ const cancelSubmit = submitCallbacks.some(callback => {
+ try {
+ return callback(change, revision) === false;
+ } catch (err) {
+ console.error(err);
}
- }
+ return false;
+ });
- /**
- * @param {!{change: !Object, revisionActions: !Object}} detail
- */
- _handleShowRevisionActions(detail) {
- const registeredCallbacks = this._getEventCallbacks(
- EventType.SHOW_REVISION_ACTIONS
- );
- for (const cb of registeredCallbacks) {
- try {
- cb(detail.revisionActions, detail.change);
- } catch (err) {
- console.error(err);
- }
- }
- }
+ return !cancelSubmit;
+ }
- handleCommitMessage(change, msg) {
- for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
- try {
- cb(change, msg);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleComment(detail) {
- for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
- try {
- cb(detail.node);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleLabelChange(detail) {
- for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
- try {
- cb(detail.change);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- _handleHighlightjsLoaded(detail) {
- for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
- try {
- cb(detail.hljs);
- } catch (err) {
- console.error(err);
- }
- }
- }
-
- modifyRevertMsg(change, revertMsg, origMsg) {
- for (const cb of this._getEventCallbacks(EventType.REVERT)) {
- try {
- revertMsg = cb(change, revertMsg, origMsg);
- } catch (err) {
- console.error(err);
- }
- }
- return revertMsg;
- }
-
- modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) {
- for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) {
- try {
- revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg);
- } catch (err) {
- console.error(err);
- }
- }
- return revertSubmissionMsg;
- }
-
- getDiffLayers(path, changeNum, patchNum) {
- const layers = [];
- for (const annotationApi of
- this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
- try {
- const layer = annotationApi.getLayer(path, changeNum, patchNum);
- layers.push(layer);
- } catch (err) {
- console.error(err);
- }
- }
- return layers;
- }
-
- /**
- * Retrieves coverage data possibly provided by a plugin.
- *
- * Will wait for plugins to be loaded. If multiple plugins offer a coverage
- * provider, the first one is returned. If no plugin offers a coverage provider,
- * will resolve to null.
- *
- * @return {!Promise<?GrAnnotationActionsInterface>}
- */
- getCoverageAnnotationApi() {
- return Gerrit.awaitPluginsLoaded()
- .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF)
- .find(api => api.getCoverageProvider()));
- }
-
- getAdminMenuLinks() {
- const links = [];
- for (const adminApi of
- this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) {
- links.push(...adminApi.getMenuLinks());
- }
- return links;
- }
-
- getLabelValuesPostRevert(change) {
- let labels = {};
- for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) {
- try {
- labels = cb(change);
- } catch (err) {
- console.error(err);
- }
- }
- return labels;
- }
-
- _getEventCallbacks(type) {
- return this._eventCallbacks[type] || [];
+ _removeEventCallbacks() {
+ for (const k in EventType) {
+ if (!EventType.hasOwnProperty(k)) { continue; }
+ this._eventCallbacks[EventType[k]] = [];
}
}
- customElements.define(GrJsApiInterface.is, GrJsApiInterface);
-})();
+ _handleHistory(detail) {
+ for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
+ try {
+ cb(detail.path);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleShowChange(detail) {
+ // Note (issue 8221) Shallow clone the change object and add a mergeable
+ // getter with deprecation warning. This makes the change detail appear as
+ // though SKIP_MERGEABLE was not set, so that plugins that expect it can
+ // still access.
+ //
+ // This clone and getter can be removed after plugins migrate to use
+ // info.mergeable.
+ //
+ // assign on getter with existing property will report error
+ // see Issue: 12286
+ const change = Object.assign({}, detail.change, {
+ get mergeable() {
+ console.warn('Accessing change.mergeable from SHOW_CHANGE is ' +
+ 'deprecated! Use info.mergeable instead.');
+ return detail.info && detail.info.mergeable;
+ },
+ });
+ const patchNum = detail.patchNum;
+ const info = detail.info;
+
+ let revision;
+ for (const rev of Object.values(change.revisions || {})) {
+ if (this.patchNumEquals(rev._number, patchNum)) {
+ revision = rev;
+ break;
+ }
+ }
+
+ for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) {
+ try {
+ cb(change, revision, info);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ /**
+ * @param {!{change: !Object, revisionActions: !Object}} detail
+ */
+ _handleShowRevisionActions(detail) {
+ const registeredCallbacks = this._getEventCallbacks(
+ EventType.SHOW_REVISION_ACTIONS
+ );
+ for (const cb of registeredCallbacks) {
+ try {
+ cb(detail.revisionActions, detail.change);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ handleCommitMessage(change, msg) {
+ for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
+ try {
+ cb(change, msg);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleComment(detail) {
+ for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
+ try {
+ cb(detail.node);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleLabelChange(detail) {
+ for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
+ try {
+ cb(detail.change);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ _handleHighlightjsLoaded(detail) {
+ for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
+ try {
+ cb(detail.hljs);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ modifyRevertMsg(change, revertMsg, origMsg) {
+ for (const cb of this._getEventCallbacks(EventType.REVERT)) {
+ try {
+ revertMsg = cb(change, revertMsg, origMsg);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return revertMsg;
+ }
+
+ modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) {
+ for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) {
+ try {
+ revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return revertSubmissionMsg;
+ }
+
+ getDiffLayers(path, changeNum, patchNum) {
+ const layers = [];
+ for (const annotationApi of
+ this._getEventCallbacks(EventType.ANNOTATE_DIFF)) {
+ try {
+ const layer = annotationApi.getLayer(path, changeNum, patchNum);
+ layers.push(layer);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return layers;
+ }
+
+ /**
+ * Retrieves coverage data possibly provided by a plugin.
+ *
+ * Will wait for plugins to be loaded. If multiple plugins offer a coverage
+ * provider, the first one is returned. If no plugin offers a coverage provider,
+ * will resolve to null.
+ *
+ * @return {!Promise<?GrAnnotationActionsInterface>}
+ */
+ getCoverageAnnotationApi() {
+ return Gerrit.awaitPluginsLoaded()
+ .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF)
+ .find(api => api.getCoverageProvider()));
+ }
+
+ getAdminMenuLinks() {
+ const links = [];
+ for (const adminApi of
+ this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) {
+ links.push(...adminApi.getMenuLinks());
+ }
+ return links;
+ }
+
+ getLabelValuesPostRevert(change) {
+ let labels = {};
+ for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) {
+ try {
+ labels = cb(change);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ return labels;
+ }
+
+ _getEventCallbacks(type) {
+ return this._eventCallbacks[type] || [];
+ }
+}
+
+customElements.define(GrJsApiInterface.is, GrJsApiInterface);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
deleted file mode 100644
index a4909ec..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ /dev/null
@@ -1,51 +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">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../plugins/gr-admin-api/gr-admin-api.html">
-<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
-<link rel="import" href="../../plugins/gr-change-metadata-api/gr-change-metadata-api.html">
-<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
-<link rel="import" href="../../plugins/gr-event-helper/gr-event-helper.html">
-<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
-<link rel="import" href="../../plugins/gr-repo-api/gr-repo-api.html">
-<link rel="import" href="../../plugins/gr-settings-api/gr-settings-api.html">
-<link rel="import" href="../../plugins/gr-styles-api/gr-styles-api.html">
-<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-<!--
- Note: the order matters as files depend on each other.
- 1. gr-api-utils will be used in multiple files below.
- 2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
- also gr-plugin-endpoints
- 3. gr-public-js-api depends on gr-plugin-rest-api
--->
-<script src="gr-api-utils.js"></script>
-<script src="../gr-event-interface/gr-event-interface.js"></script>
-<script src="gr-annotation-actions-context.js"></script>
-<script src="gr-annotation-actions-js-api.js"></script>
-<script src="gr-change-actions-js-api.js"></script>
-<script src="gr-change-reply-js-api.js"></script>
-<link rel="import" href="./gr-js-api-interface-element.html">
-<script src="gr-plugin-endpoints.js"></script>
-<script src="gr-plugin-action-context.js"></script>
-<script src="gr-plugin-rest-api.js"></script>
-<script src="gr-public-js-api.js"></script>
-<script src="gr-plugin-loader.js"></script>
-<script src="gr-gerrit.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
new file mode 100644
index 0000000..6b7b13e
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -0,0 +1,58 @@
+/**
+ * @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.
+ */
+import '../../../scripts/bundled-polymer.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../plugins/gr-admin-api/gr-admin-api.js';
+import '../../plugins/gr-attribute-helper/gr-attribute-helper.js';
+import '../../plugins/gr-change-metadata-api/gr-change-metadata-api.js';
+import '../../plugins/gr-dom-hooks/gr-dom-hooks.js';
+import '../../plugins/gr-event-helper/gr-event-helper.js';
+import '../../plugins/gr-popup-interface/gr-popup-interface.js';
+import '../../plugins/gr-repo-api/gr-repo-api.js';
+import '../../plugins/gr-settings-api/gr-settings-api.js';
+import '../../plugins/gr-styles-api/gr-styles-api.js';
+import '../../plugins/gr-theme-api/gr-theme-api.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import './gr-api-utils.js';
+import '../gr-event-interface/gr-event-interface.js';
+import './gr-annotation-actions-context.js';
+import './gr-annotation-actions-js-api.js';
+import './gr-change-actions-js-api.js';
+import './gr-change-reply-js-api.js';
+import './gr-js-api-interface-element.js';
+import './gr-plugin-endpoints.js';
+import './gr-plugin-action-context.js';
+import './gr-plugin-rest-api.js';
+import './gr-public-js-api.js';
+import './gr-plugin-loader.js';
+import './gr-gerrit.js';
+
+/*
+ Note: the order matters as files depend on each other.
+ 1. gr-api-utils will be used in multiple files below.
+ 2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
+ also gr-plugin-endpoints
+ 3. gr-public-js-api depends on gr-plugin-rest-api
+*/
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index efc7206..04ad490 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_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-api-interface</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-js-api-interface.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-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,539 +40,541 @@
</template>
</test-fixture>
-<script>
- suite('gr-js-api-interface tests', async () => {
- await readyToTest();
- const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
- let element;
- let plugin;
- let errorStub;
- let sandbox;
- let getResponseObjectStub;
- let sendStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-js-api-interface tests', () => {
+ const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
+ let element;
+ let plugin;
+ let errorStub;
+ let sandbox;
+ let getResponseObjectStub;
+ let sendStub;
- const throwErrFn = function() {
- throw Error('Unfortunately, this handler has stopped');
+ const throwErrFn = function() {
+ throw Error('Unfortunately, this handler has stopped');
+ };
+
+ setup(() => {
+ window.clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve({name: 'Judy Hopps'});
+ },
+ getResponseObject: getResponseObjectStub,
+ send(...args) {
+ return sendStub(...args);
+ },
+ });
+ element = fixture('basic');
+ errorStub = sandbox.stub(console, 'error');
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._loadPlugins([]);
+ });
+
+ teardown(() => {
+ window.clock.restore();
+ sandbox.restore();
+ element._removeEventCallbacks();
+ plugin = null;
+ });
+
+ test('url', () => {
+ assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
+ assert.equal(plugin.url('/static/test.js'),
+ 'http://test.com/plugins/testplugin/static/test.js');
+ });
+
+ test('url for preloaded plugin without ASSETS_PATH', () => {
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'preloaded:testpluginB');
+ assert.equal(plugin.url(),
+ `${window.location.origin}/plugins/testpluginB/`);
+ assert.equal(plugin.url('/static/test.js'),
+ `${window.location.origin}/plugins/testpluginB/static/test.js`);
+ });
+
+ test('url for preloaded plugin without ASSETS_PATH', () => {
+ const oldAssetsPath = window.ASSETS_PATH;
+ window.ASSETS_PATH = 'http://test.com';
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'preloaded:testpluginC');
+ assert.equal(plugin.url(), `${window.ASSETS_PATH}/plugins/testpluginC/`);
+ assert.equal(plugin.url('/static/test.js'),
+ `${window.ASSETS_PATH}/plugins/testpluginC/static/test.js`);
+ window.ASSETS_PATH = oldAssetsPath;
+ });
+
+ test('_send on failure rejects with response text', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return plugin._send().catch(r => {
+ assert.equal(r.message, 'text');
+ });
+ });
+
+ test('_send on failure without text rejects with code', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve(null); }}));
+ return plugin._send().catch(r => {
+ assert.equal(r.message, '400');
+ });
+ });
+
+ test('get', () => {
+ const response = {foo: 'foo'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.get('/url', r => {
+ assert.isTrue(sendStub.calledWith(
+ 'GET', 'http://test.com/plugins/testplugin/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('get using Promise', () => {
+ const response = {foo: 'foo'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.get('/url', r => 'rubbish').then(r => {
+ assert.isTrue(sendStub.calledWith(
+ 'GET', 'http://test.com/plugins/testplugin/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('post', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.post('/url', payload, r => {
+ assert.isTrue(sendStub.calledWith(
+ 'POST', 'http://test.com/plugins/testplugin/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('put', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return plugin.put('/url', payload, r => {
+ assert.isTrue(sendStub.calledWith(
+ 'PUT', 'http://test.com/plugins/testplugin/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete works', () => {
+ const response = {status: 204};
+ sendStub.returns(Promise.resolve(response));
+ return plugin.delete('/url', r => {
+ assert.isTrue(sendStub.calledWithExactly(
+ 'DELETE', 'http://test.com/plugins/testplugin/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete fails', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return plugin.delete('/url', r => {
+ throw new Error('Should not resolve');
+ }).catch(err => {
+ assert.isTrue(sendStub.calledWith(
+ 'DELETE', 'http://test.com/plugins/testplugin/url'));
+ assert.equal('text', err.message);
+ });
+ });
+
+ test('history event', done => {
+ plugin.on(element.EventType.HISTORY, throwErrFn);
+ plugin.on(element.EventType.HISTORY, path => {
+ assert.equal(path, '/path/to/awesomesauce');
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.HISTORY,
+ {path: '/path/to/awesomesauce'});
+ });
+
+ test('showchange event', done => {
+ const testChange = {
+ _number: 42,
+ revisions: {def: {_number: 2}, abc: {_number: 1}},
};
+ const expectedChange = Object.assign({mergeable: false}, testChange);
+ plugin.on(element.EventType.SHOW_CHANGE, throwErrFn);
+ plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => {
+ assert.deepEqual(change, expectedChange);
+ assert.deepEqual(revision, testChange.revisions.abc);
+ assert.deepEqual(info, {mergeable: false});
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.SHOW_CHANGE,
+ {change: testChange, patchNum: 1, info: {mergeable: false}});
+ });
+
+ test('show-revision-actions event', done => {
+ const testChange = {
+ _number: 42,
+ revisions: {def: {_number: 2}, abc: {_number: 1}},
+ };
+ plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn);
+ plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
+ assert.deepEqual(change, testChange);
+ assert.deepEqual(actions, {test: {}});
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS,
+ {change: testChange, revisionActions: {test: {}}});
+ });
+
+ test('handleEvent awaits plugins load', done => {
+ const testChange = {
+ _number: 42,
+ revisions: {def: {_number: 2}, abc: {_number: 1}},
+ };
+ const spy = sandbox.spy();
+ Gerrit._loadPlugins(['plugins/test.html']);
+ plugin.on(element.EventType.SHOW_CHANGE, spy);
+ element.handleEvent(element.EventType.SHOW_CHANGE,
+ {change: testChange, patchNum: 1});
+ assert.isFalse(spy.called);
+
+ // Timeout on loading plugins
+ window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
+ flush(() => {
+ assert.isTrue(spy.called);
+ done();
+ });
+ });
+
+ test('comment event', done => {
+ const testCommentNode = {foo: 'bar'};
+ plugin.on(element.EventType.COMMENT, throwErrFn);
+ plugin.on(element.EventType.COMMENT, commentNode => {
+ assert.deepEqual(commentNode, testCommentNode);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
+ });
+
+ test('revert event', () => {
+ function appendToRevertMsg(c, revertMsg, originalMsg) {
+ return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
+ }
+
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test');
+ assert.equal(errorStub.callCount, 0);
+
+ plugin.on(element.EventType.REVERT, throwErrFn);
+ plugin.on(element.EventType.REVERT, appendToRevertMsg);
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo');
+ assert.isTrue(errorStub.calledOnce);
+
+ plugin.on(element.EventType.REVERT, appendToRevertMsg);
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo\n> origTest\ninfo');
+ assert.isTrue(errorStub.calledTwice);
+ });
+
+ test('postrevert event', () => {
+ function getLabels(c) {
+ return {'Code-Review': 1};
+ }
+
+ assert.deepEqual(element.getLabelValuesPostRevert(null), {});
+ assert.equal(errorStub.callCount, 0);
+
+ plugin.on(element.EventType.POST_REVERT, throwErrFn);
+ plugin.on(element.EventType.POST_REVERT, getLabels);
+ assert.deepEqual(
+ element.getLabelValuesPostRevert(null), {'Code-Review': 1});
+ assert.isTrue(errorStub.calledOnce);
+ });
+
+ test('commitmsgedit event', done => {
+ const testMsg = 'Test CL commit message';
+ plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn);
+ plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => {
+ assert.deepEqual(msg, testMsg);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleCommitMessage(null, testMsg);
+ });
+
+ test('labelchange event', done => {
+ const testChange = {_number: 42};
+ plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
+ plugin.on(element.EventType.LABEL_CHANGE, change => {
+ assert.deepEqual(change, testChange);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange});
+ });
+
+ test('submitchange', () => {
+ plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn);
+ plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
+ assert.isTrue(element.canSubmitChange());
+ assert.isTrue(errorStub.calledOnce);
+ plugin.on(element.EventType.SUBMIT_CHANGE, () => false);
+ plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
+ assert.isFalse(element.canSubmitChange());
+ assert.isTrue(errorStub.calledTwice);
+ });
+
+ test('highlightjs-loaded event', done => {
+ const testHljs = {_number: 42};
+ plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn);
+ plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => {
+ assert.deepEqual(hljs, testHljs);
+ assert.isTrue(errorStub.calledOnce);
+ done();
+ });
+ element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
+ });
+
+ test('getLoggedIn', done => {
+ // fake fetch for authCheck
+ sandbox.stub(window, 'fetch', () => Promise.resolve({status: 204}));
+ plugin.restApi().getLoggedIn()
+ .then(loggedIn => {
+ assert.isTrue(loggedIn);
+ done();
+ });
+ });
+
+ test('attributeHelper', () => {
+ assert.isOk(plugin.attributeHelper());
+ });
+
+ test('deprecated.install', () => {
+ plugin.deprecated.install();
+ assert.strictEqual(plugin.popup, plugin.deprecated.popup);
+ assert.strictEqual(plugin.onAction, plugin.deprecated.onAction);
+ assert.notStrictEqual(plugin.install, plugin.deprecated.install);
+ });
+
+ test('getAdminMenuLinks', () => {
+ const links = [{text: 'a', url: 'b'}, {text: 'c', url: 'd'}];
+ const getCallbacksStub = sandbox.stub(element, '_getEventCallbacks')
+ .returns([
+ {getMenuLinks: () => [links[0]]},
+ {getMenuLinks: () => [links[1]]},
+ ]);
+ const result = element.getAdminMenuLinks();
+ assert.deepEqual(result, links);
+ assert.isTrue(getCallbacksStub.calledOnce);
+ assert.equal(getCallbacksStub.lastCall.args[0],
+ element.EventType.ADMIN_MENU_LINKS);
+ });
+
+ suite('test plugin with base url', () => {
+ let baseUrlPlugin;
setup(() => {
- window.clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
- getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve({name: 'Judy Hopps'});
- },
- getResponseObject: getResponseObjectStub,
- send(...args) {
- return sendStub(...args);
- },
- });
- element = fixture('basic');
- errorStub = sandbox.stub(console, 'error');
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._loadPlugins([]);
- });
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
- teardown(() => {
- window.clock.restore();
- sandbox.restore();
- element._removeEventCallbacks();
- plugin = null;
+ Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
+ 'http://test.com/r/plugins/baseurlplugin/static/test.js');
});
test('url', () => {
- assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
- assert.equal(plugin.url('/static/test.js'),
- 'http://test.com/plugins/testplugin/static/test.js');
+ assert.notEqual(baseUrlPlugin.url(),
+ 'http://test.com/plugins/baseurlplugin/');
+ assert.equal(baseUrlPlugin.url(),
+ 'http://test.com/r/plugins/baseurlplugin/');
+ assert.equal(baseUrlPlugin.url('/static/test.js'),
+ 'http://test.com/r/plugins/baseurlplugin/static/test.js');
+ });
+ });
+
+ suite('popup', () => {
+ test('popup(element) is deprecated', () => {
+ plugin.popup(document.createElement('div'));
+ assert.isTrue(console.error.calledOnce);
});
- test('url for preloaded plugin without ASSETS_PATH', () => {
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'preloaded:testpluginB');
- assert.equal(plugin.url(),
- `${window.location.origin}/plugins/testpluginB/`);
- assert.equal(plugin.url('/static/test.js'),
- `${window.location.origin}/plugins/testpluginB/static/test.js`);
+ test('popup(moduleName) creates popup with component', () => {
+ const openStub = sandbox.stub();
+ sandbox.stub(window, 'GrPopupInterface').returns({
+ open: openStub,
+ });
+ plugin.popup('some-name');
+ assert.isTrue(openStub.calledOnce);
+ assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name'));
});
- test('url for preloaded plugin without ASSETS_PATH', () => {
- const oldAssetsPath = window.ASSETS_PATH;
- window.ASSETS_PATH = 'http://test.com';
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'preloaded:testpluginC');
- assert.equal(plugin.url(), `${window.ASSETS_PATH}/plugins/testpluginC/`);
- assert.equal(plugin.url('/static/test.js'),
- `${window.ASSETS_PATH}/plugins/testpluginC/static/test.js`);
- window.ASSETS_PATH = oldAssetsPath;
+ test('deprecated.popup(element) creates popup with element', () => {
+ const el = document.createElement('div');
+ el.textContent = 'some text here';
+ const openStub = sandbox.stub(GrPopupInterface.prototype, 'open');
+ openStub.returns(Promise.resolve({
+ _getElement() {
+ return document.createElement('div');
+ }}));
+ plugin.deprecated.popup(el);
+ assert.isTrue(openStub.calledOnce);
});
+ });
- test('_send on failure rejects with response text', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve('text'); }}));
- return plugin._send().catch(r => {
- assert.equal(r.message, 'text');
+ suite('onAction', () => {
+ let change;
+ let revision;
+ let actionDetails;
+
+ setup(() => {
+ change = {};
+ revision = {};
+ actionDetails = {__key: 'some'};
+ sandbox.stub(plugin, 'on').callsArgWith(1, change, revision);
+ sandbox.stub(plugin, 'changeActions').returns({
+ addTapListener: sandbox.stub().callsArg(1),
+ getActionDetails: () => actionDetails,
});
});
- test('_send on failure without text rejects with code', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve(null); }}));
- return plugin._send().catch(r => {
- assert.equal(r.message, '400');
+ test('returns GrPluginActionContext', () => {
+ const stub = sandbox.stub();
+ plugin.deprecated.onAction('change', 'foo', ctx => {
+ assert.isTrue(ctx instanceof GrPluginActionContext);
+ assert.strictEqual(ctx.change, change);
+ assert.strictEqual(ctx.revision, revision);
+ assert.strictEqual(ctx.action, actionDetails);
+ assert.strictEqual(ctx.plugin, plugin);
+ stub();
});
+ assert.isTrue(stub.called);
});
- test('get', () => {
- const response = {foo: 'foo'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.get('/url', r => {
- assert.isTrue(sendStub.calledWith(
- 'GET', 'http://test.com/plugins/testplugin/url'));
- assert.strictEqual(r, response);
- });
+ test('other actions', () => {
+ const stub = sandbox.stub();
+ plugin.deprecated.onAction('project', 'foo', stub);
+ plugin.deprecated.onAction('edit', 'foo', stub);
+ plugin.deprecated.onAction('branch', 'foo', stub);
+ assert.isFalse(stub.called);
+ });
+ });
+
+ suite('screen', () => {
+ test('screenUrl()', () => {
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base');
+ assert.equal(plugin.screenUrl(), 'http://test.com/base/x/testplugin');
+ assert.equal(
+ plugin.screenUrl('foo'), 'http://test.com/base/x/testplugin/foo');
});
- test('get using Promise', () => {
- const response = {foo: 'foo'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.get('/url', r => 'rubbish').then(r => {
- assert.isTrue(sendStub.calledWith(
- 'GET', 'http://test.com/plugins/testplugin/url'));
- assert.strictEqual(r, response);
- });
+ test('deprecated works', () => {
+ const stub = sandbox.stub();
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+ plugin.deprecated.screen('foo', stub);
+ assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo'));
+ const fakeEl = {style: {display: ''}};
+ hookStub.onAttached.callArgWith(0, fakeEl);
+ assert.isTrue(stub.called);
+ assert.equal(fakeEl.style.display, 'none');
});
- test('post', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.post('/url', payload, r => {
- assert.isTrue(sendStub.calledWith(
- 'POST', 'http://test.com/plugins/testplugin/url', payload));
- assert.strictEqual(r, response);
- });
+ test('works', () => {
+ sandbox.stub(plugin, 'registerCustomComponent');
+ plugin.screen('foo', 'some-module');
+ assert.isTrue(plugin.registerCustomComponent.calledWith(
+ 'testplugin-screen-foo', 'some-module'));
+ });
+ });
+
+ suite('panel', () => {
+ let fakeEl;
+ let emulateAttached;
+
+ setup(()=> {
+ fakeEl = {change: {}, revision: {}};
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+ emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
});
- test('put', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return plugin.put('/url', payload, r => {
- assert.isTrue(sendStub.calledWith(
- 'PUT', 'http://test.com/plugins/testplugin/url', payload));
- assert.strictEqual(r, response);
- });
+ test('plugin.panel is deprecated', () => {
+ plugin.panel('rubbish');
+ assert.isTrue(console.error.called);
});
- test('delete works', () => {
- const response = {status: 204};
- sendStub.returns(Promise.resolve(response));
- return plugin.delete('/url', r => {
- assert.isTrue(sendStub.calledWithExactly(
- 'DELETE', 'http://test.com/plugins/testplugin/url'));
- assert.strictEqual(r, response);
- });
- });
-
- test('delete fails', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve('text'); }}));
- return plugin.delete('/url', r => {
- throw new Error('Should not resolve');
- }).catch(err => {
- assert.isTrue(sendStub.calledWith(
- 'DELETE', 'http://test.com/plugins/testplugin/url'));
- assert.equal('text', err.message);
- });
- });
-
- test('history event', done => {
- plugin.on(element.EventType.HISTORY, throwErrFn);
- plugin.on(element.EventType.HISTORY, path => {
- assert.equal(path, '/path/to/awesomesauce');
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.HISTORY,
- {path: '/path/to/awesomesauce'});
- });
-
- test('showchange event', done => {
- const testChange = {
- _number: 42,
- revisions: {def: {_number: 2}, abc: {_number: 1}},
- };
- const expectedChange = Object.assign({mergeable: false}, testChange);
- plugin.on(element.EventType.SHOW_CHANGE, throwErrFn);
- plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => {
- assert.deepEqual(change, expectedChange);
- assert.deepEqual(revision, testChange.revisions.abc);
- assert.deepEqual(info, {mergeable: false});
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.SHOW_CHANGE,
- {change: testChange, patchNum: 1, info: {mergeable: false}});
- });
-
- test('show-revision-actions event', done => {
- const testChange = {
- _number: 42,
- revisions: {def: {_number: 2}, abc: {_number: 1}},
- };
- plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn);
- plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => {
- assert.deepEqual(change, testChange);
- assert.deepEqual(actions, {test: {}});
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS,
- {change: testChange, revisionActions: {test: {}}});
- });
-
- test('handleEvent awaits plugins load', done => {
- const testChange = {
- _number: 42,
- revisions: {def: {_number: 2}, abc: {_number: 1}},
- };
- const spy = sandbox.spy();
- Gerrit._loadPlugins(['plugins/test.html']);
- plugin.on(element.EventType.SHOW_CHANGE, spy);
- element.handleEvent(element.EventType.SHOW_CHANGE,
- {change: testChange, patchNum: 1});
- assert.isFalse(spy.called);
-
- // Timeout on loading plugins
- window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
-
- flush(() => {
- assert.isTrue(spy.called);
- done();
- });
- });
-
- test('comment event', done => {
- const testCommentNode = {foo: 'bar'};
- plugin.on(element.EventType.COMMENT, throwErrFn);
- plugin.on(element.EventType.COMMENT, commentNode => {
- assert.deepEqual(commentNode, testCommentNode);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
- });
-
- test('revert event', () => {
- function appendToRevertMsg(c, revertMsg, originalMsg) {
- return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
- }
-
- assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test');
- assert.equal(errorStub.callCount, 0);
-
- plugin.on(element.EventType.REVERT, throwErrFn);
- plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
- 'test\n> origTest\ninfo');
- assert.isTrue(errorStub.calledOnce);
-
- plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
- 'test\n> origTest\ninfo\n> origTest\ninfo');
- assert.isTrue(errorStub.calledTwice);
- });
-
- test('postrevert event', () => {
- function getLabels(c) {
- return {'Code-Review': 1};
- }
-
- assert.deepEqual(element.getLabelValuesPostRevert(null), {});
- assert.equal(errorStub.callCount, 0);
-
- plugin.on(element.EventType.POST_REVERT, throwErrFn);
- plugin.on(element.EventType.POST_REVERT, getLabels);
- assert.deepEqual(
- element.getLabelValuesPostRevert(null), {'Code-Review': 1});
- assert.isTrue(errorStub.calledOnce);
- });
-
- test('commitmsgedit event', done => {
- const testMsg = 'Test CL commit message';
- plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn);
- plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => {
- assert.deepEqual(msg, testMsg);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleCommitMessage(null, testMsg);
- });
-
- test('labelchange event', done => {
- const testChange = {_number: 42};
- plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
- plugin.on(element.EventType.LABEL_CHANGE, change => {
- assert.deepEqual(change, testChange);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange});
- });
-
- test('submitchange', () => {
- plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn);
- plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
- assert.isTrue(element.canSubmitChange());
- assert.isTrue(errorStub.calledOnce);
- plugin.on(element.EventType.SUBMIT_CHANGE, () => false);
- plugin.on(element.EventType.SUBMIT_CHANGE, () => true);
- assert.isFalse(element.canSubmitChange());
- assert.isTrue(errorStub.calledTwice);
- });
-
- test('highlightjs-loaded event', done => {
- const testHljs = {_number: 42};
- plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn);
- plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => {
- assert.deepEqual(hljs, testHljs);
- assert.isTrue(errorStub.calledOnce);
- done();
- });
- element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
- });
-
- test('getLoggedIn', done => {
- // fake fetch for authCheck
- sandbox.stub(window, 'fetch', () => Promise.resolve({status: 204}));
- plugin.restApi().getLoggedIn()
- .then(loggedIn => {
- assert.isTrue(loggedIn);
- done();
- });
- });
-
- test('attributeHelper', () => {
- assert.isOk(plugin.attributeHelper());
- });
-
- test('deprecated.install', () => {
- plugin.deprecated.install();
- assert.strictEqual(plugin.popup, plugin.deprecated.popup);
- assert.strictEqual(plugin.onAction, plugin.deprecated.onAction);
- assert.notStrictEqual(plugin.install, plugin.deprecated.install);
- });
-
- test('getAdminMenuLinks', () => {
- const links = [{text: 'a', url: 'b'}, {text: 'c', url: 'd'}];
- const getCallbacksStub = sandbox.stub(element, '_getEventCallbacks')
- .returns([
- {getMenuLinks: () => [links[0]]},
- {getMenuLinks: () => [links[1]]},
- ]);
- const result = element.getAdminMenuLinks();
- assert.deepEqual(result, links);
- assert.isTrue(getCallbacksStub.calledOnce);
- assert.equal(getCallbacksStub.lastCall.args[0],
- element.EventType.ADMIN_MENU_LINKS);
- });
-
- suite('test plugin with base url', () => {
- let baseUrlPlugin;
-
- setup(() => {
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
-
- Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
- 'http://test.com/r/plugins/baseurlplugin/static/test.js');
- });
-
- test('url', () => {
- assert.notEqual(baseUrlPlugin.url(),
- 'http://test.com/plugins/baseurlplugin/');
- assert.equal(baseUrlPlugin.url(),
- 'http://test.com/r/plugins/baseurlplugin/');
- assert.equal(baseUrlPlugin.url('/static/test.js'),
- 'http://test.com/r/plugins/baseurlplugin/static/test.js');
- });
- });
-
- suite('popup', () => {
- test('popup(element) is deprecated', () => {
- plugin.popup(document.createElement('div'));
- assert.isTrue(console.error.calledOnce);
- });
-
- test('popup(moduleName) creates popup with component', () => {
- const openStub = sandbox.stub();
- sandbox.stub(window, 'GrPopupInterface').returns({
- open: openStub,
- });
- plugin.popup('some-name');
- assert.isTrue(openStub.calledOnce);
- assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name'));
- });
-
- test('deprecated.popup(element) creates popup with element', () => {
- const el = document.createElement('div');
- el.textContent = 'some text here';
- const openStub = sandbox.stub(GrPopupInterface.prototype, 'open');
- openStub.returns(Promise.resolve({
- _getElement() {
- return document.createElement('div');
- }}));
- plugin.deprecated.popup(el);
- assert.isTrue(openStub.calledOnce);
- });
- });
-
- suite('onAction', () => {
- let change;
- let revision;
- let actionDetails;
-
- setup(() => {
- change = {};
- revision = {};
- actionDetails = {__key: 'some'};
- sandbox.stub(plugin, 'on').callsArgWith(1, change, revision);
- sandbox.stub(plugin, 'changeActions').returns({
- addTapListener: sandbox.stub().callsArg(1),
- getActionDetails: () => actionDetails,
- });
- });
-
- test('returns GrPluginActionContext', () => {
- const stub = sandbox.stub();
- plugin.deprecated.onAction('change', 'foo', ctx => {
- assert.isTrue(ctx instanceof GrPluginActionContext);
- assert.strictEqual(ctx.change, change);
- assert.strictEqual(ctx.revision, revision);
- assert.strictEqual(ctx.action, actionDetails);
- assert.strictEqual(ctx.plugin, plugin);
- stub();
- });
- assert.isTrue(stub.called);
- });
-
- test('other actions', () => {
- const stub = sandbox.stub();
- plugin.deprecated.onAction('project', 'foo', stub);
- plugin.deprecated.onAction('edit', 'foo', stub);
- plugin.deprecated.onAction('branch', 'foo', stub);
- assert.isFalse(stub.called);
- });
- });
-
- suite('screen', () => {
- test('screenUrl()', () => {
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base');
- assert.equal(plugin.screenUrl(), 'http://test.com/base/x/testplugin');
- assert.equal(
- plugin.screenUrl('foo'), 'http://test.com/base/x/testplugin/foo');
- });
-
- test('deprecated works', () => {
- const stub = sandbox.stub();
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
- plugin.deprecated.screen('foo', stub);
- assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo'));
- const fakeEl = {style: {display: ''}};
- hookStub.onAttached.callArgWith(0, fakeEl);
- assert.isTrue(stub.called);
- assert.equal(fakeEl.style.display, 'none');
- });
-
- test('works', () => {
- sandbox.stub(plugin, 'registerCustomComponent');
- plugin.screen('foo', 'some-module');
- assert.isTrue(plugin.registerCustomComponent.calledWith(
- 'testplugin-screen-foo', 'some-module'));
- });
- });
-
- suite('panel', () => {
- let fakeEl;
- let emulateAttached;
-
- setup(()=> {
- fakeEl = {change: {}, revision: {}};
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
- emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl);
- });
-
- test('plugin.panel is deprecated', () => {
- plugin.panel('rubbish');
- assert.isTrue(console.error.called);
- });
-
- [
- ['CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK', 'change-view-integration'],
- ['CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK', 'change-metadata-item'],
- ].forEach(([panelName, endpointName]) => {
- test(`deprecated.panel works for ${panelName}`, () => {
- const callback = sandbox.stub();
- plugin.deprecated.panel(panelName, callback);
- assert.isTrue(plugin.hook.calledWith(endpointName));
- emulateAttached();
- assert.isTrue(callback.called);
- const args = callback.args[0][0];
- assert.strictEqual(args.body, fakeEl);
- assert.strictEqual(args.p.CHANGE_INFO, fakeEl.change);
- assert.strictEqual(args.p.REVISION_INFO, fakeEl.revision);
- });
- });
- });
-
- suite('settingsScreen', () => {
- test('plugin.settingsScreen is deprecated', () => {
- plugin.settingsScreen('rubbish');
- assert.isTrue(console.error.called);
- });
-
- test('plugin.settings() returns GrSettingsApi', () => {
- assert.isOk(plugin.settings());
- assert.isTrue(plugin.settings() instanceof GrSettingsApi);
- });
-
- test('plugin.deprecated.settingsScreen() works', () => {
- const hookStub = {onAttached: sandbox.stub()};
- sandbox.stub(plugin, 'hook').returns(hookStub);
- const fakeSettings = {};
- fakeSettings.title = sandbox.stub().returns(fakeSettings);
- fakeSettings.token = sandbox.stub().returns(fakeSettings);
- fakeSettings.module = sandbox.stub().returns(fakeSettings);
- fakeSettings.build = sandbox.stub().returns(hookStub);
- sandbox.stub(plugin, 'settings').returns(fakeSettings);
+ [
+ ['CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK', 'change-view-integration'],
+ ['CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK', 'change-metadata-item'],
+ ].forEach(([panelName, endpointName]) => {
+ test(`deprecated.panel works for ${panelName}`, () => {
const callback = sandbox.stub();
-
- plugin.deprecated.settingsScreen('path', 'menu', callback);
- assert.isTrue(fakeSettings.title.calledWith('menu'));
- assert.isTrue(fakeSettings.token.calledWith('path'));
- assert.isTrue(fakeSettings.module.calledWith('div'));
- assert.equal(fakeSettings.build.callCount, 1);
-
- const fakeBody = {};
- const fakeEl = {
- style: {
- display: '',
- },
- querySelector: sandbox.stub().returns(fakeBody),
- };
- // Emulate settings screen attached
- hookStub.onAttached.callArgWith(0, fakeEl);
+ plugin.deprecated.panel(panelName, callback);
+ assert.isTrue(plugin.hook.calledWith(endpointName));
+ emulateAttached();
assert.isTrue(callback.called);
const args = callback.args[0][0];
- assert.strictEqual(args.body, fakeBody);
- assert.equal(fakeEl.style.display, 'none');
+ assert.strictEqual(args.body, fakeEl);
+ assert.strictEqual(args.p.CHANGE_INFO, fakeEl.change);
+ assert.strictEqual(args.p.REVISION_INFO, fakeEl.revision);
});
});
});
+
+ suite('settingsScreen', () => {
+ test('plugin.settingsScreen is deprecated', () => {
+ plugin.settingsScreen('rubbish');
+ assert.isTrue(console.error.called);
+ });
+
+ test('plugin.settings() returns GrSettingsApi', () => {
+ assert.isOk(plugin.settings());
+ assert.isTrue(plugin.settings() instanceof GrSettingsApi);
+ });
+
+ test('plugin.deprecated.settingsScreen() works', () => {
+ const hookStub = {onAttached: sandbox.stub()};
+ sandbox.stub(plugin, 'hook').returns(hookStub);
+ const fakeSettings = {};
+ fakeSettings.title = sandbox.stub().returns(fakeSettings);
+ fakeSettings.token = sandbox.stub().returns(fakeSettings);
+ fakeSettings.module = sandbox.stub().returns(fakeSettings);
+ fakeSettings.build = sandbox.stub().returns(hookStub);
+ sandbox.stub(plugin, 'settings').returns(fakeSettings);
+ const callback = sandbox.stub();
+
+ plugin.deprecated.settingsScreen('path', 'menu', callback);
+ assert.isTrue(fakeSettings.title.calledWith('menu'));
+ assert.isTrue(fakeSettings.token.calledWith('path'));
+ assert.isTrue(fakeSettings.module.calledWith('div'));
+ assert.equal(fakeSettings.build.callCount, 1);
+
+ const fakeBody = {};
+ const fakeEl = {
+ style: {
+ display: '',
+ },
+ querySelector: sandbox.stub().returns(fakeBody),
+ };
+ // Emulate settings screen attached
+ hookStub.onAttached.callArgWith(0, fakeEl);
+ assert.isTrue(callback.called);
+ const args = callback.args[0][0];
+ assert.strictEqual(args.body, fakeBody);
+ assert.equal(fakeEl.style.display, 'none');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
index c8fb9b1..3ba2acc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_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-plugin-action-context</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-js-api-interface.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-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,126 +40,129 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-action-context tests', async () => {
- await readyToTest();
- let instance;
- let sandbox;
- let plugin;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-plugin-action-context tests', () => {
+ let instance;
+ let sandbox;
+ let plugin;
- setup(() => {
- sandbox = sinon.sandbox.create();
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrPluginActionContext(plugin);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ instance = new GrPluginActionContext(plugin);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('popup() and hide()', () => {
- const popupApiStub = {
- close: sandbox.stub(),
- };
- sandbox.stub(plugin.deprecated, 'popup').returns(popupApiStub);
- const el = {};
- instance.popup(el);
- assert.isTrue(instance.plugin.deprecated.popup.calledWith(el));
+ test('popup() and hide()', () => {
+ const popupApiStub = {
+ close: sandbox.stub(),
+ };
+ sandbox.stub(plugin.deprecated, 'popup').returns(popupApiStub);
+ const el = {};
+ instance.popup(el);
+ assert.isTrue(instance.plugin.deprecated.popup.calledWith(el));
- instance.hide();
- assert.isTrue(popupApiStub.close.called);
- });
+ instance.hide();
+ assert.isTrue(popupApiStub.close.called);
+ });
- test('textfield', () => {
- assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
- });
+ test('textfield', () => {
+ assert.equal(instance.textfield().tagName, 'PAPER-INPUT');
+ });
- test('br', () => {
- assert.equal(instance.br().tagName, 'BR');
- });
+ test('br', () => {
+ assert.equal(instance.br().tagName, 'BR');
+ });
- test('msg', () => {
- const el = instance.msg('foobar');
- assert.equal(el.tagName, 'GR-LABEL');
- assert.equal(el.textContent, 'foobar');
- });
+ test('msg', () => {
+ const el = instance.msg('foobar');
+ assert.equal(el.tagName, 'GR-LABEL');
+ assert.equal(el.textContent, 'foobar');
+ });
- test('div', () => {
- const el1 = document.createElement('span');
- el1.textContent = 'foo';
- const el2 = document.createElement('div');
- el2.textContent = 'bar';
- const div = instance.div(el1, el2);
- assert.equal(div.tagName, 'DIV');
- assert.equal(div.textContent, 'foobar');
- });
+ test('div', () => {
+ const el1 = document.createElement('span');
+ el1.textContent = 'foo';
+ const el2 = document.createElement('div');
+ el2.textContent = 'bar';
+ const div = instance.div(el1, el2);
+ assert.equal(div.tagName, 'DIV');
+ assert.equal(div.textContent, 'foobar');
+ });
- test('button', done => {
- const clickStub = sandbox.stub();
- const button = instance.button('foo', {onclick: clickStub});
- // If you don't attach a Polymer element to the DOM, then the ready()
- // callback will not be called and then e.g. this.$ is undefined.
- Polymer.dom(document.body).appendChild(button);
- MockInteractions.tap(button);
- flush(() => {
- assert.isTrue(clickStub.called);
- assert.equal(button.textContent, 'foo');
- done();
- });
- });
-
- test('checkbox', () => {
- const el = instance.checkbox();
- assert.equal(el.tagName, 'INPUT');
- assert.equal(el.type, 'checkbox');
- });
-
- test('label', () => {
- const fakeMsg = {};
- const fakeCheckbox = {};
- sandbox.stub(instance, 'div');
- sandbox.stub(instance, 'msg').returns(fakeMsg);
- instance.label(fakeCheckbox, 'foo');
- assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg));
- });
-
- test('call', () => {
- instance.action = {
- method: 'METHOD',
- __key: 'key',
- __url: '/changes/1/revisions/2/foo~bar',
- };
- const sendStub = sandbox.stub().returns(Promise.resolve());
- sandbox.stub(plugin, 'restApi').returns({
- send: sendStub,
- });
- const payload = {foo: 'foo'};
- const successStub = sandbox.stub();
- instance.call(payload, successStub);
- assert.isTrue(sendStub.calledWith(
- 'METHOD', '/changes/1/revisions/2/foo~bar', payload));
- });
-
- test('call error', done => {
- instance.action = {
- method: 'METHOD',
- __key: 'key',
- __url: '/changes/1/revisions/2/foo~bar',
- };
- const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom')));
- sandbox.stub(plugin, 'restApi').returns({
- send: sendStub,
- });
- const errorStub = sandbox.stub();
- document.addEventListener('show-alert', errorStub);
- instance.call();
- flush(() => {
- assert.isTrue(errorStub.calledOnce);
- assert.equal(errorStub.args[0][0].detail.message,
- 'Plugin network error: Error: boom');
- done();
- });
+ test('button', done => {
+ const clickStub = sandbox.stub();
+ const button = instance.button('foo', {onclick: clickStub});
+ // If you don't attach a Polymer element to the DOM, then the ready()
+ // callback will not be called and then e.g. this.$ is undefined.
+ dom(document.body).appendChild(button);
+ MockInteractions.tap(button);
+ flush(() => {
+ assert.isTrue(clickStub.called);
+ assert.equal(button.textContent, 'foo');
+ done();
});
});
+
+ test('checkbox', () => {
+ const el = instance.checkbox();
+ assert.equal(el.tagName, 'INPUT');
+ assert.equal(el.type, 'checkbox');
+ });
+
+ test('label', () => {
+ const fakeMsg = {};
+ const fakeCheckbox = {};
+ sandbox.stub(instance, 'div');
+ sandbox.stub(instance, 'msg').returns(fakeMsg);
+ instance.label(fakeCheckbox, 'foo');
+ assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg));
+ });
+
+ test('call', () => {
+ instance.action = {
+ method: 'METHOD',
+ __key: 'key',
+ __url: '/changes/1/revisions/2/foo~bar',
+ };
+ const sendStub = sandbox.stub().returns(Promise.resolve());
+ sandbox.stub(plugin, 'restApi').returns({
+ send: sendStub,
+ });
+ const payload = {foo: 'foo'};
+ const successStub = sandbox.stub();
+ instance.call(payload, successStub);
+ assert.isTrue(sendStub.calledWith(
+ 'METHOD', '/changes/1/revisions/2/foo~bar', payload));
+ });
+
+ test('call error', done => {
+ instance.action = {
+ method: 'METHOD',
+ __key: 'key',
+ __url: '/changes/1/revisions/2/foo~bar',
+ };
+ const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom')));
+ sandbox.stub(plugin, 'restApi').returns({
+ send: sendStub,
+ });
+ const errorStub = sandbox.stub();
+ document.addEventListener('show-alert', errorStub);
+ instance.call();
+ flush(() => {
+ assert.isTrue(errorStub.calledOnce);
+ assert.equal(errorStub.args[0][0].detail.message,
+ 'Plugin network error: Error: boom');
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index 5c0d69a..94bb771 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -19,132 +19,139 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-endpoints</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-js-api-interface.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-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+void(0);
+</script>
-<script>
- suite('gr-plugin-endpoints tests', async () => {
- await readyToTest();
- let sandbox;
- let instance;
- let pluginFoo;
- let pluginBar;
- let domHook;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-plugin-endpoints tests', () => {
+ let sandbox;
+ let instance;
+ let pluginFoo;
+ let pluginBar;
+ let domHook;
- setup(() => {
- sandbox = sinon.sandbox.create();
- domHook = {};
- instance = new GrPluginEndpoints();
- Gerrit.install(p => { pluginFoo = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/foo.html');
- instance.registerModule(
- pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
- Gerrit.install(p => { pluginBar = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/bar.html');
- instance.registerModule(
- pluginBar, 'a-place', 'style', 'bar-module', domHook);
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ domHook = {};
+ instance = new GrPluginEndpoints();
+ Gerrit.install(p => { pluginFoo = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/foo.html');
+ instance.registerModule(
+ pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
+ Gerrit.install(p => { pluginBar = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/bar.html');
+ instance.registerModule(
+ pluginBar, 'a-place', 'style', 'bar-module', domHook);
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('getDetails all', () => {
- assert.deepEqual(instance.getDetails('a-place'), [
- {
- moduleName: 'foo-module',
- plugin: pluginFoo,
- pluginUrl: pluginFoo._url,
- type: 'decorate',
- domHook,
- },
- {
- moduleName: 'bar-module',
- plugin: pluginBar,
- pluginUrl: pluginBar._url,
- type: 'style',
- domHook,
- },
- ]);
- });
-
- test('getDetails by type', () => {
- assert.deepEqual(instance.getDetails('a-place', {type: 'style'}), [
- {
- moduleName: 'bar-module',
- plugin: pluginBar,
- pluginUrl: pluginBar._url,
- type: 'style',
- domHook,
- },
- ]);
- });
-
- test('getDetails by module', () => {
- assert.deepEqual(
- instance.getDetails('a-place', {moduleName: 'foo-module'}),
- [
- {
- moduleName: 'foo-module',
- plugin: pluginFoo,
- pluginUrl: pluginFoo._url,
- type: 'decorate',
- domHook,
- },
- ]);
- });
-
- test('getModules', () => {
- assert.deepEqual(
- instance.getModules('a-place'), ['foo-module', 'bar-module']);
- });
-
- test('getPlugins', () => {
- assert.deepEqual(
- instance.getPlugins('a-place'), [pluginFoo._url]);
- });
-
- test('onNewEndpoint', () => {
- const newModuleStub = sandbox.stub();
- instance.onNewEndpoint('a-place', newModuleStub);
- instance.registerModule(
- pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
- assert.deepEqual(newModuleStub.lastCall.args[0], {
- moduleName: 'zaz-module',
+ test('getDetails all', () => {
+ assert.deepEqual(instance.getDetails('a-place'), [
+ {
+ moduleName: 'foo-module',
plugin: pluginFoo,
pluginUrl: pluginFoo._url,
- type: 'replace',
+ type: 'decorate',
domHook,
- });
- });
+ },
+ {
+ moduleName: 'bar-module',
+ plugin: pluginBar,
+ pluginUrl: pluginBar._url,
+ type: 'style',
+ domHook,
+ },
+ ]);
+ });
- test('reuse dom hooks', () => {
- instance.registerModule(
- pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
- assert.deepEqual(instance.getDetails('a-place'), [
- {
- moduleName: 'foo-module',
- plugin: pluginFoo,
- pluginUrl: pluginFoo._url,
- type: 'decorate',
- domHook,
- },
- {
- moduleName: 'bar-module',
- plugin: pluginBar,
- pluginUrl: pluginBar._url,
- type: 'style',
- domHook,
- },
- ]);
+ test('getDetails by type', () => {
+ assert.deepEqual(instance.getDetails('a-place', {type: 'style'}), [
+ {
+ moduleName: 'bar-module',
+ plugin: pluginBar,
+ pluginUrl: pluginBar._url,
+ type: 'style',
+ domHook,
+ },
+ ]);
+ });
+
+ test('getDetails by module', () => {
+ assert.deepEqual(
+ instance.getDetails('a-place', {moduleName: 'foo-module'}),
+ [
+ {
+ moduleName: 'foo-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'decorate',
+ domHook,
+ },
+ ]);
+ });
+
+ test('getModules', () => {
+ assert.deepEqual(
+ instance.getModules('a-place'), ['foo-module', 'bar-module']);
+ });
+
+ test('getPlugins', () => {
+ assert.deepEqual(
+ instance.getPlugins('a-place'), [pluginFoo._url]);
+ });
+
+ test('onNewEndpoint', () => {
+ const newModuleStub = sandbox.stub();
+ instance.onNewEndpoint('a-place', newModuleStub);
+ instance.registerModule(
+ pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
+ assert.deepEqual(newModuleStub.lastCall.args[0], {
+ moduleName: 'zaz-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'replace',
+ domHook,
});
});
+
+ test('reuse dom hooks', () => {
+ instance.registerModule(
+ pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
+ assert.deepEqual(instance.getDetails('a-place'), [
+ {
+ moduleName: 'foo-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'decorate',
+ domHook,
+ },
+ {
+ moduleName: 'bar-module',
+ plugin: pluginBar,
+ pluginUrl: pluginBar._url,
+ type: 'style',
+ domHook,
+ },
+ ]);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
index 1a9174f..46914dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_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-plugin-host</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-js-api-interface.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-js-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,531 +40,533 @@
</template>
</test-fixture>
-<script>
- suite('gr-plugin-loader tests', async () => {
- await readyToTest();
- const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
- let plugin;
- let sandbox;
- let url;
- let sendStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-plugin-loader tests', () => {
+ const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
+ let plugin;
+ let sandbox;
+ let url;
+ let sendStub;
+ setup(() => {
+ window.clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve({name: 'Judy Hopps'});
+ },
+ send(...args) {
+ return sendStub(...args);
+ },
+ });
+ sandbox.stub(document.body, 'appendChild');
+ fixture('basic');
+ url = window.location.origin;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ window.clock.restore();
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ test('reuse plugin for install calls', () => {
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+
+ let otherPlugin;
+ Gerrit.install(p => { otherPlugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ assert.strictEqual(plugin, otherPlugin);
+ });
+
+ test('flushes preinstalls if provided', () => {
+ assert.doesNotThrow(() => {
+ Gerrit._testOnly_flushPreinstalls();
+ });
+ window.Gerrit.flushPreinstalls = sandbox.stub();
+ Gerrit._testOnly_flushPreinstalls();
+ assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
+ delete window.Gerrit.flushPreinstalls;
+ });
+
+ test('versioning', () => {
+ const callback = sandbox.spy();
+ Gerrit.install(callback, '0.0pre-alpha');
+ assert(callback.notCalled);
+ });
+
+ test('report pluginsLoaded', done => {
+ stub('gr-reporting', {
+ pluginsLoaded() {
+ done();
+ },
+ });
+ Gerrit._loadPlugins([]);
+ });
+
+ test('arePluginsLoaded', done => {
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ Gerrit._loadPlugins(plugins);
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ // Timeout on loading plugins
+ window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
+ flush(() => {
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ test('plugins installed successfully', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ test('isPluginEnabled and isPluginLoaded', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ 'bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+ );
+
+ flush(() => {
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin))
+ );
+
+ done();
+ });
+ });
+
+ test('plugins installed mixed result, 1 fail 1 succeed', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ if (url === plugins[0]) {
+ throw new Error('failed');
+ }
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ done();
+ });
+ });
+
+ test('isPluginEnabled and isPluginLoaded for mixed results', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ if (url === plugins[0]) {
+ throw new Error('failed');
+ }
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+ );
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1]));
+ assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0]));
+ done();
+ });
+ });
+
+ test('plugins installed all failed', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ throw new Error('failed');
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly([]));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledTwice);
+ done();
+ });
+ });
+
+ test('plugins installed failed becasue of wrong version', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ }, url === plugins[0] ? '' : 'alpha', url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ done();
+ });
+ });
+
+ test('multiple assets for same plugin installed successfully', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/foo/static/test2.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ suite('plugin path and url', () => {
+ let importHtmlPluginStub;
+ let loadJsPluginStub;
setup(() => {
- window.clock = sinon.useFakeTimers();
- sandbox = sinon.sandbox.create();
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve({name: 'Judy Hopps'});
- },
- send(...args) {
- return sendStub(...args);
- },
+ importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
+ importHtmlPluginStub(url);
});
- sandbox.stub(document.body, 'appendChild');
- fixture('basic');
- url = window.location.origin;
+ loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
+ loadJsPluginStub(url);
+ });
+ });
+
+ test('invalid plugin path', () => {
+ const failToLoadStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => {
+ failToLoadStub(...args);
+ });
+
+ Gerrit._loadPlugins([
+ 'foo/bar',
+ ]);
+
+ assert.isTrue(failToLoadStub.calledOnce);
+ assert.isTrue(failToLoadStub.calledWithExactly(
+ 'Unrecognized plugin path foo/bar',
+ 'foo/bar'
+ ));
+ });
+
+ test('relative path for plugins', () => {
+ Gerrit._loadPlugins([
+ 'foo/bar.js',
+ 'foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`)
+ );
+ });
+
+ test('relative path should honor getBaseUrl', () => {
+ const testUrl = '/test';
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => testUrl);
+
+ Gerrit._loadPlugins([
+ 'foo/bar.js',
+ 'foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(
+ `${url}${testUrl}/foo/bar.html`
+ )
+ );
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`)
+ );
+ });
+
+ test('absolute path for plugins', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)
+ );
+ });
+ });
+
+ suite('With ASSETS_PATH', () => {
+ let importHtmlPluginStub;
+ let loadJsPluginStub;
+ setup(() => {
+ window.ASSETS_PATH = 'https://cdn.com';
+ importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
+ importHtmlPluginStub(url);
+ });
+ loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
+ loadJsPluginStub(url);
+ });
});
teardown(() => {
- sandbox.restore();
- window.clock.restore();
- Gerrit._testOnly_resetPlugins();
+ window.ASSETS_PATH = '';
});
- test('reuse plugin for install calls', () => {
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
-
- let otherPlugin;
- Gerrit.install(p => { otherPlugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- assert.strictEqual(plugin, otherPlugin);
- });
-
- test('flushes preinstalls if provided', () => {
- assert.doesNotThrow(() => {
- Gerrit._testOnly_flushPreinstalls();
- });
- window.Gerrit.flushPreinstalls = sandbox.stub();
- Gerrit._testOnly_flushPreinstalls();
- assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
- delete window.Gerrit.flushPreinstalls;
- });
-
- test('versioning', () => {
- const callback = sandbox.spy();
- Gerrit.install(callback, '0.0pre-alpha');
- assert(callback.notCalled);
- });
-
- test('report pluginsLoaded', done => {
- stub('gr-reporting', {
- pluginsLoaded() {
- done();
- },
- });
- Gerrit._loadPlugins([]);
- });
-
- test('arePluginsLoaded', done => {
- assert.isFalse(Gerrit._arePluginsLoaded());
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- Gerrit._loadPlugins(plugins);
- assert.isFalse(Gerrit._arePluginsLoaded());
- // Timeout on loading plugins
- window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
-
- flush(() => {
- assert.isTrue(Gerrit._arePluginsLoaded());
- done();
- });
- });
-
- test('plugins installed successfully', done => {
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => void 0, undefined, url);
- });
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- done();
- });
- });
-
- test('isPluginEnabled and isPluginLoaded', done => {
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => void 0, undefined, url);
- });
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- 'bar/static/test.js',
- ];
- Gerrit._loadPlugins(plugins);
- assert.isTrue(
- plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
- );
-
- flush(() => {
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(
- plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin))
- );
-
- done();
- });
- });
-
- test('plugins installed mixed result, 1 fail 1 succeed', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- if (url === plugins[0]) {
- throw new Error('failed');
- }
- }, undefined, url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledOnce);
- done();
- });
- });
-
- test('isPluginEnabled and isPluginLoaded for mixed results', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- if (url === plugins[0]) {
- throw new Error('failed');
- }
- }, undefined, url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
- assert.isTrue(
- plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
- );
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledOnce);
- assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1]));
- assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0]));
- done();
- });
- });
-
- test('plugins installed all failed', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- throw new Error('failed');
- }, undefined, url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly([]));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledTwice);
- done();
- });
- });
-
- test('plugins installed failed becasue of wrong version', done => {
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
-
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
-
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => {
- }, url === plugins[0] ? '' : 'alpha', url);
- });
-
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- assert.isTrue(alertStub.calledOnce);
- done();
- });
- });
-
- test('multiple assets for same plugin installed successfully', done => {
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => void 0, undefined, url);
- });
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
- });
-
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/foo/static/test2.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
- Gerrit._loadPlugins(plugins);
-
- flush(() => {
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
- assert.isTrue(Gerrit._arePluginsLoaded());
- done();
- });
- });
-
- suite('plugin path and url', () => {
- let importHtmlPluginStub;
- let loadJsPluginStub;
- setup(() => {
- importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
- importHtmlPluginStub(url);
- });
- loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
- loadJsPluginStub(url);
- });
- });
-
- test('invalid plugin path', () => {
- const failToLoadStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => {
- failToLoadStub(...args);
- });
-
- Gerrit._loadPlugins([
- 'foo/bar',
- ]);
-
- assert.isTrue(failToLoadStub.calledOnce);
- assert.isTrue(failToLoadStub.calledWithExactly(
- 'Unrecognized plugin path foo/bar',
- 'foo/bar'
- ));
- });
-
- test('relative path for plugins', () => {
- Gerrit._loadPlugins([
- 'foo/bar.js',
- 'foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`)
- );
- });
-
- test('relative path should honor getBaseUrl', () => {
- const testUrl = '/test';
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => testUrl);
-
- Gerrit._loadPlugins([
- 'foo/bar.js',
- 'foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(
- `${url}${testUrl}/foo/bar.html`
- )
- );
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`)
- );
- });
-
- test('absolute path for plugins', () => {
- Gerrit._loadPlugins([
- 'http://e.com/foo/bar.js',
- 'http://e.com/foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)
- );
- });
- });
-
- suite('With ASSETS_PATH', () => {
- let importHtmlPluginStub;
- let loadJsPluginStub;
- setup(() => {
- window.ASSETS_PATH = 'https://cdn.com';
- importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
- importHtmlPluginStub(url);
- });
- loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
- loadJsPluginStub(url);
- });
- });
-
- teardown(() => {
- window.ASSETS_PATH = '';
- });
-
- test('Should try load plugins from assets path instead', () => {
- Gerrit._loadPlugins([
- 'foo/bar.js',
- 'foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
- });
-
- test('Should honor original path if exists', () => {
- Gerrit._loadPlugins([
- 'http://e.com/foo/bar.html',
- 'http://e.com/foo/bar.js',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`));
- });
-
- test('Should try replace current host with assetsPath', () => {
- const host = window.location.origin;
- Gerrit._loadPlugins([
- `${host}/foo/bar.html`,
- `${host}/foo/bar.js`,
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
- });
- });
-
- test('adds js plugins will call the body', () => {
+ test('Should try load plugins from assets path instead', () => {
Gerrit._loadPlugins([
- 'http://e.com/foo/bar.js',
- 'http://e.com/bar/foo.js',
+ 'foo/bar.js',
+ 'foo/bar.html',
]);
- assert.isTrue(document.body.appendChild.calledTwice);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
});
- test('can call awaitPluginsLoaded multiple times', done => {
- const plugins = [
+ test('Should honor original path if exists', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.html',
'http://e.com/foo/bar.js',
- 'http://e.com/bar/foo.js',
- ];
+ ]);
- let installed = false;
- function pluginCallback(url) {
- if (url === plugins[1]) {
- installed = true;
- }
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`));
+ });
+
+ test('Should try replace current host with assetsPath', () => {
+ const host = window.location.origin;
+ Gerrit._loadPlugins([
+ `${host}/foo/bar.html`,
+ `${host}/foo/bar.js`,
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
+ });
+ });
+
+ test('adds js plugins will call the body', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/bar/foo.js',
+ ]);
+ assert.isTrue(document.body.appendChild.calledTwice);
+ });
+
+ test('can call awaitPluginsLoaded multiple times', done => {
+ const plugins = [
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/bar/foo.js',
+ ];
+
+ let installed = false;
+ function pluginCallback(url) {
+ if (url === plugins[1]) {
+ installed = true;
}
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- Gerrit.install(() => pluginCallback(url), undefined, url);
- });
+ }
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => pluginCallback(url), undefined, url);
+ });
- Gerrit._loadPlugins(plugins);
+ Gerrit._loadPlugins(plugins);
+
+ Gerrit.awaitPluginsLoaded().then(() => {
+ assert.isTrue(installed);
Gerrit.awaitPluginsLoaded().then(() => {
- assert.isTrue(installed);
-
- Gerrit.awaitPluginsLoaded().then(() => {
- done();
- });
- });
- });
-
- suite('preloaded plugins', () => {
- test('skips preloaded plugins when load plugins', () => {
- const importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
- importHtmlPluginStub(url);
- });
- const loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
- loadJsPluginStub(url);
- });
-
- Gerrit._preloadedPlugins = {
- foo: () => void 0,
- bar: () => void 0,
- };
-
- Gerrit._loadPlugins([
- 'http://e.com/plugins/foo.js',
- 'plugins/bar.html',
- 'http://e.com/plugins/test/foo.js',
- ]);
-
- assert.isTrue(importHtmlPluginStub.notCalled);
- assert.isTrue(loadJsPluginStub.calledOnce);
- });
-
- test('isPluginPreloaded', () => {
- Gerrit._preloadedPlugins = {baz: ()=>{}};
- assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar'));
- assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42'));
- assert.isTrue(
- Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
- );
- Gerrit._preloadedPlugins = null;
- });
-
- test('preloaded plugins are installed', () => {
- const installStub = sandbox.stub();
- Gerrit._preloadedPlugins = {foo: installStub};
- Gerrit._pluginLoader.installPreloadedPlugins();
- assert.isTrue(installStub.called);
- const pluginApi = installStub.lastCall.args[0];
- assert.strictEqual(pluginApi.getPluginName(), 'foo');
- });
-
- test('installing preloaded plugin', () => {
- let plugin;
- Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
- assert.strictEqual(plugin.getPluginName(), 'foo');
- assert.strictEqual(plugin.url('/some/thing.html'),
- `${window.location.origin}/plugins/foo/some/thing.html`);
+ done();
});
});
});
+
+ suite('preloaded plugins', () => {
+ test('skips preloaded plugins when load plugins', () => {
+ const importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
+ importHtmlPluginStub(url);
+ });
+ const loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ loadJsPluginStub(url);
+ });
+
+ Gerrit._preloadedPlugins = {
+ foo: () => void 0,
+ bar: () => void 0,
+ };
+
+ Gerrit._loadPlugins([
+ 'http://e.com/plugins/foo.js',
+ 'plugins/bar.html',
+ 'http://e.com/plugins/test/foo.js',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.notCalled);
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ });
+
+ test('isPluginPreloaded', () => {
+ Gerrit._preloadedPlugins = {baz: ()=>{}};
+ assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar'));
+ assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42'));
+ assert.isTrue(
+ Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
+ );
+ Gerrit._preloadedPlugins = null;
+ });
+
+ test('preloaded plugins are installed', () => {
+ const installStub = sandbox.stub();
+ Gerrit._preloadedPlugins = {foo: installStub};
+ Gerrit._pluginLoader.installPreloadedPlugins();
+ assert.isTrue(installStub.called);
+ const pluginApi = installStub.lastCall.args[0];
+ assert.strictEqual(pluginApi.getPluginName(), 'foo');
+ });
+
+ test('installing preloaded plugin', () => {
+ let plugin;
+ Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
+ assert.strictEqual(plugin.getPluginName(), 'foo');
+ assert.strictEqual(plugin.url('/some/thing.html'),
+ `${window.location.origin}/plugins/foo/some/thing.html`);
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
index a486bf1..64c31d7 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -19,139 +19,141 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-rest-api</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-js-api-interface.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-js-api-interface.js"></script>
-<script>
- suite('gr-plugin-rest-api tests', async () => {
- await readyToTest();
- let instance;
- let sandbox;
- let getResponseObjectStub;
- let sendStub;
- let restApiStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-js-api-interface.js';
+suite('gr-plugin-rest-api tests', () => {
+ let instance;
+ let sandbox;
+ let getResponseObjectStub;
+ let sendStub;
+ let restApiStub;
- setup(() => {
- sandbox = sinon.sandbox.create();
- getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
- sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- restApiStub = {
- getAccount: () => Promise.resolve({name: 'Judy Hopps'}),
- getResponseObject: getResponseObjectStub,
- send: sendStub,
- getLoggedIn: sandbox.stub(),
- getVersion: sandbox.stub(),
- getConfig: sandbox.stub(),
- };
- stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => {
- a[k] = (...args) => restApiStub[k](...args);
- return a;
- }, {}));
- Gerrit.install(p => {}, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrPluginRestApi();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ restApiStub = {
+ getAccount: () => Promise.resolve({name: 'Judy Hopps'}),
+ getResponseObject: getResponseObjectStub,
+ send: sendStub,
+ getLoggedIn: sandbox.stub(),
+ getVersion: sandbox.stub(),
+ getConfig: sandbox.stub(),
+ };
+ stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => {
+ a[k] = (...args) => restApiStub[k](...args);
+ return a;
+ }, {}));
+ Gerrit.install(p => {}, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ instance = new GrPluginRestApi();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('fetch', () => {
- const payload = {foo: 'foo'};
- return instance.fetch('HTTP_METHOD', '/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
- assert.equal(r.status, 200);
- assert.isFalse(getResponseObjectStub.called);
- });
- });
-
- test('send', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.send('HTTP_METHOD', '/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
- assert.strictEqual(r, response);
- });
- });
-
- test('get', () => {
- const response = {foo: 'foo'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.get('/url').then(r => {
- assert.isTrue(sendStub.calledWith('GET', '/url'));
- assert.strictEqual(r, response);
- });
- });
-
- test('post', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.post('/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('POST', '/url', payload));
- assert.strictEqual(r, response);
- });
- });
-
- test('put', () => {
- const payload = {foo: 'foo'};
- const response = {bar: 'bar'};
- getResponseObjectStub.returns(Promise.resolve(response));
- return instance.put('/url', payload).then(r => {
- assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
- assert.strictEqual(r, response);
- });
- });
-
- test('delete works', () => {
- const response = {status: 204};
- sendStub.returns(Promise.resolve(response));
- return instance.delete('/url').then(r => {
- assert.isTrue(sendStub.calledWith('DELETE', '/url'));
- assert.strictEqual(r, response);
- });
- });
-
- test('delete fails', () => {
- sendStub.returns(Promise.resolve(
- {status: 400, text() { return Promise.resolve('text'); }}));
- return instance.delete('/url').then(r => {
- throw new Error('Should not resolve');
- })
- .catch(err => {
- assert.isTrue(sendStub.calledWith('DELETE', '/url'));
- assert.equal('text', err.message);
- });
- });
-
- test('getLoggedIn', () => {
- restApiStub.getLoggedIn.returns(Promise.resolve(true));
- return instance.getLoggedIn().then(result => {
- assert.isTrue(restApiStub.getLoggedIn.calledOnce);
- assert.isTrue(result);
- });
- });
-
- test('getVersion', () => {
- restApiStub.getVersion.returns(Promise.resolve('foo bar'));
- return instance.getVersion().then(result => {
- assert.isTrue(restApiStub.getVersion.calledOnce);
- assert.equal(result, 'foo bar');
- });
- });
-
- test('getConfig', () => {
- restApiStub.getConfig.returns(Promise.resolve('foo bar'));
- return instance.getConfig().then(result => {
- assert.isTrue(restApiStub.getConfig.calledOnce);
- assert.equal(result, 'foo bar');
- });
+ test('fetch', () => {
+ const payload = {foo: 'foo'};
+ return instance.fetch('HTTP_METHOD', '/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
+ assert.equal(r.status, 200);
+ assert.isFalse(getResponseObjectStub.called);
});
});
+
+ test('send', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.send('HTTP_METHOD', '/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('get', () => {
+ const response = {foo: 'foo'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.get('/url').then(r => {
+ assert.isTrue(sendStub.calledWith('GET', '/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('post', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.post('/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('POST', '/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('put', () => {
+ const payload = {foo: 'foo'};
+ const response = {bar: 'bar'};
+ getResponseObjectStub.returns(Promise.resolve(response));
+ return instance.put('/url', payload).then(r => {
+ assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete works', () => {
+ const response = {status: 204};
+ sendStub.returns(Promise.resolve(response));
+ return instance.delete('/url').then(r => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete fails', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return instance.delete('/url').then(r => {
+ throw new Error('Should not resolve');
+ })
+ .catch(err => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.equal('text', err.message);
+ });
+ });
+
+ test('getLoggedIn', () => {
+ restApiStub.getLoggedIn.returns(Promise.resolve(true));
+ return instance.getLoggedIn().then(result => {
+ assert.isTrue(restApiStub.getLoggedIn.calledOnce);
+ assert.isTrue(result);
+ });
+ });
+
+ test('getVersion', () => {
+ restApiStub.getVersion.returns(Promise.resolve('foo bar'));
+ return instance.getVersion().then(result => {
+ assert.isTrue(restApiStub.getVersion.calledOnce);
+ assert.equal(result, 'foo bar');
+ });
+ });
+
+ test('getConfig', () => {
+ restApiStub.getConfig.returns(Promise.resolve('foo bar'));
+ return instance.getConfig().then(result => {
+ assert.isTrue(restApiStub.getConfig.calledOnce);
+ assert.equal(result, 'foo bar');
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index 6e99e01..ad9b852 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -14,156 +14,170 @@
* 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 GrLabelInfo extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-label-info'; }
+import '../../../styles/gr-voting-styles.js';
+import '../../../styles/shared-styles.js';
+import '../gr-account-label/gr-account-label.js';
+import '../gr-account-chip/gr-account-chip.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-label/gr-label.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-label-info_html.js';
- static get properties() {
- return {
- labelInfo: Object,
- label: String,
- /** @type {?} */
- change: Object,
- account: Object,
- mutable: Boolean,
- };
- }
+/** @extends Polymer.Element */
+class GrLabelInfo extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * @param {!Object} labelInfo
- * @param {!Object} account
- * @param {Object} changeLabelsRecord not used, but added as a parameter in
- * order to trigger computation when a label is removed from the change.
- */
- _mapLabelInfo(labelInfo, account, changeLabelsRecord) {
- const result = [];
- if (!labelInfo || !account) { return result; }
- if (!labelInfo.values) {
- if (labelInfo.rejected || labelInfo.approved) {
- const ok = labelInfo.approved || !labelInfo.rejected;
- return [{
- value: ok ? '👍️' : '👎️',
- className: ok ? 'positive' : 'negative',
- account: ok ? labelInfo.approved : labelInfo.rejected,
- }];
- }
- return result;
- }
- // Sort votes by positivity.
- const votes = (labelInfo.all || []).sort((a, b) => a.value - b.value);
- const values = Object.keys(labelInfo.values);
- for (const label of votes) {
- if (label.value && label.value != labelInfo.default_value) {
- let labelClassName;
- let labelValPrefix = '';
- if (label.value > 0) {
- labelValPrefix = '+';
- if (parseInt(label.value, 10) ===
- parseInt(values[values.length - 1], 10)) {
- labelClassName = 'max';
- } else {
- labelClassName = 'positive';
- }
- } else if (label.value < 0) {
- if (parseInt(label.value, 10) === parseInt(values[0], 10)) {
- labelClassName = 'min';
- } else {
- labelClassName = 'negative';
- }
- }
- const formattedLabel = {
- value: labelValPrefix + label.value,
- className: labelClassName,
- account: label,
- };
- if (label._account_id === account._account_id) {
- // Put self-votes at the top.
- result.unshift(formattedLabel);
- } else {
- result.push(formattedLabel);
- }
- }
+ static get is() { return 'gr-label-info'; }
+
+ static get properties() {
+ return {
+ labelInfo: Object,
+ label: String,
+ /** @type {?} */
+ change: Object,
+ account: Object,
+ mutable: Boolean,
+ };
+ }
+
+ /**
+ * @param {!Object} labelInfo
+ * @param {!Object} account
+ * @param {Object} changeLabelsRecord not used, but added as a parameter in
+ * order to trigger computation when a label is removed from the change.
+ */
+ _mapLabelInfo(labelInfo, account, changeLabelsRecord) {
+ const result = [];
+ if (!labelInfo || !account) { return result; }
+ if (!labelInfo.values) {
+ if (labelInfo.rejected || labelInfo.approved) {
+ const ok = labelInfo.approved || !labelInfo.rejected;
+ return [{
+ value: ok ? '👍️' : '👎️',
+ className: ok ? 'positive' : 'negative',
+ account: ok ? labelInfo.approved : labelInfo.rejected,
+ }];
}
return result;
}
-
- /**
- * A user is able to delete a vote iff the mutable property is true and the
- * reviewer that left the vote exists in the list of removable_reviewers
- * received from the backend.
- *
- * @param {!Object} reviewer An object describing the reviewer that left the
- * vote.
- * @param {boolean} mutable
- * @param {!Object} change
- */
- _computeDeleteClass(reviewer, mutable, change) {
- if (!mutable || !change || !change.removable_reviewers) {
- return 'hidden';
- }
- const removable = change.removable_reviewers;
- if (removable.find(r => r._account_id === reviewer._account_id)) {
- return '';
- }
- return 'hidden';
- }
-
- /**
- * Closure annotation for Polymer.prototype.splice is off.
- * For now, supressing annotations.
- *
- * @suppress {checkTypes} */
- _onDeleteVote(e) {
- e.preventDefault();
- let target = Polymer.dom(e).rootTarget;
- while (!target.classList.contains('deleteBtn')) {
- if (!target.parentElement) { return; }
- target = target.parentElement;
- }
-
- target.disabled = true;
- const accountID = parseInt(target.getAttribute('data-account-id'), 10);
- this._xhrPromise =
- this.$.restAPI.deleteVote(this.change._number, accountID, this.label)
- .then(response => {
- target.disabled = false;
- if (!response.ok) { return; }
- Gerrit.Nav.navigateToChange(this.change);
- })
- .catch(err => {
- target.disabled = false;
- return;
- });
- }
-
- _computeValueTooltip(labelInfo, score) {
- if (!labelInfo || !labelInfo.values || !labelInfo.values[score]) {
- return '';
- }
- return labelInfo.values[score];
- }
-
- /**
- * @param {!Object} labelInfo
- * @param {Object} changeLabelsRecord not used, but added as a parameter in
- * order to trigger computation when a label is removed from the change.
- */
- _computeShowPlaceholder(labelInfo, changeLabelsRecord) {
- if (labelInfo && labelInfo.all) {
- for (const label of labelInfo.all) {
- if (label.value && label.value != labelInfo.default_value) {
- return 'hidden';
+ // Sort votes by positivity.
+ const votes = (labelInfo.all || []).sort((a, b) => a.value - b.value);
+ const values = Object.keys(labelInfo.values);
+ for (const label of votes) {
+ if (label.value && label.value != labelInfo.default_value) {
+ let labelClassName;
+ let labelValPrefix = '';
+ if (label.value > 0) {
+ labelValPrefix = '+';
+ if (parseInt(label.value, 10) ===
+ parseInt(values[values.length - 1], 10)) {
+ labelClassName = 'max';
+ } else {
+ labelClassName = 'positive';
+ }
+ } else if (label.value < 0) {
+ if (parseInt(label.value, 10) === parseInt(values[0], 10)) {
+ labelClassName = 'min';
+ } else {
+ labelClassName = 'negative';
}
}
+ const formattedLabel = {
+ value: labelValPrefix + label.value,
+ className: labelClassName,
+ account: label,
+ };
+ if (label._account_id === account._account_id) {
+ // Put self-votes at the top.
+ result.unshift(formattedLabel);
+ } else {
+ result.push(formattedLabel);
+ }
}
- return '';
}
+ return result;
}
- customElements.define(GrLabelInfo.is, GrLabelInfo);
-})();
+ /**
+ * A user is able to delete a vote iff the mutable property is true and the
+ * reviewer that left the vote exists in the list of removable_reviewers
+ * received from the backend.
+ *
+ * @param {!Object} reviewer An object describing the reviewer that left the
+ * vote.
+ * @param {boolean} mutable
+ * @param {!Object} change
+ */
+ _computeDeleteClass(reviewer, mutable, change) {
+ if (!mutable || !change || !change.removable_reviewers) {
+ return 'hidden';
+ }
+ const removable = change.removable_reviewers;
+ if (removable.find(r => r._account_id === reviewer._account_id)) {
+ return '';
+ }
+ return 'hidden';
+ }
+
+ /**
+ * Closure annotation for Polymer.prototype.splice is off.
+ * For now, supressing annotations.
+ *
+ * @suppress {checkTypes} */
+ _onDeleteVote(e) {
+ e.preventDefault();
+ let target = dom(e).rootTarget;
+ while (!target.classList.contains('deleteBtn')) {
+ if (!target.parentElement) { return; }
+ target = target.parentElement;
+ }
+
+ target.disabled = true;
+ const accountID = parseInt(target.getAttribute('data-account-id'), 10);
+ this._xhrPromise =
+ this.$.restAPI.deleteVote(this.change._number, accountID, this.label)
+ .then(response => {
+ target.disabled = false;
+ if (!response.ok) { return; }
+ Gerrit.Nav.navigateToChange(this.change);
+ })
+ .catch(err => {
+ target.disabled = false;
+ return;
+ });
+ }
+
+ _computeValueTooltip(labelInfo, score) {
+ if (!labelInfo || !labelInfo.values || !labelInfo.values[score]) {
+ return '';
+ }
+ return labelInfo.values[score];
+ }
+
+ /**
+ * @param {!Object} labelInfo
+ * @param {Object} changeLabelsRecord not used, but added as a parameter in
+ * order to trigger computation when a label is removed from the change.
+ */
+ _computeShowPlaceholder(labelInfo, changeLabelsRecord) {
+ if (labelInfo && labelInfo.all) {
+ for (const label of labelInfo.all) {
+ if (label.value && label.value != labelInfo.default_value) {
+ return 'hidden';
+ }
+ }
+ }
+ return '';
+ }
+}
+
+customElements.define(GrLabelInfo.is, GrLabelInfo);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
index 27bd1c7..d19467b 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
@@ -1,33 +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/gr-voting-styles.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../gr-account-label/gr-account-label.html">
-<link rel="import" href="../gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-icons/gr-icons.html">
-<link rel="import" href="../gr-label/gr-label.html">
-<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-label-info">
- <template>
+export const htmlTemplate = html`
<style include="gr-voting-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -90,36 +79,22 @@
padding-top: var(--spacing-s);
}
</style>
- <table>
- <p class$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]">
+ <p class\$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]">
No votes.
- </p>
- <template
- is="dom-repeat"
- items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]"
- as="mappedLabel">
+ </p><table>
+
+ <template is="dom-repeat" items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]" as="mappedLabel">
<tr class="labelValueContainer">
<td>
- <gr-label
- has-tooltip
- title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]"
- class$="[[mappedLabel.className]] voteChip">
+ <gr-label has-tooltip="" title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]" class\$="[[mappedLabel.className]] voteChip">
[[mappedLabel.value]]
</gr-label>
</td>
<td>
- <gr-account-chip
- account="[[mappedLabel.account]]"
- transparent-background></gr-account-chip>
+ <gr-account-chip account="[[mappedLabel.account]]" transparent-background=""></gr-account-chip>
</td>
<td>
- <gr-button
- link
- aria-label="Remove"
- on-click="_onDeleteVote"
- tooltip="Remove vote"
- data-account-id$="[[mappedLabel.account._account_id]]"
- class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]">
+ <gr-button link="" aria-label="Remove" on-click="_onDeleteVote" tooltip="Remove vote" data-account-id\$="[[mappedLabel.account._account_id]]" class\$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]">
<iron-icon icon="gr-icons:delete"></iron-icon>
</gr-button>
</td>
@@ -127,6 +102,4 @@
</template>
</table>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-label-info.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
index 013a6ee..44627ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_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-label-info</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-label-info.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-label-info.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-label-info.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,205 +39,208 @@
</template>
</test-fixture>
-<script>
- suite('gr-account-link 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-label-info.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-account-link tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ // Needed to trigger computed bindings.
+ element.account = {};
+ element.change = {labels: {}};
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('remove reviewer votes', () => {
setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- // Needed to trigger computed bindings.
- element.account = {};
- element.change = {labels: {}};
+ sandbox.stub(element, '_computeValueTooltip').returns('');
+ element.account = {
+ _account_id: 1,
+ name: 'bojack',
+ };
+ const test = {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ };
+ element.change = {
+ _number: 42,
+ change_id: 'the id',
+ actions: [],
+ topic: 'the topic',
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {test},
+ removable_reviewers: [],
+ };
+ element.labelInfo = test;
+ element.label = 'test';
+
+ flushAsynchronousOperations();
});
- teardown(() => {
- sandbox.restore();
+ test('_computeCanDeleteVote', () => {
+ element.mutable = false;
+ const button = element.shadowRoot
+ .querySelector('gr-button');
+ assert.isTrue(isHidden(button));
+ element.change.removable_reviewers = [element.account];
+ element.mutable = true;
+ assert.isFalse(isHidden(button));
});
- suite('remove reviewer votes', () => {
- setup(() => {
- sandbox.stub(element, '_computeValueTooltip').returns('');
- element.account = {
- _account_id: 1,
- name: 'bojack',
- };
- const test = {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- };
- element.change = {
- _number: 42,
- change_id: 'the id',
- actions: [],
- topic: 'the topic',
- status: 'NEW',
- submit_type: 'CHERRY_PICK',
- labels: {test},
- removable_reviewers: [],
- };
- element.labelInfo = test;
- element.label = 'test';
+ test('deletes votes', () => {
+ const deleteResponse = Promise.resolve({ok: true});
+ const deleteStub = sandbox.stub(
+ element.$.restAPI, 'deleteVote').returns(deleteResponse);
- flushAsynchronousOperations();
+ element.change.removable_reviewers = [element.account];
+ element.change.labels.test.recommended = {_account_id: 1};
+ element.mutable = true;
+ const button = element.shadowRoot
+ .querySelector('gr-button');
+ MockInteractions.tap(button);
+ assert.isTrue(button.disabled);
+ return deleteResponse.then(() => {
+ assert.isFalse(button.disabled);
+ assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
});
-
- test('_computeCanDeleteVote', () => {
- element.mutable = false;
- const button = element.shadowRoot
- .querySelector('gr-button');
- assert.isTrue(isHidden(button));
- element.change.removable_reviewers = [element.account];
- element.mutable = true;
- assert.isFalse(isHidden(button));
- });
-
- test('deletes votes', () => {
- const deleteResponse = Promise.resolve({ok: true});
- const deleteStub = sandbox.stub(
- element.$.restAPI, 'deleteVote').returns(deleteResponse);
-
- element.change.removable_reviewers = [element.account];
- element.change.labels.test.recommended = {_account_id: 1};
- element.mutable = true;
- const button = element.shadowRoot
- .querySelector('gr-button');
- MockInteractions.tap(button);
- assert.isTrue(button.disabled);
- return deleteResponse.then(() => {
- assert.isFalse(button.disabled);
- assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
- });
- });
- });
-
- suite('label color and order', () => {
- test('valueless label rejected', () => {
- element.labelInfo = {rejected: {name: 'someone'}};
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('negative'));
- });
-
- test('valueless label approved', () => {
- element.labelInfo = {approved: {name: 'someone'}};
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('positive'));
- });
-
- test('-2 to +2', () => {
- element.labelInfo = {
- all: [
- {value: 2, name: 'user 2'},
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 3'},
- {value: -2, name: 'user 4'},
- ],
- values: {
- '-2': 'Awful',
- '-1': 'Don\'t submit as-is',
- ' 0': 'No score',
- '+1': 'Looks good to me',
- '+2': 'Ready to submit',
- },
- };
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- assert.isTrue(labels[2].classList.contains('negative'));
- assert.isTrue(labels[3].classList.contains('min'));
- });
-
- test('-1 to +1', () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 2'},
- ],
- values: {
- '-1': 'Don\'t submit as-is',
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('min'));
- });
-
- test('0 to +2', () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 2'},
- {value: 2, name: 'user '},
- ],
- values: {
- ' 0': 'Don\'t submit as-is',
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
- };
- flushAsynchronousOperations();
- const labels = Polymer.dom(element.root).querySelectorAll('gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- });
-
- test('self votes at top', () => {
- element.account = {
- _account_id: 1,
- name: 'bojack',
- };
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 1', _account_id: 2},
- {value: -1, name: 'bojack', _account_id: 1},
- ],
- values: {
- '-1': 'Don\'t submit as-is',
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- flushAsynchronousOperations();
- const chips =
- Polymer.dom(element.root).querySelectorAll('gr-account-chip');
- assert.equal(chips[0].account._account_id, element.account._account_id);
- });
- });
-
- test('_computeValueTooltip', () => {
- // Existing label.
- let labelInfo = {values: {0: 'Baz'}};
- let score = '0';
- assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz');
-
- // Non-exsistent score.
- score = '2';
- assert.equal(element._computeValueTooltip(labelInfo, score), '');
-
- // No values on label.
- labelInfo = {values: {}};
- score = '0';
- assert.equal(element._computeValueTooltip(labelInfo, score), '');
- });
-
- test('placeholder', () => {
- element.labelInfo = {};
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.placeholder')));
- element.labelInfo = {all: []};
- assert.isFalse(isHidden(element.shadowRoot
- .querySelector('.placeholder')));
- element.labelInfo = {all: [{value: 1}]};
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('.placeholder')));
});
});
+
+ suite('label color and order', () => {
+ test('valueless label rejected', () => {
+ element.labelInfo = {rejected: {name: 'someone'}};
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('negative'));
+ });
+
+ test('valueless label approved', () => {
+ element.labelInfo = {approved: {name: 'someone'}};
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('positive'));
+ });
+
+ test('-2 to +2', () => {
+ element.labelInfo = {
+ all: [
+ {value: 2, name: 'user 2'},
+ {value: 1, name: 'user 1'},
+ {value: -1, name: 'user 3'},
+ {value: -2, name: 'user 4'},
+ ],
+ values: {
+ '-2': 'Awful',
+ '-1': 'Don\'t submit as-is',
+ ' 0': 'No score',
+ '+1': 'Looks good to me',
+ '+2': 'Ready to submit',
+ },
+ };
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('max'));
+ assert.isTrue(labels[1].classList.contains('positive'));
+ assert.isTrue(labels[2].classList.contains('negative'));
+ assert.isTrue(labels[3].classList.contains('min'));
+ });
+
+ test('-1 to +1', () => {
+ element.labelInfo = {
+ all: [
+ {value: 1, name: 'user 1'},
+ {value: -1, name: 'user 2'},
+ ],
+ values: {
+ '-1': 'Don\'t submit as-is',
+ ' 0': 'No score',
+ '+1': 'Looks good to me',
+ },
+ };
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('max'));
+ assert.isTrue(labels[1].classList.contains('min'));
+ });
+
+ test('0 to +2', () => {
+ element.labelInfo = {
+ all: [
+ {value: 1, name: 'user 2'},
+ {value: 2, name: 'user '},
+ ],
+ values: {
+ ' 0': 'Don\'t submit as-is',
+ '+1': 'No score',
+ '+2': 'Looks good to me',
+ },
+ };
+ flushAsynchronousOperations();
+ const labels = dom(element.root).querySelectorAll('gr-label');
+ assert.isTrue(labels[0].classList.contains('max'));
+ assert.isTrue(labels[1].classList.contains('positive'));
+ });
+
+ test('self votes at top', () => {
+ element.account = {
+ _account_id: 1,
+ name: 'bojack',
+ };
+ element.labelInfo = {
+ all: [
+ {value: 1, name: 'user 1', _account_id: 2},
+ {value: -1, name: 'bojack', _account_id: 1},
+ ],
+ values: {
+ '-1': 'Don\'t submit as-is',
+ ' 0': 'No score',
+ '+1': 'Looks good to me',
+ },
+ };
+ flushAsynchronousOperations();
+ const chips =
+ dom(element.root).querySelectorAll('gr-account-chip');
+ assert.equal(chips[0].account._account_id, element.account._account_id);
+ });
+ });
+
+ test('_computeValueTooltip', () => {
+ // Existing label.
+ let labelInfo = {values: {0: 'Baz'}};
+ let score = '0';
+ assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz');
+
+ // Non-exsistent score.
+ score = '2';
+ assert.equal(element._computeValueTooltip(labelInfo, score), '');
+
+ // No values on label.
+ labelInfo = {values: {}};
+ score = '0';
+ assert.equal(element._computeValueTooltip(labelInfo, score), '');
+ });
+
+ test('placeholder', () => {
+ element.labelInfo = {};
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ element.labelInfo = {all: []};
+ assert.isFalse(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ element.labelInfo = {all: [{value: 1}]};
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
index b594757..c797919 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -14,20 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrLabel extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-label'; }
- }
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.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-label_html.js';
- customElements.define(GrLabel.is, GrLabel);
-})();
+/**
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrLabel extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-label'; }
+}
+
+customElements.define(GrLabel.is, GrLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
index c1c9b23..1644c07 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
@@ -1,24 +1,21 @@
-<!--
-@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-tooltip-behavior/gr-tooltip-behavior.html">
-<dom-module id="gr-label">
- <template>
+export const htmlTemplate = html`
<slot></slot>
- </template>
- <script src="gr-label.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index cb5ad7c..f585347 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -14,69 +14,76 @@
* 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 GrLabeledAutocomplete extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-labeled-autocomplete'; }
- /**
- * Fired when a value is chosen.
- *
- * @event commit
- */
+import '../gr-autocomplete/gr-autocomplete.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-labeled-autocomplete_html.js';
- static get properties() {
- return {
+/** @extends Polymer.Element */
+class GrLabeledAutocomplete extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /**
- * Used just like the query property of gr-autocomplete.
- *
- * @type {function(string): Promise<?>}
- */
- query: {
- type: Function,
- value() {
- return function() {
- return Promise.resolve([]);
- };
- },
+ static get is() { return 'gr-labeled-autocomplete'; }
+ /**
+ * Fired when a value is chosen.
+ *
+ * @event commit
+ */
+
+ static get properties() {
+ return {
+
+ /**
+ * Used just like the query property of gr-autocomplete.
+ *
+ * @type {function(string): Promise<?>}
+ */
+ query: {
+ type: Function,
+ value() {
+ return function() {
+ return Promise.resolve([]);
+ };
},
+ },
- text: {
- type: String,
- value: '',
- notify: true,
- },
- label: String,
- placeholder: String,
- disabled: Boolean,
+ text: {
+ type: String,
+ value: '',
+ notify: true,
+ },
+ label: String,
+ placeholder: String,
+ disabled: Boolean,
- _autocompleteThreshold: {
- type: Number,
- value: 0,
- readOnly: true,
- },
- };
- }
-
- _handleTriggerClick(e) {
- // Stop propagation here so we don't confuse gr-autocomplete, which
- // listens for taps on body to try to determine when it's blurred.
- e.stopPropagation();
- this.$.autocomplete.focus();
- }
-
- setText(text) {
- this.$.autocomplete.setText(text);
- }
-
- clear() {
- this.setText('');
- }
+ _autocompleteThreshold: {
+ type: Number,
+ value: 0,
+ readOnly: true,
+ },
+ };
}
- customElements.define(GrLabeledAutocomplete.is, GrLabeledAutocomplete);
-})();
+ _handleTriggerClick(e) {
+ // Stop propagation here so we don't confuse gr-autocomplete, which
+ // listens for taps on body to try to determine when it's blurred.
+ e.stopPropagation();
+ this.$.autocomplete.focus();
+ }
+
+ setText(text) {
+ this.$.autocomplete.setText(text);
+ }
+
+ clear() {
+ this.setText('');
+ }
+}
+
+customElements.define(GrLabeledAutocomplete.is, GrLabeledAutocomplete);
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js
index 47be6f7..fe0b03c 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_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="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-labeled-autocomplete">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -50,16 +47,8 @@
<div id="container">
<div id="header">[[label]]</div>
<div id="body">
- <gr-autocomplete
- id="autocomplete"
- threshold="[[_autocompleteThreshold]]"
- query="[[query]]"
- disabled="[[disabled]]"
- placeholder="[[placeholder]]"
- borderless></gr-autocomplete>
+ <gr-autocomplete id="autocomplete" threshold="[[_autocompleteThreshold]]" query="[[query]]" disabled="[[disabled]]" placeholder="[[placeholder]]" borderless=""></gr-autocomplete>
<div id="trigger" on-click="_handleTriggerClick">▼</div>
</div>
</div>
- </template>
- <script src="gr-labeled-autocomplete.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
index e79e741..cd58932 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_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-labeled-autocomplete</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-labeled-autocomplete.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-labeled-autocomplete.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-labeled-autocomplete.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,32 +39,34 @@
</template>
</test-fixture>
-<script>
- suite('gr-labeled-autocomplete 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-labeled-autocomplete.js';
+suite('gr-labeled-autocomplete tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => { sandbox.restore(); });
-
- test('tapping trigger focuses autocomplete', () => {
- const e = {stopPropagation: () => undefined};
- sandbox.stub(e, 'stopPropagation');
- sandbox.stub(element.$.autocomplete, 'focus');
- element._handleTriggerClick(e);
- assert.isTrue(e.stopPropagation.calledOnce);
- assert.isTrue(element.$.autocomplete.focus.calledOnce);
- });
-
- test('setText', () => {
- sandbox.stub(element.$.autocomplete, 'setText');
- element.setText('foo-bar');
- assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar'));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => { sandbox.restore(); });
+
+ test('tapping trigger focuses autocomplete', () => {
+ const e = {stopPropagation: () => undefined};
+ sandbox.stub(e, 'stopPropagation');
+ sandbox.stub(element.$.autocomplete, 'focus');
+ element._handleTriggerClick(e);
+ assert.isTrue(e.stopPropagation.calledOnce);
+ assert.isTrue(element.$.autocomplete.focus.calledOnce);
+ });
+
+ test('setText', () => {
+ sandbox.stub(element.$.autocomplete, 'setText');
+ element.setText('foo-bar');
+ assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index 81126ec..9bd8a11 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -14,155 +14,163 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
- const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
+import '../gr-js-api-interface/gr-js-api-interface.js';
+import {importHref} from '../../../scripts/import-href.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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-lib-loader_html.js';
- /** @extends Polymer.Element */
- class GrLibLoader extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-lib-loader'; }
+const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
+const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
- static get properties() {
- return {
- _hljsState: {
- type: Object,
+/** @extends Polymer.Element */
+class GrLibLoader extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- // NOTE: intended singleton.
- value: {
- configured: false,
- loading: false,
- callbacks: [],
- },
+ static get is() { return 'gr-lib-loader'; }
+
+ static get properties() {
+ return {
+ _hljsState: {
+ type: Object,
+
+ // NOTE: intended singleton.
+ value: {
+ configured: false,
+ loading: false,
+ callbacks: [],
},
- };
- }
-
- /**
- * Get the HLJS library. Returns a promise that resolves with a reference to
- * the library after it's been loaded. The promise resolves immediately if
- * it's already been loaded.
- *
- * @return {!Promise<Object>}
- */
- getHLJS() {
- return new Promise((resolve, reject) => {
- // If the lib is totally loaded, resolve immediately.
- if (this._getHighlightLib()) {
- resolve(this._getHighlightLib());
- return;
- }
-
- // If the library is not currently being loaded, then start loading it.
- if (!this._hljsState.loading) {
- this._hljsState.loading = true;
- this._loadScript(this._getHLJSUrl())
- .then(this._onHLJSLibLoaded.bind(this))
- .catch(reject);
- }
-
- this._hljsState.callbacks.push(resolve);
- });
- }
-
- /**
- * Loads the dark theme document. Returns a promise that resolves with a
- * custom-style DOM element.
- *
- * @return {!Promise<Element>}
- * @suppress {checkTypes}
- */
- getDarkTheme() {
- return new Promise((resolve, reject) => {
- Polymer.importHref(
- this._getLibRoot() + DARK_THEME_PATH, () => {
- const module = document.createElement('style');
- module.setAttribute('include', 'dark-theme');
- const cs = document.createElement('custom-style');
- cs.appendChild(module);
-
- resolve(cs);
- },
- reject);
- });
- }
-
- /**
- * Execute callbacks awaiting the HLJS lib load.
- */
- _onHLJSLibLoaded() {
- const lib = this._getHighlightLib();
- this._hljsState.loading = false;
- this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, {
- hljs: lib,
- });
- for (const cb of this._hljsState.callbacks) {
- cb(lib);
- }
- this._hljsState.callbacks = [];
- }
-
- /**
- * Get the HLJS library, assuming it has been loaded. Configure the library
- * if it hasn't already been configured.
- *
- * @return {!Object}
- */
- _getHighlightLib() {
- const lib = window.hljs;
- if (lib && !this._hljsState.configured) {
- this._hljsState.configured = true;
-
- lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
- }
- return lib;
- }
-
- /**
- * Get the resource path used to load the application. If the application
- * was loaded through a CDN, then this will be the path to CDN resources.
- *
- * @return {string}
- */
- _getLibRoot() {
- if (window.STATIC_RESOURCE_PATH) {
- return window.STATIC_RESOURCE_PATH + '/';
- }
- return '/';
- }
-
- /**
- * Load and execute a JS file from the lib root.
- *
- * @param {string} src The path to the JS file without the lib root.
- * @return {Promise} a promise that resolves when the script's onload
- * executes.
- */
- _loadScript(src) {
- return new Promise((resolve, reject) => {
- const script = document.createElement('script');
-
- if (!src) {
- reject(new Error('Unable to load blank script url.'));
- return;
- }
-
- script.setAttribute('src', src);
- script.onload = resolve;
- script.onerror = reject;
- Polymer.dom(document.head).appendChild(script);
- });
- }
-
- _getHLJSUrl() {
- const root = this._getLibRoot();
- if (!root) { return null; }
- return root + HLJS_PATH;
- }
+ },
+ };
}
- customElements.define(GrLibLoader.is, GrLibLoader);
-})();
+ /**
+ * Get the HLJS library. Returns a promise that resolves with a reference to
+ * the library after it's been loaded. The promise resolves immediately if
+ * it's already been loaded.
+ *
+ * @return {!Promise<Object>}
+ */
+ getHLJS() {
+ return new Promise((resolve, reject) => {
+ // If the lib is totally loaded, resolve immediately.
+ if (this._getHighlightLib()) {
+ resolve(this._getHighlightLib());
+ return;
+ }
+
+ // If the library is not currently being loaded, then start loading it.
+ if (!this._hljsState.loading) {
+ this._hljsState.loading = true;
+ this._loadScript(this._getHLJSUrl())
+ .then(this._onHLJSLibLoaded.bind(this))
+ .catch(reject);
+ }
+
+ this._hljsState.callbacks.push(resolve);
+ });
+ }
+
+ /**
+ * Loads the dark theme document. Returns a promise that resolves with a
+ * custom-style DOM element.
+ *
+ * @return {!Promise<Element>}
+ * @suppress {checkTypes}
+ */
+ getDarkTheme() {
+ return new Promise((resolve, reject) => {
+ importHref(
+ this._getLibRoot() + DARK_THEME_PATH, () => {
+ const module = document.createElement('style');
+ module.setAttribute('include', 'dark-theme');
+ const cs = document.createElement('custom-style');
+ cs.appendChild(module);
+
+ resolve(cs);
+ },
+ reject);
+ });
+ }
+
+ /**
+ * Execute callbacks awaiting the HLJS lib load.
+ */
+ _onHLJSLibLoaded() {
+ const lib = this._getHighlightLib();
+ this._hljsState.loading = false;
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, {
+ hljs: lib,
+ });
+ for (const cb of this._hljsState.callbacks) {
+ cb(lib);
+ }
+ this._hljsState.callbacks = [];
+ }
+
+ /**
+ * Get the HLJS library, assuming it has been loaded. Configure the library
+ * if it hasn't already been configured.
+ *
+ * @return {!Object}
+ */
+ _getHighlightLib() {
+ const lib = window.hljs;
+ if (lib && !this._hljsState.configured) {
+ this._hljsState.configured = true;
+
+ lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
+ }
+ return lib;
+ }
+
+ /**
+ * Get the resource path used to load the application. If the application
+ * was loaded through a CDN, then this will be the path to CDN resources.
+ *
+ * @return {string}
+ */
+ _getLibRoot() {
+ if (window.STATIC_RESOURCE_PATH) {
+ return window.STATIC_RESOURCE_PATH + '/';
+ }
+ return '/';
+ }
+
+ /**
+ * Load and execute a JS file from the lib root.
+ *
+ * @param {string} src The path to the JS file without the lib root.
+ * @return {Promise} a promise that resolves when the script's onload
+ * executes.
+ */
+ _loadScript(src) {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+
+ if (!src) {
+ reject(new Error('Unable to load blank script url.'));
+ return;
+ }
+
+ script.setAttribute('src', src);
+ script.onload = resolve;
+ script.onerror = reject;
+ dom(document.head).appendChild(script);
+ });
+ }
+
+ _getHLJSUrl() {
+ const root = this._getLibRoot();
+ if (!root) { return null; }
+ return root + HLJS_PATH;
+ }
+}
+
+customElements.define(GrLibLoader.is, GrLibLoader);
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
index fb55c67..3bc0d72 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
@@ -1,25 +1,21 @@
-<!--
-@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="../../shared/gr-js-api-interface/gr-js-api-interface.html">
-
-<dom-module id="gr-lib-loader">
- <template>
+export const htmlTemplate = html`
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
- </template>
- <script src="gr-lib-loader.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
index a1d9c0f..1f726b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_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-lib-loader</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-lib-loader.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-lib-loader.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-lib-loader.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,117 +40,119 @@
</template>
</test-fixture>
-<script>
- suite('gr-lib-loader tests', async () => {
- await readyToTest();
- let sandbox;
- let element;
- let resolveLoad;
- let loadStub;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-lib-loader.js';
+suite('gr-lib-loader tests', () => {
+ let sandbox;
+ let element;
+ let resolveLoad;
+ let loadStub;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+
+ loadStub = sandbox.stub(element, '_loadScript', () =>
+ new Promise(resolve => resolveLoad = resolve)
+ );
+
+ // Assert preconditions:
+ assert.isFalse(element._hljsState.loading);
+ });
+
+ teardown(() => {
+ if (window.hljs) {
+ delete window.hljs;
+ }
+ sandbox.restore();
+
+ // Because the element state is a singleton, clean it up.
+ element._hljsState.configured = false;
+ element._hljsState.loading = false;
+ element._hljsState.callbacks = [];
+ });
+
+ test('only load once', done => {
+ sandbox.stub(element, '_getHLJSUrl').returns('');
+ const firstCallHandler = sinon.stub();
+ element.getHLJS().then(firstCallHandler);
+
+ // It should now be in the loading state.
+ assert.isTrue(loadStub.called);
+ assert.isTrue(element._hljsState.loading);
+ assert.isFalse(firstCallHandler.called);
+
+ const secondCallHandler = sinon.stub();
+ element.getHLJS().then(secondCallHandler);
+
+ // No change in state.
+ assert.isTrue(element._hljsState.loading);
+ assert.isFalse(firstCallHandler.called);
+ assert.isFalse(secondCallHandler.called);
+
+ // Now load the library.
+ resolveLoad();
+ flush(() => {
+ // The state should be loaded and both handlers called.
+ assert.isFalse(element._hljsState.loading);
+ assert.isTrue(firstCallHandler.called);
+ assert.isTrue(secondCallHandler.called);
+ done();
+ });
+ });
+
+ suite('preloaded', () => {
+ let hljsStub;
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
-
- loadStub = sandbox.stub(element, '_loadScript', () =>
- new Promise(resolve => resolveLoad = resolve)
- );
-
- // Assert preconditions:
- assert.isFalse(element._hljsState.loading);
+ hljsStub = {
+ configure: sinon.stub(),
+ };
+ window.hljs = hljsStub;
});
teardown(() => {
- if (window.hljs) {
- delete window.hljs;
- }
- sandbox.restore();
-
- // Because the element state is a singleton, clean it up.
- element._hljsState.configured = false;
- element._hljsState.loading = false;
- element._hljsState.callbacks = [];
+ delete window.hljs;
});
- test('only load once', done => {
- sandbox.stub(element, '_getHLJSUrl').returns('');
+ test('returns hljs', done => {
const firstCallHandler = sinon.stub();
element.getHLJS().then(firstCallHandler);
-
- // It should now be in the loading state.
- assert.isTrue(loadStub.called);
- assert.isTrue(element._hljsState.loading);
- assert.isFalse(firstCallHandler.called);
-
- const secondCallHandler = sinon.stub();
- element.getHLJS().then(secondCallHandler);
-
- // No change in state.
- assert.isTrue(element._hljsState.loading);
- assert.isFalse(firstCallHandler.called);
- assert.isFalse(secondCallHandler.called);
-
- // Now load the library.
- resolveLoad();
flush(() => {
- // The state should be loaded and both handlers called.
- assert.isFalse(element._hljsState.loading);
assert.isTrue(firstCallHandler.called);
- assert.isTrue(secondCallHandler.called);
+ assert.isTrue(firstCallHandler.calledWith(hljsStub));
done();
});
});
- suite('preloaded', () => {
- let hljsStub;
-
- setup(() => {
- hljsStub = {
- configure: sinon.stub(),
- };
- window.hljs = hljsStub;
- });
-
- teardown(() => {
- delete window.hljs;
- });
-
- test('returns hljs', done => {
- const firstCallHandler = sinon.stub();
- element.getHLJS().then(firstCallHandler);
- flush(() => {
- assert.isTrue(firstCallHandler.called);
- assert.isTrue(firstCallHandler.calledWith(hljsStub));
- done();
- });
- });
-
- test('configures hljs', done => {
- element.getHLJS().then(() => {
- assert.isTrue(window.hljs.configure.calledOnce);
- done();
- });
- });
- });
-
- suite('_getHLJSUrl', () => {
- suite('checking _getLibRoot', () => {
- let root;
-
- setup(() => {
- sandbox.stub(element, '_getLibRoot', () => root);
- });
-
- test('with no root', () => {
- assert.isNull(element._getHLJSUrl());
- });
-
- test('with root', () => {
- root = 'test-root.com/';
- assert.equal(element._getHLJSUrl(),
- 'test-root.com/bower_components/highlightjs/highlight.min.js');
- });
+ test('configures hljs', done => {
+ element.getHLJS().then(() => {
+ assert.isTrue(window.hljs.configure.calledOnce);
+ done();
});
});
});
+
+ suite('_getHLJSUrl', () => {
+ suite('checking _getLibRoot', () => {
+ let root;
+
+ setup(() => {
+ sandbox.stub(element, '_getLibRoot', () => root);
+ });
+
+ test('with no root', () => {
+ assert.isNull(element._getHLJSUrl());
+ });
+
+ test('with root', () => {
+ root = 'test-root.com/';
+ assert.equal(element._getHLJSUrl(),
+ 'test-root.com/bower_components/highlightjs/highlight.min.js');
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
index ee032f6..d47dbbc 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -14,92 +14,99 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.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-limited-text_html.js';
+
+/**
+ * The gr-limited-text element is for displaying text with a maximum length
+ * (in number of characters) to display. If the length of the text exceeds the
+ * configured limit, then an ellipsis indicates that the text was truncated
+ * and a tooltip containing the full text is enabled.
+ *
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrLimitedText extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-limited-text'; }
+
+ static get properties() {
+ return {
+ /** The un-truncated text to display. */
+ text: String,
+
+ /** The maximum length for the text to display before truncating. */
+ limit: {
+ type: Number,
+ value: null,
+ },
+
+ /** Boolean property used by Gerrit.TooltipBehavior. */
+ hasTooltip: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * Disable the tooltip.
+ * When set to true, will not show tooltip even text is over limit
+ */
+ disableTooltip: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * The maximum number of characters to display in the tooltop.
+ */
+ tooltipLimit: {
+ type: Number,
+ value: 1024,
+ },
+ };
+ }
+
+ static get observers() {
+ return [
+ '_updateTitle(text, limit, tooltipLimit)',
+ ];
+ }
/**
- * The gr-limited-text element is for displaying text with a maximum length
- * (in number of characters) to display. If the length of the text exceeds the
- * configured limit, then an ellipsis indicates that the text was truncated
- * and a tooltip containing the full text is enabled.
- *
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
+ * The text or limit have changed. Recompute whether a tooltip needs to be
+ * enabled.
*/
- class GrLimitedText extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-limited-text'; }
-
- static get properties() {
- return {
- /** The un-truncated text to display. */
- text: String,
-
- /** The maximum length for the text to display before truncating. */
- limit: {
- type: Number,
- value: null,
- },
-
- /** Boolean property used by Gerrit.TooltipBehavior. */
- hasTooltip: {
- type: Boolean,
- value: false,
- },
-
- /**
- * Disable the tooltip.
- * When set to true, will not show tooltip even text is over limit
- */
- disableTooltip: {
- type: Boolean,
- value: false,
- },
-
- /**
- * The maximum number of characters to display in the tooltop.
- */
- tooltipLimit: {
- type: Number,
- value: 1024,
- },
- };
+ _updateTitle(text, limit, tooltipLimit) {
+ // Polymer 2: check for undefined
+ if ([text, limit, tooltipLimit].some(arg => arg === undefined)) {
+ return;
}
- static get observers() {
- return [
- '_updateTitle(text, limit, tooltipLimit)',
- ];
- }
-
- /**
- * The text or limit have changed. Recompute whether a tooltip needs to be
- * enabled.
- */
- _updateTitle(text, limit, tooltipLimit) {
- // Polymer 2: check for undefined
- if ([text, limit, tooltipLimit].some(arg => arg === undefined)) {
- return;
- }
-
- this.hasTooltip = !!limit && !!text && text.length > limit;
- if (this.hasTooltip && !this.disableTooltip) {
- this.setAttribute('title', text.substr(0, tooltipLimit));
- } else {
- this.removeAttribute('title');
- }
- }
-
- _computeDisplayText(text, limit) {
- if (!!limit && !!text && text.length > limit) {
- return text.substr(0, limit - 1) + '…';
- }
- return text;
+ this.hasTooltip = !!limit && !!text && text.length > limit;
+ if (this.hasTooltip && !this.disableTooltip) {
+ this.setAttribute('title', text.substr(0, tooltipLimit));
+ } else {
+ this.removeAttribute('title');
}
}
- customElements.define(GrLimitedText.is, GrLimitedText);
-})();
+ _computeDisplayText(text, limit) {
+ if (!!limit && !!text && text.length > limit) {
+ return text.substr(0, limit - 1) + '…';
+ }
+ return text;
+ }
+}
+
+customElements.define(GrLimitedText.is, GrLimitedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
index d00416b..c14f9f9 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
@@ -1,24 +1,21 @@
-<!--
-@license
-Copyright (C) 2017 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-tooltip-behavior/gr-tooltip-behavior.html">
-
-<dom-module id="gr-limited-text">
- <template>[[_computeDisplayText(text, limit)]]</template>
- <script src="gr-limited-text.js"></script>
-</dom-module>
+export const htmlTemplate = html`
+[[_computeDisplayText(text, limit)]]
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
index c34a348..8240cbf 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-limited-text</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>
-<link rel="import" href="gr-limited-text.html">
+<script type="module" src="./gr-limited-text.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-limited-text.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,72 +41,74 @@
</template>
</test-fixture>
-<script>
- suite('gr-limited-text 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-limited-text.js';
+suite('gr-limited-text tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('_updateTitle', () => {
- const updateSpy = sandbox.spy(element, '_updateTitle');
- element.text = 'abc 123';
- flushAsynchronousOperations();
- assert.isTrue(updateSpy.calledOnce);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
-
- element.limit = 10;
- flushAsynchronousOperations();
- assert.isTrue(updateSpy.calledTwice);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
-
- element.limit = 3;
- flushAsynchronousOperations();
- assert.isTrue(updateSpy.calledThrice);
- assert.equal(element.getAttribute('title'), 'abc 123');
- assert.isTrue(element.hasTooltip);
-
- element.tooltipLimit = 3;
- flushAsynchronousOperations();
- assert.equal(element.getAttribute('title'), 'abc');
-
- element.tooltipLimit = 1024;
- element.limit = 100;
- flushAsynchronousOperations();
- assert.equal(updateSpy.callCount, 6);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
-
- element.limit = null;
- flushAsynchronousOperations();
- assert.equal(updateSpy.callCount, 7);
- assert.isNotOk(element.getAttribute('title'));
- assert.isFalse(element.hasTooltip);
- });
-
- test('_computeDisplayText', () => {
- assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
- assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
- assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
- });
-
- test('when disable tooltip', () => {
- sandbox.spy(element, '_updateTitle');
- element.text = 'abcdefghijklmn';
- element.disableTooltip = true;
- element.limit = 10;
- flushAsynchronousOperations();
- assert.equal(element.getAttribute('title'), null);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_updateTitle', () => {
+ const updateSpy = sandbox.spy(element, '_updateTitle');
+ element.text = 'abc 123';
+ flushAsynchronousOperations();
+ assert.isTrue(updateSpy.calledOnce);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+
+ element.limit = 10;
+ flushAsynchronousOperations();
+ assert.isTrue(updateSpy.calledTwice);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+
+ element.limit = 3;
+ flushAsynchronousOperations();
+ assert.isTrue(updateSpy.calledThrice);
+ assert.equal(element.getAttribute('title'), 'abc 123');
+ assert.isTrue(element.hasTooltip);
+
+ element.tooltipLimit = 3;
+ flushAsynchronousOperations();
+ assert.equal(element.getAttribute('title'), 'abc');
+
+ element.tooltipLimit = 1024;
+ element.limit = 100;
+ flushAsynchronousOperations();
+ assert.equal(updateSpy.callCount, 6);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+
+ element.limit = null;
+ flushAsynchronousOperations();
+ assert.equal(updateSpy.callCount, 7);
+ assert.isNotOk(element.getAttribute('title'));
+ assert.isFalse(element.hasTooltip);
+ });
+
+ test('_computeDisplayText', () => {
+ assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
+ assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
+ assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
+ });
+
+ test('when disable tooltip', () => {
+ sandbox.spy(element, '_updateTitle');
+ element.text = 'abcdefghijklmn';
+ element.disableTooltip = true;
+ element.limit = 10;
+ flushAsynchronousOperations();
+ assert.equal(element.getAttribute('title'), null);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
index ccab685..2957c9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -14,52 +14,64 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrLinkedChip extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-linked-chip'; }
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import '../gr-button/gr-button.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-limited-text/gr-limited-text.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-linked-chip_html.js';
- static get properties() {
- return {
- href: String,
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- removable: {
- type: Boolean,
- value: false,
- },
- text: String,
- transparentBackground: {
- type: Boolean,
- value: false,
- },
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrLinkedChip extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** If provided, sets the maximum length of the content. */
- limit: Number,
- };
- }
+ static get is() { return 'gr-linked-chip'; }
- _getBackgroundClass(transparent) {
- return transparent ? 'transparentBackground' : '';
- }
+ static get properties() {
+ return {
+ href: String,
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ removable: {
+ type: Boolean,
+ value: false,
+ },
+ text: String,
+ transparentBackground: {
+ type: Boolean,
+ value: false,
+ },
- _handleRemoveTap(e) {
- e.preventDefault();
- this.fire('remove');
- }
+ /** If provided, sets the maximum length of the content. */
+ limit: Number,
+ };
}
- customElements.define(GrLinkedChip.is, GrLinkedChip);
-})();
+ _getBackgroundClass(transparent) {
+ return transparent ? 'transparentBackground' : '';
+ }
+
+ _handleRemoveTap(e) {
+ e.preventDefault();
+ this.fire('remove');
+ }
+}
+
+customElements.define(GrLinkedChip.is, GrLinkedChip);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
index 844b8be..c028d02 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
@@ -1,30 +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/gr-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="../gr-button/gr-button.html">
-<link rel="import" href="../gr-icons/gr-icons.html">
-<link rel="import" href="../gr-limited-text/gr-limited-text.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-linked-chip">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -78,22 +70,12 @@
width: 1.2rem;
}
</style>
- <div class$="container [[_getBackgroundClass(transparentBackground)]]">
- <a href$="[[href]]">
- <gr-limited-text
- limit="[[limit]]"
- text="[[text]]"></gr-limited-text>
+ <div class\$="container [[_getBackgroundClass(transparentBackground)]]">
+ <a href\$="[[href]]">
+ <gr-limited-text limit="[[limit]]" text="[[text]]"></gr-limited-text>
</a>
- <gr-button
- id="remove"
- link
- hidden$="[[!removable]]"
- hidden
- class$="remove [[_getBackgroundClass(transparentBackground)]]"
- on-click="_handleRemoveTap">
+ <gr-button id="remove" link="" hidden\$="[[!removable]]" hidden="" class\$="remove [[_getBackgroundClass(transparentBackground)]]" on-click="_handleRemoveTap">
<iron-icon icon="gr-icons:close"></iron-icon>
</gr-button>
</div>
- </template>
- <script src="gr-linked-chip.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
index 2bc7cfa..af8d217 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -19,12 +19,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-linked-chip</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>
<!-- Can't use absolute path below for mock-interaction.js.
Web component tester(wct) has a built-in http server and it serves "/components" directory (which is
actually /node_modules directory). Also, wct patches some files to load modules from /components.
@@ -33,9 +33,14 @@
-->
<script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script>
-<link rel="import" href="gr-linked-chip.html">
+<script type="module" src="./gr-linked-chip.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-linked-chip.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -43,27 +48,29 @@
</template>
</test-fixture>
-<script>
- suite('gr-linked-chip 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-linked-chip.js';
+suite('gr-linked-chip tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('remove fired', () => {
- const spy = sandbox.spy();
- element.addEventListener('remove', spy);
- flushAsynchronousOperations();
- MockInteractions.tap(element.$.remove);
- assert.isTrue(spy.called);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('remove fired', () => {
+ const spy = sandbox.spy();
+ element.addEventListener('remove', spy);
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.$.remove);
+ assert.isTrue(spy.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index f970734..07ce424 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.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,110 +14,121 @@
* 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 GrLinkedText extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-linked-text'; }
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-navigation/gr-navigation.js';
+import './link-text-parser.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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 'ba-linkify/ba-linkify.js';
+import {htmlTemplate} from './gr-linked-text_html.js';
- static get properties() {
- return {
- removeZeroWidthSpace: Boolean,
- content: {
- type: String,
- observer: '_contentChanged',
- },
- pre: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
- config: Object,
- };
- }
+/** @extends Polymer.Element */
+class GrLinkedText extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- static get observers() {
- return [
- '_contentOrConfigChanged(content, config)',
- ];
- }
+ static get is() { return 'gr-linked-text'; }
- _contentChanged(content) {
- // In the case where the config may not be set (perhaps due to the
- // request for it still being in flight), set the content anyway to
- // prevent waiting on the config to display the text.
- if (this.config != null) { return; }
- this.$.output.textContent = content;
- }
-
- /**
- * Because either the source text or the linkification config has changed,
- * the content should be re-parsed.
- *
- * @param {string|null|undefined} content The raw, un-linkified source
- * string to parse.
- * @param {Object|null|undefined} config The server config specifying
- * commentLink patterns
- */
- _contentOrConfigChanged(content, config) {
- if (!Gerrit.Nav || !Gerrit.Nav.mapCommentlinks) return;
- config = Gerrit.Nav.mapCommentlinks(config);
- const output = Polymer.dom(this.$.output);
- output.textContent = '';
- const parser = new GrLinkTextParser(config,
- this._handleParseResult.bind(this), this.removeZeroWidthSpace);
- parser.parse(content);
-
- // Ensure that external links originating from HTML commentlink configs
- // open in a new tab. @see Issue 5567
- // Ensure links to the same host originating from commentlink configs
- // open in the same tab. When target is not set - default is _self
- // @see Issue 4616
- output.querySelectorAll('a').forEach(anchor => {
- if (anchor.hostname === window.location.hostname) {
- anchor.removeAttribute('target');
- } else {
- anchor.setAttribute('target', '_blank');
- }
- anchor.setAttribute('rel', 'noopener');
- });
- }
-
- /**
- * This method is called when the GrLikTextParser emits a partial result
- * (used as the "callback" parameter). It will be called in either of two
- * ways:
- * - To create a link: when called with `text` and `href` arguments, a link
- * element should be created and attached to the resulting DOM.
- * - To attach an arbitrary fragment: when called with only the `fragment`
- * argument, the fragment should be attached to the resulting DOM as is.
- *
- * @param {string|null} text
- * @param {string|null} href
- * @param {DocumentFragment|undefined} fragment
- */
- _handleParseResult(text, href, fragment) {
- const output = Polymer.dom(this.$.output);
- if (href) {
- const a = document.createElement('a');
- a.href = href;
- a.textContent = text;
- a.target = '_blank';
- a.rel = 'noopener';
- output.appendChild(a);
- } else if (fragment) {
- output.appendChild(fragment);
- }
- }
+ static get properties() {
+ return {
+ removeZeroWidthSpace: Boolean,
+ content: {
+ type: String,
+ observer: '_contentChanged',
+ },
+ pre: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ config: Object,
+ };
}
- customElements.define(GrLinkedText.is, GrLinkedText);
-})();
+ static get observers() {
+ return [
+ '_contentOrConfigChanged(content, config)',
+ ];
+ }
+
+ _contentChanged(content) {
+ // In the case where the config may not be set (perhaps due to the
+ // request for it still being in flight), set the content anyway to
+ // prevent waiting on the config to display the text.
+ if (this.config != null) { return; }
+ this.$.output.textContent = content;
+ }
+
+ /**
+ * Because either the source text or the linkification config has changed,
+ * the content should be re-parsed.
+ *
+ * @param {string|null|undefined} content The raw, un-linkified source
+ * string to parse.
+ * @param {Object|null|undefined} config The server config specifying
+ * commentLink patterns
+ */
+ _contentOrConfigChanged(content, config) {
+ if (!Gerrit.Nav || !Gerrit.Nav.mapCommentlinks) return;
+ config = Gerrit.Nav.mapCommentlinks(config);
+ const output = dom(this.$.output);
+ output.textContent = '';
+ const parser = new GrLinkTextParser(config,
+ this._handleParseResult.bind(this), this.removeZeroWidthSpace);
+ parser.parse(content);
+
+ // Ensure that external links originating from HTML commentlink configs
+ // open in a new tab. @see Issue 5567
+ // Ensure links to the same host originating from commentlink configs
+ // open in the same tab. When target is not set - default is _self
+ // @see Issue 4616
+ output.querySelectorAll('a').forEach(anchor => {
+ if (anchor.hostname === window.location.hostname) {
+ anchor.removeAttribute('target');
+ } else {
+ anchor.setAttribute('target', '_blank');
+ }
+ anchor.setAttribute('rel', 'noopener');
+ });
+ }
+
+ /**
+ * This method is called when the GrLikTextParser emits a partial result
+ * (used as the "callback" parameter). It will be called in either of two
+ * ways:
+ * - To create a link: when called with `text` and `href` arguments, a link
+ * element should be created and attached to the resulting DOM.
+ * - To attach an arbitrary fragment: when called with only the `fragment`
+ * argument, the fragment should be attached to the resulting DOM as is.
+ *
+ * @param {string|null} text
+ * @param {string|null} href
+ * @param {DocumentFragment|undefined} fragment
+ */
+ _handleParseResult(text, href, fragment) {
+ const output = dom(this.$.output);
+ if (href) {
+ const a = document.createElement('a');
+ a.href = href;
+ a.textContent = text;
+ a.target = '_blank';
+ a.rel = 'noopener';
+ output.appendChild(a);
+ } else if (fragment) {
+ output.appendChild(fragment);
+ }
+ }
+}
+
+customElements.define(GrLinkedText.is, GrLinkedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js
index 61facc0..43d7144 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_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="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
-
-<script src="/bower_components/ba-linkify/ba-linkify.js"></script>
-<script src="link-text-parser.js"></script>
-<dom-module id="gr-linked-text">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -39,6 +32,4 @@
}
</style>
<span id="output"></span>
- </template>
- <script src="gr-linked-text.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index 9e373b7..b16acf4 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-linked-text</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-linked-text.html">
+<script type="module" src="./gr-linked-text.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-linked-text.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -39,340 +45,344 @@
</template>
</test-fixture>
-<script>
- suite('gr-linked-text tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-linked-text.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-linked-text tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
- element.config = {
- ph: {
- match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
- link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
- },
- prefixsameinlinkandpattern: {
- match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)',
- link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
- },
- changeid: {
- match: '(I[0-9a-f]{8,40})',
- link: '#/q/$1',
- },
- changeid2: {
- match: 'Change-Id: +(I[0-9a-f]{8,40})',
- link: '#/q/$1',
- },
- googlesearch: {
- match: 'google:(.+)',
- link: 'https://bing.com/search?q=$1', // html should supercede link.
- html: '<a href="https://google.com/search?q=$1">$1</a>',
- },
- hashedhtml: {
- match: 'hash:(.+)',
- html: '<a href="#/awesomesauce">$1</a>',
- },
- baseurl: {
- match: 'test (.+)',
- html: '<a href="/r/awesomesauce">$1</a>',
- },
- anotatstartwithbaseurl: {
- match: 'a test (.+)',
- html: '[Lookup: <a href="/r/awesomesauce">$1</a>]',
- },
- disabledconfig: {
- match: 'foo:(.+)',
- link: 'https://google.com/search?q=$1',
- enabled: false,
- },
- };
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('URL pattern was parsed and linked.', () => {
- // Regular inline link.
- const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
- element.content = url;
- const linkEl = element.$.output.childNodes[0];
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.rel, 'noopener');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, url);
- });
-
- test('Bug pattern was parsed and linked', () => {
- // "Issue/Bug" pattern.
- element.content = 'Issue 3650';
-
- let linkEl = element.$.output.childNodes[0];
- const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, 'Issue 3650');
-
- element.content = 'Bug 3650';
- linkEl = element.$.output.childNodes[0];
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.rel, 'noopener');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, 'Bug 3650');
- });
-
- test('Pattern with same prefix as link was correctly parsed', () => {
- // Pattern starts with the same prefix (`http`) as the url.
- element.content = 'httpexample 3650';
-
- assert.equal(element.$.output.childNodes.length, 1);
- const linkEl = element.$.output.childNodes[0];
- const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
- assert.equal(linkEl.target, '_blank');
- assert.equal(linkEl.href, url);
- assert.equal(linkEl.textContent, 'httpexample 3650');
- });
-
- test('Change-Id pattern was parsed and linked', () => {
- // "Change-Id:" pattern.
- const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- const prefix = 'Change-Id: ';
- element.content = prefix + changeID;
-
- const textNode = element.$.output.childNodes[0];
- const linkEl = element.$.output.childNodes[1];
- assert.equal(textNode.textContent, prefix);
- const url = '/q/' + changeID;
- assert.isFalse(linkEl.hasAttribute('target'));
- // Since url is a path, the host is added automatically.
- assert.isTrue(linkEl.href.endsWith(url));
- assert.equal(linkEl.textContent, changeID);
- });
-
- test('Change-Id pattern was parsed and linked with base url', () => {
- window.CANONICAL_PATH = '/r';
-
- // "Change-Id:" pattern.
- const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- const prefix = 'Change-Id: ';
- element.content = prefix + changeID;
-
- const textNode = element.$.output.childNodes[0];
- const linkEl = element.$.output.childNodes[1];
- assert.equal(textNode.textContent, prefix);
- const url = '/r/q/' + changeID;
- assert.isFalse(linkEl.hasAttribute('target'));
- // Since url is a path, the host is added automatically.
- assert.isTrue(linkEl.href.endsWith(url));
- assert.equal(linkEl.textContent, changeID);
- });
-
- test('Multiple matches', () => {
- element.content = 'Issue 3650\nIssue 3450';
- const linkEl1 = element.$.output.childNodes[0];
- const linkEl2 = element.$.output.childNodes[2];
-
- assert.equal(linkEl1.target, '_blank');
- assert.equal(linkEl1.href,
- 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650');
- assert.equal(linkEl1.textContent, 'Issue 3650');
-
- assert.equal(linkEl2.target, '_blank');
- assert.equal(linkEl2.href,
- 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450');
- assert.equal(linkEl2.textContent, 'Issue 3450');
- });
-
- test('Change-Id pattern parsed before bug pattern', () => {
- // "Change-Id:" pattern.
- const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- const prefix = 'Change-Id: ';
-
- // "Issue/Bug" pattern.
- const bug = 'Issue 3650';
-
- const changeUrl = '/q/' + changeID;
- const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
-
- element.content = prefix + changeID + bug;
-
- const textNode = element.$.output.childNodes[0];
- const changeLinkEl = element.$.output.childNodes[1];
- const bugLinkEl = element.$.output.childNodes[2];
-
- assert.equal(textNode.textContent, prefix);
-
- assert.isFalse(changeLinkEl.hasAttribute('target'));
- assert.isTrue(changeLinkEl.href.endsWith(changeUrl));
- assert.equal(changeLinkEl.textContent, changeID);
-
- assert.equal(bugLinkEl.target, '_blank');
- assert.equal(bugLinkEl.href, bugUrl);
- assert.equal(bugLinkEl.textContent, 'Issue 3650');
- });
-
- test('html field in link config', () => {
- element.content = 'google:do a barrel roll';
- const linkEl = element.$.output.childNodes[0];
- assert.equal(linkEl.getAttribute('href'),
- 'https://google.com/search?q=do a barrel roll');
- assert.equal(linkEl.textContent, 'do a barrel roll');
- });
-
- test('removing hash from links', () => {
- element.content = 'hash:foo';
- const linkEl = element.$.output.childNodes[0];
- assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('html with base url', () => {
- window.CANONICAL_PATH = '/r';
-
- element.content = 'test foo';
- const linkEl = element.$.output.childNodes[0];
- assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('a is not at start', () => {
- window.CANONICAL_PATH = '/r';
-
- element.content = 'a test foo';
- const linkEl = element.$.output.childNodes[1];
- assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('hash html with base url', () => {
- window.CANONICAL_PATH = '/r';
-
- element.content = 'hash:foo';
- const linkEl = element.$.output.childNodes[0];
- assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
- assert.equal(linkEl.textContent, 'foo');
- });
-
- test('disabled config', () => {
- element.content = 'foo:baz';
- assert.equal(element.$.output.innerHTML, 'foo:baz');
- });
-
- test('R=email labels link correctly', () => {
- element.removeZeroWidthSpace = true;
- element.content = 'R=\u200Btest@google.com';
- assert.equal(element.$.output.textContent, 'R=test@google.com');
- assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1);
- });
-
- test('CC=email labels link correctly', () => {
- element.removeZeroWidthSpace = true;
- element.content = 'CC=\u200Btest@google.com';
- assert.equal(element.$.output.textContent, 'CC=test@google.com');
- assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1);
- });
-
- test('only {http,https,mailto} protocols are linkified', () => {
- element.content = 'xx mailto:test@google.com yy';
- let links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
- assert.equal(links[0].innerHTML, 'mailto:test@google.com');
-
- element.content = 'xx http://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'http://google.com');
- assert.equal(links[0].innerHTML, 'http://google.com');
-
- element.content = 'xx https://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'https://google.com');
- assert.equal(links[0].innerHTML, 'https://google.com');
-
- element.content = 'xx ssh://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
-
- element.content = 'xx ftp://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
- });
-
- test('links without leading whitespace are linkified', () => {
- element.content = 'xx abcmailto:test@google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
- let links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
- assert.equal(links[0].innerHTML, 'mailto:test@google.com');
-
- element.content = 'xx defhttp://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'http://google.com');
- assert.equal(links[0].innerHTML, 'http://google.com');
-
- element.content = 'xx qwehttps://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'https://google.com');
- assert.equal(links[0].innerHTML, 'https://google.com');
-
- // Non-latin character
- element.content = 'xx абвhttps://google.com yy';
- assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 1);
- assert.equal(links[0].getAttribute('href'), 'https://google.com');
- assert.equal(links[0].innerHTML, 'https://google.com');
-
- element.content = 'xx ssh://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
-
- element.content = 'xx ftp://google.com yy';
- links = element.$.output.querySelectorAll('a');
- assert.equal(links.length, 0);
- });
-
- test('overlapping links', () => {
- element.config = {
- b1: {
- match: '(B:\\s*)(\\d+)',
- html: '$1<a href="ftp://foo/$2">$2</a>',
- },
- b2: {
- match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)',
- html: '$1<a href="ftp://foo/$2">$2</a>',
- },
- };
- element.content = '- B: 123, 45';
- const links = Polymer.dom(element.root).querySelectorAll('a');
-
- assert.equal(links.length, 2);
- assert.equal(element.shadowRoot
- .querySelector('span').textContent, '- B: 123, 45');
-
- assert.equal(links[0].href, 'ftp://foo/123');
- assert.equal(links[0].textContent, '123');
-
- assert.equal(links[1].href, 'ftp://foo/45');
- assert.equal(links[1].textContent, '45');
- });
-
- test('_contentOrConfigChanged called with config', () => {
- const contentStub = sandbox.stub(element, '_contentChanged');
- const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
- element.content = 'some text';
- assert.isTrue(contentStub.called);
- assert.isTrue(contentConfigStub.called);
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x);
+ element.config = {
+ ph: {
+ match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
+ link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
+ },
+ prefixsameinlinkandpattern: {
+ match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)',
+ link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
+ },
+ changeid: {
+ match: '(I[0-9a-f]{8,40})',
+ link: '#/q/$1',
+ },
+ changeid2: {
+ match: 'Change-Id: +(I[0-9a-f]{8,40})',
+ link: '#/q/$1',
+ },
+ googlesearch: {
+ match: 'google:(.+)',
+ link: 'https://bing.com/search?q=$1', // html should supercede link.
+ html: '<a href="https://google.com/search?q=$1">$1</a>',
+ },
+ hashedhtml: {
+ match: 'hash:(.+)',
+ html: '<a href="#/awesomesauce">$1</a>',
+ },
+ baseurl: {
+ match: 'test (.+)',
+ html: '<a href="/r/awesomesauce">$1</a>',
+ },
+ anotatstartwithbaseurl: {
+ match: 'a test (.+)',
+ html: '[Lookup: <a href="/r/awesomesauce">$1</a>]',
+ },
+ disabledconfig: {
+ match: 'foo:(.+)',
+ link: 'https://google.com/search?q=$1',
+ enabled: false,
+ },
+ };
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('URL pattern was parsed and linked.', () => {
+ // Regular inline link.
+ const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+ element.content = url;
+ const linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.rel, 'noopener');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, url);
+ });
+
+ test('Bug pattern was parsed and linked', () => {
+ // "Issue/Bug" pattern.
+ element.content = 'Issue 3650';
+
+ let linkEl = element.$.output.childNodes[0];
+ const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'Issue 3650');
+
+ element.content = 'Bug 3650';
+ linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.rel, 'noopener');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'Bug 3650');
+ });
+
+ test('Pattern with same prefix as link was correctly parsed', () => {
+ // Pattern starts with the same prefix (`http`) as the url.
+ element.content = 'httpexample 3650';
+
+ assert.equal(element.$.output.childNodes.length, 1);
+ const linkEl = element.$.output.childNodes[0];
+ const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+ assert.equal(linkEl.target, '_blank');
+ assert.equal(linkEl.href, url);
+ assert.equal(linkEl.textContent, 'httpexample 3650');
+ });
+
+ test('Change-Id pattern was parsed and linked', () => {
+ // "Change-Id:" pattern.
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
+ element.content = prefix + changeID;
+
+ const textNode = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[1];
+ assert.equal(textNode.textContent, prefix);
+ const url = '/q/' + changeID;
+ assert.isFalse(linkEl.hasAttribute('target'));
+ // Since url is a path, the host is added automatically.
+ assert.isTrue(linkEl.href.endsWith(url));
+ assert.equal(linkEl.textContent, changeID);
+ });
+
+ test('Change-Id pattern was parsed and linked with base url', () => {
+ window.CANONICAL_PATH = '/r';
+
+ // "Change-Id:" pattern.
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
+ element.content = prefix + changeID;
+
+ const textNode = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[1];
+ assert.equal(textNode.textContent, prefix);
+ const url = '/r/q/' + changeID;
+ assert.isFalse(linkEl.hasAttribute('target'));
+ // Since url is a path, the host is added automatically.
+ assert.isTrue(linkEl.href.endsWith(url));
+ assert.equal(linkEl.textContent, changeID);
+ });
+
+ test('Multiple matches', () => {
+ element.content = 'Issue 3650\nIssue 3450';
+ const linkEl1 = element.$.output.childNodes[0];
+ const linkEl2 = element.$.output.childNodes[2];
+
+ assert.equal(linkEl1.target, '_blank');
+ assert.equal(linkEl1.href,
+ 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650');
+ assert.equal(linkEl1.textContent, 'Issue 3650');
+
+ assert.equal(linkEl2.target, '_blank');
+ assert.equal(linkEl2.href,
+ 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450');
+ assert.equal(linkEl2.textContent, 'Issue 3450');
+ });
+
+ test('Change-Id pattern parsed before bug pattern', () => {
+ // "Change-Id:" pattern.
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
+
+ // "Issue/Bug" pattern.
+ const bug = 'Issue 3650';
+
+ const changeUrl = '/q/' + changeID;
+ const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+
+ element.content = prefix + changeID + bug;
+
+ const textNode = element.$.output.childNodes[0];
+ const changeLinkEl = element.$.output.childNodes[1];
+ const bugLinkEl = element.$.output.childNodes[2];
+
+ assert.equal(textNode.textContent, prefix);
+
+ assert.isFalse(changeLinkEl.hasAttribute('target'));
+ assert.isTrue(changeLinkEl.href.endsWith(changeUrl));
+ assert.equal(changeLinkEl.textContent, changeID);
+
+ assert.equal(bugLinkEl.target, '_blank');
+ assert.equal(bugLinkEl.href, bugUrl);
+ assert.equal(bugLinkEl.textContent, 'Issue 3650');
+ });
+
+ test('html field in link config', () => {
+ element.content = 'google:do a barrel roll';
+ const linkEl = element.$.output.childNodes[0];
+ assert.equal(linkEl.getAttribute('href'),
+ 'https://google.com/search?q=do a barrel roll');
+ assert.equal(linkEl.textContent, 'do a barrel roll');
+ });
+
+ test('removing hash from links', () => {
+ element.content = 'hash:foo';
+ const linkEl = element.$.output.childNodes[0];
+ assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('html with base url', () => {
+ window.CANONICAL_PATH = '/r';
+
+ element.content = 'test foo';
+ const linkEl = element.$.output.childNodes[0];
+ assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('a is not at start', () => {
+ window.CANONICAL_PATH = '/r';
+
+ element.content = 'a test foo';
+ const linkEl = element.$.output.childNodes[1];
+ assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('hash html with base url', () => {
+ window.CANONICAL_PATH = '/r';
+
+ element.content = 'hash:foo';
+ const linkEl = element.$.output.childNodes[0];
+ assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
+ assert.equal(linkEl.textContent, 'foo');
+ });
+
+ test('disabled config', () => {
+ element.content = 'foo:baz';
+ assert.equal(element.$.output.innerHTML, 'foo:baz');
+ });
+
+ test('R=email labels link correctly', () => {
+ element.removeZeroWidthSpace = true;
+ element.content = 'R=\u200Btest@google.com';
+ assert.equal(element.$.output.textContent, 'R=test@google.com');
+ assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1);
+ });
+
+ test('CC=email labels link correctly', () => {
+ element.removeZeroWidthSpace = true;
+ element.content = 'CC=\u200Btest@google.com';
+ assert.equal(element.$.output.textContent, 'CC=test@google.com');
+ assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1);
+ });
+
+ test('only {http,https,mailto} protocols are linkified', () => {
+ element.content = 'xx mailto:test@google.com yy';
+ let links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+ assert.equal(links[0].innerHTML, 'mailto:test@google.com');
+
+ element.content = 'xx http://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'http://google.com');
+ assert.equal(links[0].innerHTML, 'http://google.com');
+
+ element.content = 'xx https://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ element.content = 'xx ssh://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+
+ element.content = 'xx ftp://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+ });
+
+ test('links without leading whitespace are linkified', () => {
+ element.content = 'xx abcmailto:test@google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc');
+ let links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
+ assert.equal(links[0].innerHTML, 'mailto:test@google.com');
+
+ element.content = 'xx defhttp://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'http://google.com');
+ assert.equal(links[0].innerHTML, 'http://google.com');
+
+ element.content = 'xx qwehttps://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ // Non-latin character
+ element.content = 'xx абвhttps://google.com yy';
+ assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв');
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 1);
+ assert.equal(links[0].getAttribute('href'), 'https://google.com');
+ assert.equal(links[0].innerHTML, 'https://google.com');
+
+ element.content = 'xx ssh://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+
+ element.content = 'xx ftp://google.com yy';
+ links = element.$.output.querySelectorAll('a');
+ assert.equal(links.length, 0);
+ });
+
+ test('overlapping links', () => {
+ element.config = {
+ b1: {
+ match: '(B:\\s*)(\\d+)',
+ html: '$1<a href="ftp://foo/$2">$2</a>',
+ },
+ b2: {
+ match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)',
+ html: '$1<a href="ftp://foo/$2">$2</a>',
+ },
+ };
+ element.content = '- B: 123, 45';
+ const links = dom(element.root).querySelectorAll('a');
+
+ assert.equal(links.length, 2);
+ assert.equal(element.shadowRoot
+ .querySelector('span').textContent, '- B: 123, 45');
+
+ assert.equal(links[0].href, 'ftp://foo/123');
+ assert.equal(links[0].textContent, '123');
+
+ assert.equal(links[1].href, 'ftp://foo/45');
+ assert.equal(links[1].textContent, '45');
+ });
+
+ test('_contentOrConfigChanged called with config', () => {
+ const contentStub = sandbox.stub(element, '_contentChanged');
+ const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+ element.content = 'some text';
+ assert.isTrue(contentStub.called);
+ assert.isTrue(contentConfigStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index 8913cd8..d52a912 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -14,106 +14,119 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
+import '@polymer/iron-input/iron-input.js';
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../gr-button/gr-button.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-list-view_html.js';
- /**
- * @appliesMixin Gerrit.BaseUrlMixin
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrListView extends Polymer.mixinBehaviors( [
- Gerrit.BaseUrlBehavior,
- Gerrit.FireBehavior,
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-list-view'; }
+const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
- static get properties() {
- return {
- createNew: Boolean,
- items: Array,
- itemsPerPage: Number,
- filter: {
- type: String,
- observer: '_filterChanged',
- },
- offset: Number,
- loading: Boolean,
- path: String,
- };
- }
+/**
+ * @appliesMixin Gerrit.BaseUrlMixin
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrListView extends mixinBehaviors( [
+ Gerrit.BaseUrlBehavior,
+ Gerrit.FireBehavior,
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
- /** @override */
- detached() {
- super.detached();
- this.cancelDebouncer('reload');
- }
+ static get is() { return 'gr-list-view'; }
- _filterChanged(newFilter, oldFilter) {
- if (!newFilter && !oldFilter) {
- return;
- }
-
- this._debounceReload(newFilter);
- }
-
- _debounceReload(filter) {
- this.debounce('reload', () => {
- if (filter) {
- return page.show(`${this.path}/q/filter:` +
- this.encodeURL(filter, false));
- }
- page.show(this.path);
- }, REQUEST_DEBOUNCE_INTERVAL_MS);
- }
-
- _createNewItem() {
- this.fire('create-clicked');
- }
-
- _computeNavLink(offset, direction, itemsPerPage, filter, path) {
- // Offset could be a string when passed from the router.
- offset = +(offset || 0);
- const newOffset = Math.max(0, offset + (itemsPerPage * direction));
- let href = this.getBaseUrl() + path;
- if (filter) {
- href += '/q/filter:' + this.encodeURL(filter, false);
- }
- if (newOffset > 0) {
- href += ',' + newOffset;
- }
- return href;
- }
-
- _computeCreateClass(createNew) {
- return createNew ? 'show' : '';
- }
-
- _hidePrevArrow(loading, offset) {
- return loading || offset === 0;
- }
-
- _hideNextArrow(loading, items) {
- if (loading || !items || !items.length) {
- return true;
- }
- const lastPage = items.length < this.itemsPerPage + 1;
- return lastPage;
- }
-
- // TODO: fix offset (including itemsPerPage)
- // to either support a decimal or make it go to the nearest
- // whole number (e.g 3).
- _computePage(offset, itemsPerPage) {
- return offset / itemsPerPage + 1;
- }
+ static get properties() {
+ return {
+ createNew: Boolean,
+ items: Array,
+ itemsPerPage: Number,
+ filter: {
+ type: String,
+ observer: '_filterChanged',
+ },
+ offset: Number,
+ loading: Boolean,
+ path: String,
+ };
}
- customElements.define(GrListView.is, GrListView);
-})();
+ /** @override */
+ detached() {
+ super.detached();
+ this.cancelDebouncer('reload');
+ }
+
+ _filterChanged(newFilter, oldFilter) {
+ if (!newFilter && !oldFilter) {
+ return;
+ }
+
+ this._debounceReload(newFilter);
+ }
+
+ _debounceReload(filter) {
+ this.debounce('reload', () => {
+ if (filter) {
+ return page.show(`${this.path}/q/filter:` +
+ this.encodeURL(filter, false));
+ }
+ page.show(this.path);
+ }, REQUEST_DEBOUNCE_INTERVAL_MS);
+ }
+
+ _createNewItem() {
+ this.fire('create-clicked');
+ }
+
+ _computeNavLink(offset, direction, itemsPerPage, filter, path) {
+ // Offset could be a string when passed from the router.
+ offset = +(offset || 0);
+ const newOffset = Math.max(0, offset + (itemsPerPage * direction));
+ let href = this.getBaseUrl() + path;
+ if (filter) {
+ href += '/q/filter:' + this.encodeURL(filter, false);
+ }
+ if (newOffset > 0) {
+ href += ',' + newOffset;
+ }
+ return href;
+ }
+
+ _computeCreateClass(createNew) {
+ return createNew ? 'show' : '';
+ }
+
+ _hidePrevArrow(loading, offset) {
+ return loading || offset === 0;
+ }
+
+ _hideNextArrow(loading, items) {
+ if (loading || !items || !items.length) {
+ return true;
+ }
+ const lastPage = items.length < this.itemsPerPage + 1;
+ return lastPage;
+ }
+
+ // TODO: fix offset (including itemsPerPage)
+ // to either support a decimal or make it go to the nearest
+ // whole number (e.g 3).
+ _computePage(offset, itemsPerPage) {
+ return offset / itemsPerPage + 1;
+ }
+}
+
+customElements.define(GrListView.is, GrListView);
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
index 3d41a7c..0d33dd0 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
@@ -1,31 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="/bower_components/iron-input/iron-input.html">
-<link rel="import" href="/bower_components/iron-icon/iron-icon.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-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-
-<dom-module id="gr-list-view">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
#filter {
max-width: 25em;
@@ -70,19 +61,12 @@
<div id="topContainer">
<div class="filterContainer">
<label>Filter:</label>
- <iron-input
- type="text"
- bind-value="{{filter}}">
- <input
- is="iron-input"
- type="text"
- id="filter"
- bind-value="{{filter}}">
+ <iron-input type="text" bind-value="{{filter}}">
+ <input is="iron-input" type="text" id="filter" bind-value="{{filter}}">
</iron-input>
</div>
- <div id="createNewContainer"
- class$="[[_computeCreateClass(createNew)]]">
- <gr-button primary link id="createNew" on-click="_createNewItem">
+ <div id="createNewContainer" class\$="[[_computeCreateClass(createNew)]]">
+ <gr-button primary="" link="" id="createNew" on-click="_createNewItem">
Create New
</gr-button>
</div>
@@ -90,17 +74,11 @@
<slot></slot>
<nav>
Page [[_computePage(offset, itemsPerPage)]]
- <a id="prevArrow"
- href$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]"
- hidden$="[[_hidePrevArrow(loading, offset)]]" hidden>
+ <a id="prevArrow" href\$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]" hidden\$="[[_hidePrevArrow(loading, offset)]]" hidden="">
<iron-icon icon="gr-icons:chevron-left"></iron-icon>
</a>
- <a id="nextArrow"
- href$="[[_computeNavLink(offset, 1, itemsPerPage, filter, path)]]"
- hidden$="[[_hideNextArrow(loading, items)]]" hidden>
+ <a id="nextArrow" href\$="[[_computeNavLink(offset, 1, itemsPerPage, filter, path)]]" hidden\$="[[_hideNextArrow(loading, items)]]" hidden="">
<iron-icon icon="gr-icons:chevron-right"></iron-icon>
</a>
</nav>
- </template>
- <script src="gr-list-view.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
index 70605a1..cd45650 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -19,16 +19,21 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-list-view</title>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/bower_components/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/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-list-view.html">
+<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-list-view.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-list-view.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -36,133 +41,135 @@
</template>
</test-fixture>
-<script>
- suite('gr-list-view 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-list-view.js';
+suite('gr-list-view tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeNavLink', () => {
+ const offset = 25;
+ const projectsPerPage = 25;
+ let filter = 'test';
+ const path = '/admin/projects';
+
+ sandbox.stub(element, 'getBaseUrl', () => '');
+
+ assert.equal(
+ element._computeNavLink(offset, 1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:test,50');
+
+ assert.equal(
+ element._computeNavLink(offset, -1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:test');
+
+ assert.equal(
+ element._computeNavLink(offset, 1, projectsPerPage, null, path),
+ '/admin/projects,50');
+
+ assert.equal(
+ element._computeNavLink(offset, -1, projectsPerPage, null, path),
+ '/admin/projects');
+
+ filter = 'plugins/';
+ assert.equal(
+ element._computeNavLink(offset, 1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:plugins%252F,50');
+ });
+
+ test('_onValueChange', done => {
+ element.path = '/admin/projects';
+ sandbox.stub(page, 'show', url => {
+ assert.equal(url, '/admin/projects/q/filter:test');
+ done();
});
+ element.filter = 'test';
+ });
- teardown(() => {
- sandbox.restore();
- });
+ test('_filterChanged not reload when swap between falsy values', () => {
+ sandbox.stub(element, '_debounceReload');
+ element.filter = null;
+ element.filter = undefined;
+ element.filter = '';
+ assert.isFalse(element._debounceReload.called);
+ });
- test('_computeNavLink', () => {
- const offset = 25;
- const projectsPerPage = 25;
- let filter = 'test';
- const path = '/admin/projects';
+ test('next button', done => {
+ element.itemsPerPage = 25;
+ let projects = new Array(26);
- sandbox.stub(element, 'getBaseUrl', () => '');
-
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test,50');
-
- assert.equal(
- element._computeNavLink(offset, -1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test');
-
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, null, path),
- '/admin/projects,50');
-
- assert.equal(
- element._computeNavLink(offset, -1, projectsPerPage, null, path),
- '/admin/projects');
-
- filter = 'plugins/';
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:plugins%252F,50');
- });
-
- test('_onValueChange', done => {
- element.path = '/admin/projects';
- sandbox.stub(page, 'show', url => {
- assert.equal(url, '/admin/projects/q/filter:test');
- done();
- });
- element.filter = 'test';
- });
-
- test('_filterChanged not reload when swap between falsy values', () => {
- sandbox.stub(element, '_debounceReload');
- element.filter = null;
- element.filter = undefined;
- element.filter = '';
- assert.isFalse(element._debounceReload.called);
- });
-
- test('next button', done => {
- element.itemsPerPage = 25;
- let projects = new Array(26);
-
- flush(() => {
- let loading;
- assert.isFalse(element._hideNextArrow(loading, projects));
- loading = true;
- assert.isTrue(element._hideNextArrow(loading, projects));
- loading = false;
- assert.isFalse(element._hideNextArrow(loading, projects));
- element._projects = [];
- assert.isTrue(element._hideNextArrow(loading, element._projects));
- projects = new Array(4);
- assert.isTrue(element._hideNextArrow(loading, projects));
- done();
- });
- });
-
- test('prev button', () => {
- assert.isTrue(element._hidePrevArrow(true, 0));
- flush(() => {
- let offset = 0;
- assert.isTrue(element._hidePrevArrow(false, offset));
- offset = 5;
- assert.isFalse(element._hidePrevArrow(false, offset));
- });
- });
-
- test('createNew link appears correctly', () => {
- assert.isFalse(element.shadowRoot
- .querySelector('#createNewContainer').classList
- .contains('show'));
- element.createNew = true;
- flushAsynchronousOperations();
- assert.isTrue(element.shadowRoot
- .querySelector('#createNewContainer').classList
- .contains('show'));
- });
-
- test('fires create clicked event when button tapped', () => {
- const clickHandler = sandbox.stub();
- element.addEventListener('create-clicked', clickHandler);
- element.createNew = true;
- flushAsynchronousOperations();
- MockInteractions.tap(element.shadowRoot.querySelector('#createNew'));
- assert.isTrue(clickHandler.called);
- });
-
- test('next/prev links change when path changes', () => {
- const BRANCHES_PATH = '/path/to/branches';
- const TAGS_PATH = '/path/to/tags';
- sandbox.stub(element, '_computeNavLink');
- element.offset = 0;
- element.itemsPerPage = 25;
- element.filter = '';
- element.path = BRANCHES_PATH;
- assert.equal(element._computeNavLink.lastCall.args[4], BRANCHES_PATH);
- element.path = TAGS_PATH;
- assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH);
- });
-
- test('_computePage', () => {
- assert.equal(element._computePage(0, 25), 1);
- assert.equal(element._computePage(50, 25), 3);
+ flush(() => {
+ let loading;
+ assert.isFalse(element._hideNextArrow(loading, projects));
+ loading = true;
+ assert.isTrue(element._hideNextArrow(loading, projects));
+ loading = false;
+ assert.isFalse(element._hideNextArrow(loading, projects));
+ element._projects = [];
+ assert.isTrue(element._hideNextArrow(loading, element._projects));
+ projects = new Array(4);
+ assert.isTrue(element._hideNextArrow(loading, projects));
+ done();
});
});
+
+ test('prev button', () => {
+ assert.isTrue(element._hidePrevArrow(true, 0));
+ flush(() => {
+ let offset = 0;
+ assert.isTrue(element._hidePrevArrow(false, offset));
+ offset = 5;
+ assert.isFalse(element._hidePrevArrow(false, offset));
+ });
+ });
+
+ test('createNew link appears correctly', () => {
+ assert.isFalse(element.shadowRoot
+ .querySelector('#createNewContainer').classList
+ .contains('show'));
+ element.createNew = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.shadowRoot
+ .querySelector('#createNewContainer').classList
+ .contains('show'));
+ });
+
+ test('fires create clicked event when button tapped', () => {
+ const clickHandler = sandbox.stub();
+ element.addEventListener('create-clicked', clickHandler);
+ element.createNew = true;
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.shadowRoot.querySelector('#createNew'));
+ assert.isTrue(clickHandler.called);
+ });
+
+ test('next/prev links change when path changes', () => {
+ const BRANCHES_PATH = '/path/to/branches';
+ const TAGS_PATH = '/path/to/tags';
+ sandbox.stub(element, '_computeNavLink');
+ element.offset = 0;
+ element.itemsPerPage = 25;
+ element.filter = '';
+ element.path = BRANCHES_PATH;
+ assert.equal(element._computeNavLink.lastCall.args[4], BRANCHES_PATH);
+ element.path = TAGS_PATH;
+ assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH);
+ });
+
+ test('_computePage', () => {
+ assert.equal(element._computePage(0, 25), 1);
+ assert.equal(element._computePage(50, 25), 3);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index 3a957ef..fd68971 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -14,108 +14,117 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const AWAIT_MAX_ITERS = 10;
- const AWAIT_STEP = 5;
- const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
+import {IronOverlayBehaviorImpl, IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
+import '../../../behaviors/fire-behavior/fire-behavior.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-overlay_html.js';
+
+const AWAIT_MAX_ITERS = 10;
+const AWAIT_STEP = 5;
+const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrOverlay extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ IronOverlayBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-overlay'; }
+ /**
+ * Fired when a fullscreen overlay is closed
+ *
+ * @event fullscreen-overlay-closed
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
+ * Fired when an overlay is opened in full screen mode
+ *
+ * @event fullscreen-overlay-opened
*/
- class GrOverlay extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Polymer.IronOverlayBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-overlay'; }
- /**
- * Fired when a fullscreen overlay is closed
- *
- * @event fullscreen-overlay-closed
- */
- /**
- * Fired when an overlay is opened in full screen mode
- *
- * @event fullscreen-overlay-opened
- */
+ static get properties() {
+ return {
+ _fullScreenOpen: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
- static get properties() {
- return {
- _fullScreenOpen: {
- type: Boolean,
- value: false,
- },
- };
- }
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('iron-overlay-closed',
+ () => this._close());
+ this.addEventListener('iron-overlay-cancelled',
+ () => this._close());
+ }
- /** @override */
- created() {
- super.created();
- this.addEventListener('iron-overlay-closed',
- () => this._close());
- this.addEventListener('iron-overlay-cancelled',
- () => this._close());
- }
-
- open(...args) {
- return new Promise((resolve, reject) => {
- Polymer.IronOverlayBehaviorImpl.open.apply(this, args);
- if (this._isMobile()) {
- this.fire('fullscreen-overlay-opened');
- this._fullScreenOpen = true;
- }
- this._awaitOpen(resolve, reject);
- });
- }
-
- _isMobile() {
- return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`);
- }
-
- _close() {
- if (this._fullScreenOpen) {
- this.fire('fullscreen-overlay-closed');
- this._fullScreenOpen = false;
+ open(...args) {
+ return new Promise((resolve, reject) => {
+ IronOverlayBehaviorImpl.open.apply(this, args);
+ if (this._isMobile()) {
+ this.fire('fullscreen-overlay-opened');
+ this._fullScreenOpen = true;
}
- }
+ this._awaitOpen(resolve, reject);
+ });
+ }
- /**
- * Override the focus stops that iron-overlay-behavior tries to find.
- */
- setFocusStops(stops) {
- this.__firstFocusableNode = stops.start;
- this.__lastFocusableNode = stops.end;
- }
+ _isMobile() {
+ return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`);
+ }
- /**
- * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
- * opening. Eventually replace with a direct way to listen to the overlay.
- */
- _awaitOpen(fn, reject) {
- let iters = 0;
- const step = () => {
- this.async(() => {
- if (this.style.display !== 'none') {
- fn.call(this);
- } else if (iters++ < AWAIT_MAX_ITERS) {
- step.call(this);
- } else {
- reject(new Error('gr-overlay _awaitOpen failed to resolve'));
- }
- }, AWAIT_STEP);
- };
- step.call(this);
- }
-
- _id() {
- return this.getAttribute('id') || 'global';
+ _close() {
+ if (this._fullScreenOpen) {
+ this.fire('fullscreen-overlay-closed');
+ this._fullScreenOpen = false;
}
}
- customElements.define(GrOverlay.is, GrOverlay);
-})();
+ /**
+ * Override the focus stops that iron-overlay-behavior tries to find.
+ */
+ setFocusStops(stops) {
+ this.__firstFocusableNode = stops.start;
+ this.__lastFocusableNode = stops.end;
+ }
+
+ /**
+ * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
+ * opening. Eventually replace with a direct way to listen to the overlay.
+ */
+ _awaitOpen(fn, reject) {
+ let iters = 0;
+ const step = () => {
+ this.async(() => {
+ if (this.style.display !== 'none') {
+ fn.call(this);
+ } else if (iters++ < AWAIT_MAX_ITERS) {
+ step.call(this);
+ } else {
+ reject(new Error('gr-overlay _awaitOpen failed to resolve'));
+ }
+ }, AWAIT_STEP);
+ };
+ step.call(this);
+ }
+
+ _id() {
+ return this.getAttribute('id') || 'global';
+ }
+}
+
+customElements.define(GrOverlay.is, GrOverlay);
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
index 1afd1c9..5fe000a 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
@@ -1,27 +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="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-overlay">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
background: var(--dialog-background-color);
@@ -42,6 +37,4 @@
}
</style>
<slot></slot>
- </template>
- <script src="gr-overlay.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
index e1218af3..72f01fb 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
@@ -19,18 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-overlay</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/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
-<link rel="import" href="gr-overlay.html">
+<script type="module" src="./gr-overlay.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-overlay.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -40,57 +45,59 @@
</template>
</test-fixture>
-<script>
- suite('gr-overlay 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-overlay.js';
+suite('gr-overlay 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('events are fired on fullscreen view', done => {
- sandbox.stub(element, '_isMobile').returns(true);
- const openHandler = sandbox.stub();
- const closeHandler = sandbox.stub();
- element.addEventListener('fullscreen-overlay-opened', openHandler);
- element.addEventListener('fullscreen-overlay-closed', closeHandler);
+ test('events are fired on fullscreen view', done => {
+ sandbox.stub(element, '_isMobile').returns(true);
+ const openHandler = sandbox.stub();
+ const closeHandler = sandbox.stub();
+ element.addEventListener('fullscreen-overlay-opened', openHandler);
+ element.addEventListener('fullscreen-overlay-closed', closeHandler);
- element.open().then(() => {
- assert.isTrue(element._isMobile.called);
- assert.isTrue(element._fullScreenOpen);
- assert.isTrue(openHandler.called);
+ element.open().then(() => {
+ assert.isTrue(element._isMobile.called);
+ assert.isTrue(element._fullScreenOpen);
+ assert.isTrue(openHandler.called);
- element._close();
- assert.isFalse(element._fullScreenOpen);
- assert.isTrue(closeHandler.called);
- done();
- });
- });
-
- test('events are not fired on desktop view', done => {
- sandbox.stub(element, '_isMobile').returns(false);
- const openHandler = sandbox.stub();
- const closeHandler = sandbox.stub();
- element.addEventListener('fullscreen-overlay-opened', openHandler);
- element.addEventListener('fullscreen-overlay-closed', closeHandler);
-
- element.open().then(() => {
- assert.isTrue(element._isMobile.called);
- assert.isFalse(element._fullScreenOpen);
- assert.isFalse(openHandler.called);
-
- element._close();
- assert.isFalse(element._fullScreenOpen);
- assert.isFalse(closeHandler.called);
- done();
- });
+ element._close();
+ assert.isFalse(element._fullScreenOpen);
+ assert.isTrue(closeHandler.called);
+ done();
});
});
+
+ test('events are not fired on desktop view', done => {
+ sandbox.stub(element, '_isMobile').returns(false);
+ const openHandler = sandbox.stub();
+ const closeHandler = sandbox.stub();
+ element.addEventListener('fullscreen-overlay-opened', openHandler);
+ element.addEventListener('fullscreen-overlay-closed', closeHandler);
+
+ element.open().then(() => {
+ assert.isTrue(element._isMobile.called);
+ assert.isFalse(element._fullScreenOpen);
+ assert.isFalse(openHandler.called);
+
+ element._close();
+ assert.isFalse(element._fullScreenOpen);
+ assert.isFalse(closeHandler.called);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
index ac876c4..23b284c 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -14,62 +14,69 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
- /** @extends Polymer.Element */
- class GrPageNav extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-page-nav'; }
+import '../../../scripts/bundled-polymer.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-page-nav_html.js';
- static get properties() {
- return {
- _headerHeight: Number,
- };
- }
+/** @extends Polymer.Element */
+class GrPageNav extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- /** @override */
- attached() {
- super.attached();
- this.listen(window, 'scroll', '_handleBodyScroll');
- }
+ static get is() { return 'gr-page-nav'; }
- /** @override */
- detached() {
- super.detached();
- this.unlisten(window, 'scroll', '_handleBodyScroll');
- }
-
- _handleBodyScroll() {
- if (this._headerHeight === undefined) {
- let top = this._getOffsetTop(this);
- for (let offsetParent = this.offsetParent;
- offsetParent;
- offsetParent = this._getOffsetParent(offsetParent)) {
- top += this._getOffsetTop(offsetParent);
- }
- this._headerHeight = top;
- }
-
- this.$.nav.classList.toggle('pinned',
- this._getScrollY() >= this._headerHeight);
- }
-
- /* Functions used for test purposes */
- _getOffsetParent(element) {
- if (!element || !element.offsetParent) { return ''; }
- return element.offsetParent;
- }
-
- _getOffsetTop(element) {
- return element.offsetTop;
- }
-
- _getScrollY() {
- return window.scrollY;
- }
+ static get properties() {
+ return {
+ _headerHeight: Number,
+ };
}
- customElements.define(GrPageNav.is, GrPageNav);
-})();
+ /** @override */
+ attached() {
+ super.attached();
+ this.listen(window, 'scroll', '_handleBodyScroll');
+ }
+
+ /** @override */
+ detached() {
+ super.detached();
+ this.unlisten(window, 'scroll', '_handleBodyScroll');
+ }
+
+ _handleBodyScroll() {
+ if (this._headerHeight === undefined) {
+ let top = this._getOffsetTop(this);
+ for (let offsetParent = this.offsetParent;
+ offsetParent;
+ offsetParent = this._getOffsetParent(offsetParent)) {
+ top += this._getOffsetTop(offsetParent);
+ }
+ this._headerHeight = top;
+ }
+
+ this.$.nav.classList.toggle('pinned',
+ this._getScrollY() >= this._headerHeight);
+ }
+
+ /* Functions used for test purposes */
+ _getOffsetParent(element) {
+ if (!element || !element.offsetParent) { return ''; }
+ return element.offsetParent;
+ }
+
+ _getOffsetTop(element) {
+ return element.offsetTop;
+ }
+
+ _getScrollY() {
+ return window.scrollY;
+ }
+}
+
+customElements.define(GrPageNav.is, GrPageNav);
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
index f1c3a6f..fe17a1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
@@ -1,25 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-tooltip-behavior/gr-tooltip-behavior.html">
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-
-<dom-module id="gr-page-nav">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
#nav {
background-color: var(--table-header-background-color);
@@ -42,6 +39,4 @@
<nav id="nav">
<slot></slot>
</nav>
- </template>
- <script src="gr-page-nav.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
index fdfe46c..4f47b95 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
@@ -19,18 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-page-nav</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/page/page.js"></script>
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
+<script src="/node_modules/page/page.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
-<link rel="import" href="gr-page-nav.html">
+<script type="module" src="./gr-page-nav.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-page-nav.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -42,53 +47,55 @@
</template>
</test-fixture>
-<script>
- suite('gr-page-nav 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-page-nav.js';
+suite('gr-page-nav tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('header is not pinned just below top', () => {
- sandbox.stub(element, '_getOffsetParent', () => 0);
- sandbox.stub(element, '_getOffsetTop', () => 10);
- sandbox.stub(element, '_getScrollY', () => 5);
- element._handleBodyScroll();
- assert.isFalse(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is pinned when scroll down the page', () => {
- sandbox.stub(element, '_getOffsetParent', () => 0);
- sandbox.stub(element, '_getOffsetTop', () => 10);
- sandbox.stub(element, '_getScrollY', () => 25);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isTrue(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is not pinned just below top with header set', () => {
- element._headerHeight = 20;
- sandbox.stub(element, '_getScrollY', () => 15);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isFalse(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is pinned when scroll down the page with header set', () => {
- element._headerHeight = 20;
- sandbox.stub(element, '_getScrollY', () => 25);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isTrue(element.$.nav.classList.contains('pinned'));
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ flushAsynchronousOperations();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('header is not pinned just below top', () => {
+ sandbox.stub(element, '_getOffsetParent', () => 0);
+ sandbox.stub(element, '_getOffsetTop', () => 10);
+ sandbox.stub(element, '_getScrollY', () => 5);
+ element._handleBodyScroll();
+ assert.isFalse(element.$.nav.classList.contains('pinned'));
+ });
+
+ test('header is pinned when scroll down the page', () => {
+ sandbox.stub(element, '_getOffsetParent', () => 0);
+ sandbox.stub(element, '_getOffsetTop', () => 10);
+ sandbox.stub(element, '_getScrollY', () => 25);
+ window.scrollY = 100;
+ element._handleBodyScroll();
+ assert.isTrue(element.$.nav.classList.contains('pinned'));
+ });
+
+ test('header is not pinned just below top with header set', () => {
+ element._headerHeight = 20;
+ sandbox.stub(element, '_getScrollY', () => 15);
+ window.scrollY = 100;
+ element._handleBodyScroll();
+ assert.isFalse(element.$.nav.classList.contains('pinned'));
+ });
+
+ test('header is pinned when scroll down the page with header set', () => {
+ element._headerHeight = 20;
+ sandbox.stub(element, '_getScrollY', () => 25);
+ window.scrollY = 100;
+ element._handleBodyScroll();
+ assert.isTrue(element.$.nav.classList.contains('pinned'));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
index 18e2596..15d5f76 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -14,110 +14,122 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const SUGGESTIONS_LIMIT = 15;
- const REF_PREFIX = 'refs/heads/';
+import '@polymer/iron-icon/iron-icon.js';
+import '../../../styles/shared-styles.js';
+import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-labeled-autocomplete/gr-labeled-autocomplete.js';
+import '../gr-rest-api-interface/gr-rest-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-repo-branch-picker_html.js';
- /**
- * @appliesMixin Gerrit.URLEncodingMixin
- * @extends Polymer.Element
- */
- class GrRepoBranchPicker extends Polymer.mixinBehaviors( [
- Gerrit.URLEncodingBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-repo-branch-picker'; }
+const SUGGESTIONS_LIMIT = 15;
+const REF_PREFIX = 'refs/heads/';
- static get properties() {
- return {
- repo: {
- type: String,
- notify: true,
- observer: '_repoChanged',
+/**
+ * @appliesMixin Gerrit.URLEncodingMixin
+ * @extends Polymer.Element
+ */
+class GrRepoBranchPicker extends mixinBehaviors( [
+ Gerrit.URLEncodingBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-repo-branch-picker'; }
+
+ static get properties() {
+ return {
+ repo: {
+ type: String,
+ notify: true,
+ observer: '_repoChanged',
+ },
+ branch: {
+ type: String,
+ notify: true,
+ },
+ _branchDisabled: Boolean,
+ _query: {
+ type: Function,
+ value() {
+ return this._getRepoBranchesSuggestions.bind(this);
},
- branch: {
- type: String,
- notify: true,
+ },
+ _repoQuery: {
+ type: Function,
+ value() {
+ return this._getRepoSuggestions.bind(this);
},
- _branchDisabled: Boolean,
- _query: {
- type: Function,
- value() {
- return this._getRepoBranchesSuggestions.bind(this);
- },
- },
- _repoQuery: {
- type: Function,
- value() {
- return this._getRepoSuggestions.bind(this);
- },
- },
- };
- }
+ },
+ };
+ }
- /** @override */
- attached() {
- super.attached();
- if (this.repo) {
- this.$.repoInput.setText(this.repo);
- }
- }
-
- /** @override */
- ready() {
- super.ready();
- this._branchDisabled = !this.repo;
- }
-
- _getRepoBranchesSuggestions(input) {
- if (!this.repo) { return Promise.resolve([]); }
- if (input.startsWith(REF_PREFIX)) {
- input = input.substring(REF_PREFIX.length);
- }
- return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT)
- .then(this._branchResponseToSuggestions.bind(this));
- }
-
- _getRepoSuggestions(input) {
- return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT)
- .then(this._repoResponseToSuggestions.bind(this));
- }
-
- _repoResponseToSuggestions(res) {
- return res.map(repo => {
- return {
- name: repo.name,
- value: this.singleDecodeURL(repo.id),
- };
- });
- }
-
- _branchResponseToSuggestions(res) {
- return Object.keys(res).map(key => {
- let branch = res[key].ref;
- if (branch.startsWith(REF_PREFIX)) {
- branch = branch.substring(REF_PREFIX.length);
- }
- return {name: branch, value: branch};
- });
- }
-
- _repoCommitted(e) {
- this.repo = e.detail.value;
- }
-
- _branchCommitted(e) {
- this.branch = e.detail.value;
- }
-
- _repoChanged() {
- this.$.branchInput.clear();
- this._branchDisabled = !this.repo;
+ /** @override */
+ attached() {
+ super.attached();
+ if (this.repo) {
+ this.$.repoInput.setText(this.repo);
}
}
- customElements.define(GrRepoBranchPicker.is, GrRepoBranchPicker);
-})();
+ /** @override */
+ ready() {
+ super.ready();
+ this._branchDisabled = !this.repo;
+ }
+
+ _getRepoBranchesSuggestions(input) {
+ if (!this.repo) { return Promise.resolve([]); }
+ if (input.startsWith(REF_PREFIX)) {
+ input = input.substring(REF_PREFIX.length);
+ }
+ return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT)
+ .then(this._branchResponseToSuggestions.bind(this));
+ }
+
+ _getRepoSuggestions(input) {
+ return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT)
+ .then(this._repoResponseToSuggestions.bind(this));
+ }
+
+ _repoResponseToSuggestions(res) {
+ return res.map(repo => {
+ return {
+ name: repo.name,
+ value: this.singleDecodeURL(repo.id),
+ };
+ });
+ }
+
+ _branchResponseToSuggestions(res) {
+ return Object.keys(res).map(key => {
+ let branch = res[key].ref;
+ if (branch.startsWith(REF_PREFIX)) {
+ branch = branch.substring(REF_PREFIX.length);
+ }
+ return {name: branch, value: branch};
+ });
+ }
+
+ _repoCommitted(e) {
+ this.repo = e.detail.value;
+ }
+
+ _branchCommitted(e) {
+ this.branch = e.detail.value;
+ }
+
+ _repoChanged() {
+ this.$.branchInput.clear();
+ this._branchDisabled = !this.repo;
+ }
+}
+
+customElements.define(GrRepoBranchPicker.is, GrRepoBranchPicker);
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
index ce596f8..fe6b522 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
@@ -1,29 +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="/bower_components/iron-icon/iron-icon.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<dom-module id="gr-repo-branch-picker">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@@ -37,24 +30,11 @@
}
</style>
<div>
- <gr-labeled-autocomplete
- id="repoInput"
- label="Repository"
- placeholder="Select repo"
- on-commit="_repoCommitted"
- query="[[_repoQuery]]">
+ <gr-labeled-autocomplete id="repoInput" label="Repository" placeholder="Select repo" on-commit="_repoCommitted" query="[[_repoQuery]]">
</gr-labeled-autocomplete>
<iron-icon icon="gr-icons:chevron-right"></iron-icon>
- <gr-labeled-autocomplete
- id="branchInput"
- label="Branch"
- placeholder="Select branch"
- disabled="[[_branchDisabled]]"
- on-commit="_branchCommitted"
- query="[[_query]]">
+ <gr-labeled-autocomplete id="branchInput" label="Branch" placeholder="Select branch" disabled="[[_branchDisabled]]" on-commit="_branchCommitted" query="[[_query]]">
</gr-labeled-autocomplete>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
- </template>
- <script src="gr-repo-branch-picker.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
index b068b25..191b5d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_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-repo-branch-picker</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-repo-branch-picker.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-repo-branch-picker.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-repo-branch-picker.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,113 +39,115 @@
</template>
</test-fixture>
-<script>
- suite('gr-repo-branch-picker 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-repo-branch-picker.js';
+suite('gr-repo-branch-picker tests', () => {
+ let element;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ suite('_getRepoSuggestions', () => {
setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
+ sandbox.stub(element.$.restAPI, 'getRepos')
+ .returns(Promise.resolve([
+ {
+ id: 'plugins%2Favatars-external',
+ name: 'plugins/avatars-external',
+ }, {
+ id: 'plugins%2Favatars-gravatar',
+ name: 'plugins/avatars-gravatar',
+ }, {
+ id: 'plugins%2Favatars%2Fexternal',
+ name: 'plugins/avatars/external',
+ }, {
+ id: 'plugins%2Favatars%2Fgravatar',
+ name: 'plugins/avatars/gravatar',
+ },
+ ]));
});
- teardown(() => { sandbox.restore(); });
-
- suite('_getRepoSuggestions', () => {
- setup(() => {
- sandbox.stub(element.$.restAPI, 'getRepos')
- .returns(Promise.resolve([
- {
- id: 'plugins%2Favatars-external',
- name: 'plugins/avatars-external',
- }, {
- id: 'plugins%2Favatars-gravatar',
- name: 'plugins/avatars-gravatar',
- }, {
- id: 'plugins%2Favatars%2Fexternal',
- name: 'plugins/avatars/external',
- }, {
- id: 'plugins%2Favatars%2Fgravatar',
- name: 'plugins/avatars/gravatar',
- },
- ]));
- });
-
- test('converts to suggestion objects', () => {
- const input = 'plugins/avatars';
- return element._getRepoSuggestions(input).then(suggestions => {
- assert.isTrue(element.$.restAPI.getRepos.calledWith(input));
- const unencodedNames = [
- 'plugins/avatars-external',
- 'plugins/avatars-gravatar',
- 'plugins/avatars/external',
- 'plugins/avatars/gravatar',
- ];
- assert.deepEqual(suggestions.map(s => s.name), unencodedNames);
- assert.deepEqual(suggestions.map(s => s.value), unencodedNames);
- });
- });
- });
-
- suite('_getRepoBranchesSuggestions', () => {
- setup(() => {
- sandbox.stub(element.$.restAPI, 'getRepoBranches')
- .returns(Promise.resolve([
- {ref: 'refs/heads/stable-2.10'},
- {ref: 'refs/heads/stable-2.11'},
- {ref: 'refs/heads/stable-2.12'},
- {ref: 'refs/heads/stable-2.13'},
- {ref: 'refs/heads/stable-2.14'},
- {ref: 'refs/heads/stable-2.15'},
- ]));
- });
-
- test('converts to suggestion objects', () => {
- const repo = 'gerrit';
- const branchInput = 'stable-2.1';
- element.repo = repo;
- return element._getRepoBranchesSuggestions(branchInput)
- .then(suggestions => {
- assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
- branchInput, repo, 15));
- const refNames = [
- 'stable-2.10',
- 'stable-2.11',
- 'stable-2.12',
- 'stable-2.13',
- 'stable-2.14',
- 'stable-2.15',
- ];
- assert.deepEqual(suggestions.map(s => s.name), refNames);
- assert.deepEqual(suggestions.map(s => s.value), refNames);
- });
- });
-
- test('filters out ref prefix', () => {
- const repo = 'gerrit';
- const branchInput = 'refs/heads/stable-2.1';
- element.repo = repo;
- return element._getRepoBranchesSuggestions(branchInput)
- .then(suggestions => {
- assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
- 'stable-2.1', repo, 15));
- });
- });
-
- test('does not query when repo is unset', done => {
- element
- ._getRepoBranchesSuggestions('')
- .then(() => {
- assert.isFalse(element.$.restAPI.getRepoBranches.called);
- element.repo = 'gerrit';
- return element._getRepoBranchesSuggestions('');
- })
- .then(() => {
- assert.isTrue(element.$.restAPI.getRepoBranches.called);
- done();
- });
+ test('converts to suggestion objects', () => {
+ const input = 'plugins/avatars';
+ return element._getRepoSuggestions(input).then(suggestions => {
+ assert.isTrue(element.$.restAPI.getRepos.calledWith(input));
+ const unencodedNames = [
+ 'plugins/avatars-external',
+ 'plugins/avatars-gravatar',
+ 'plugins/avatars/external',
+ 'plugins/avatars/gravatar',
+ ];
+ assert.deepEqual(suggestions.map(s => s.name), unencodedNames);
+ assert.deepEqual(suggestions.map(s => s.value), unencodedNames);
});
});
});
+
+ suite('_getRepoBranchesSuggestions', () => {
+ setup(() => {
+ sandbox.stub(element.$.restAPI, 'getRepoBranches')
+ .returns(Promise.resolve([
+ {ref: 'refs/heads/stable-2.10'},
+ {ref: 'refs/heads/stable-2.11'},
+ {ref: 'refs/heads/stable-2.12'},
+ {ref: 'refs/heads/stable-2.13'},
+ {ref: 'refs/heads/stable-2.14'},
+ {ref: 'refs/heads/stable-2.15'},
+ ]));
+ });
+
+ test('converts to suggestion objects', () => {
+ const repo = 'gerrit';
+ const branchInput = 'stable-2.1';
+ element.repo = repo;
+ return element._getRepoBranchesSuggestions(branchInput)
+ .then(suggestions => {
+ assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
+ branchInput, repo, 15));
+ const refNames = [
+ 'stable-2.10',
+ 'stable-2.11',
+ 'stable-2.12',
+ 'stable-2.13',
+ 'stable-2.14',
+ 'stable-2.15',
+ ];
+ assert.deepEqual(suggestions.map(s => s.name), refNames);
+ assert.deepEqual(suggestions.map(s => s.value), refNames);
+ });
+ });
+
+ test('filters out ref prefix', () => {
+ const repo = 'gerrit';
+ const branchInput = 'refs/heads/stable-2.1';
+ element.repo = repo;
+ return element._getRepoBranchesSuggestions(branchInput)
+ .then(suggestions => {
+ assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
+ 'stable-2.1', repo, 15));
+ });
+ });
+
+ test('does not query when repo is unset', done => {
+ element
+ ._getRepoBranchesSuggestions('')
+ .then(() => {
+ assert.isFalse(element.$.restAPI.getRepoBranches.called);
+ element.repo = 'gerrit';
+ return element._getRepoBranchesSuggestions('');
+ })
+ .then(() => {
+ assert.isTrue(element.$.restAPI.getRepoBranches.called);
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index 091c88e..01baa43 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -19,377 +19,380 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-auth</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="../../../behaviors/base-url-behavior/base-url-behavior.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="../../../behaviors/base-url-behavior/base-url-behavior.js"></script>
-<script src="gr-auth.js"></script>
+<script type="module" src="./gr-auth.js"></script>
-<script>
- suite('gr-auth', async () => {
- await readyToTest();
- let auth;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../behaviors/base-url-behavior/base-url-behavior.js';
+import './gr-auth.js';
+suite('gr-auth', () => {
+ let auth;
+ let sandbox;
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ auth = Gerrit.Auth;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('Auth class methods', () => {
+ let fakeFetch;
setup(() => {
- sandbox = sinon.sandbox.create();
- auth = Gerrit.Auth;
+ auth = new Auth();
+ fakeFetch = sandbox.stub(window, 'fetch');
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('Auth class methods', () => {
- let fakeFetch;
- setup(() => {
- auth = new Auth();
- fakeFetch = sandbox.stub(window, 'fetch');
+ test('auth-check returns 403', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ done();
});
+ });
- test('auth-check returns 403', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
+ test('auth-check returns 204', done => {
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed => {
+ assert.isTrue(authed);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ done();
+ });
+ });
+
+ test('auth-check returns 502', done => {
+ fakeFetch.returns(Promise.resolve({status: 502}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ done();
+ });
+ });
+
+ test('auth-check failed', done => {
+ fakeFetch.returns(Promise.reject(new Error('random error')));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.ERROR);
+ done();
+ });
+ });
+ });
+
+ suite('cache and events behaivor', () => {
+ let fakeFetch;
+ let clock;
+ setup(() => {
+ auth = new Auth();
+ clock = sinon.useFakeTimers();
+ fakeFetch = sandbox.stub(window, 'fetch');
+ });
+
+ test('cache auth-check result', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed2 => {
assert.isFalse(authed);
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
done();
});
});
+ });
- test('auth-check returns 204', done => {
+ test('clearCache should refetch auth-check result', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed => {
- assert.isTrue(authed);
+ auth.clearCache();
+ auth.authCheck().then(authed2 => {
+ assert.isTrue(authed2);
assert.equal(auth.status, Auth.STATUS.AUTHED);
done();
});
});
+ });
- test('auth-check returns 502', done => {
- fakeFetch.returns(Promise.resolve({status: 502}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ test('cache expired on auth-check after certain time', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ clock.tick(1000 * 10000);
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed2 => {
+ assert.isTrue(authed2);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
done();
});
});
+ });
- test('auth-check failed', done => {
+ test('no cache if auth-check failed', done => {
+ fakeFetch.returns(Promise.reject(new Error('random error')));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.ERROR);
+ assert.equal(fakeFetch.callCount, 1);
+ auth.authCheck().then(() => {
+ assert.equal(fakeFetch.callCount, 2);
+ done();
+ });
+ });
+ });
+
+ test('fire event when switch from authed to unauthed', done => {
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed => {
+ assert.isTrue(authed);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ clock.tick(1000 * 10000);
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isFalse(authed2);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ assert.isTrue(emitStub.called);
+ done();
+ });
+ });
+ });
+
+ test('fire event when switch from authed to error', done => {
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ auth.authCheck().then(authed => {
+ assert.isTrue(authed);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isFalse(authed2);
+ assert.isTrue(emitStub.called);
assert.equal(auth.status, Auth.STATUS.ERROR);
done();
});
});
});
- suite('cache and events behaivor', () => {
- let fakeFetch;
- let clock;
- setup(() => {
- auth = new Auth();
- clock = sinon.useFakeTimers();
- fakeFetch = sandbox.stub(window, 'fetch');
- });
-
- test('cache auth-check result', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- done();
- });
+ test('no event from non-authed to other status', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ clock.tick(1000 * 10000);
+ fakeFetch.returns(Promise.resolve({status: 204}));
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isTrue(authed2);
+ assert.isFalse(emitStub.called);
+ assert.equal(auth.status, Auth.STATUS.AUTHED);
+ done();
});
});
+ });
- test('clearCache should refetch auth-check result', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.clearCache();
- auth.authCheck().then(authed2 => {
- assert.isTrue(authed2);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- done();
- });
- });
- });
-
- test('cache expired on auth-check after certain time', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed2 => {
- assert.isTrue(authed2);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- done();
- });
- });
- });
-
- test('no cache if auth-check failed', done => {
+ test('no event from non-authed to other status', done => {
+ fakeFetch.returns(Promise.resolve({status: 403}));
+ auth.authCheck().then(authed => {
+ assert.isFalse(authed);
+ assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+ clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
+ const emitStub = sinon.stub();
+ Gerrit.emit = emitStub;
+ auth.authCheck().then(authed2 => {
+ assert.isFalse(authed2);
+ assert.isFalse(emitStub.called);
assert.equal(auth.status, Auth.STATUS.ERROR);
- assert.equal(fakeFetch.callCount, 1);
- auth.authCheck().then(() => {
- assert.equal(fakeFetch.callCount, 2);
- done();
- });
- });
- });
-
- test('fire event when switch from authed to unauthed', done => {
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed => {
- assert.isTrue(authed);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.resolve({status: 403}));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed2);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- assert.isTrue(emitStub.called);
- done();
- });
- });
- });
-
- test('fire event when switch from authed to error', done => {
- fakeFetch.returns(Promise.resolve({status: 204}));
- auth.authCheck().then(authed => {
- assert.isTrue(authed);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed2);
- assert.isTrue(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.ERROR);
- done();
- });
- });
- });
-
- test('no event from non-authed to other status', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.resolve({status: 204}));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isTrue(authed2);
- assert.isFalse(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.AUTHED);
- done();
- });
- });
- });
-
- test('no event from non-authed to other status', done => {
- fakeFetch.returns(Promise.resolve({status: 403}));
- auth.authCheck().then(authed => {
- assert.isFalse(authed);
- assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
- clock.tick(1000 * 10000);
- fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub();
- Gerrit.emit = emitStub;
- auth.authCheck().then(authed2 => {
- assert.isFalse(authed2);
- assert.isFalse(emitStub.called);
- assert.equal(auth.status, Auth.STATUS.ERROR);
- done();
- });
- });
- });
- });
-
- suite('default (xsrf token header)', () => {
- setup(() => {
- sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
- });
-
- test('GET', done => {
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.credentials, 'same-origin');
- done();
- });
- });
-
- test('POST', done => {
- sandbox.stub(auth, '_getCookie')
- .withArgs('XSRF_TOKEN')
- .returns('foobar');
- auth.fetch('/url', {method: 'POST'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.credentials, 'same-origin');
- assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
- done();
- });
- });
- });
-
- suite('cors (access token)', () => {
- setup(() => {
- sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
- });
-
- let getToken;
-
- const makeToken = opt_accessToken => {
- return {
- access_token: opt_accessToken || 'zbaz',
- expires_at: new Date(Date.now() + 10e8).getTime(),
- };
- };
-
- setup(() => {
- getToken = sandbox.stub();
- getToken.returns(Promise.resolve(makeToken()));
- auth.setup(getToken);
- });
-
- test('base url support', done => {
- const baseUrl = 'http://foo';
- sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
- auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
- const [url] = fetch.lastCall.args;
- assert.equal(url, 'http://foo/a/url?access_token=zbaz');
- done();
- });
- });
-
- test('fetch not signed in', done => {
- getToken.returns(Promise.resolve());
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.bar, 'bar');
- assert.equal(Object.keys(options.headers).length, 0);
- done();
- });
- });
-
- test('fetch signed in', done => {
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/a/url?access_token=zbaz');
- assert.equal(options.bar, 'bar');
- done();
- });
- });
-
- test('getToken calls are cached', done => {
- Promise.all([
- auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
- assert.equal(getToken.callCount, 1);
- done();
- });
- });
-
- test('getToken refreshes token', done => {
- sandbox.stub(auth, '_isTokenValid');
- auth._isTokenValid
- .onFirstCall().returns(true)
- .onSecondCall()
- .returns(false)
- .onThirdCall()
- .returns(true);
- auth.fetch('/url-one')
- .then(() => {
- getToken.returns(Promise.resolve(makeToken('bzzbb')));
- return auth.fetch('/url-two');
- })
- .then(() => {
- const [[firstUrl], [secondUrl]] = fetch.args;
- assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
- assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
- done();
- });
- });
-
- test('signed in token error falls back to anonymous', done => {
- getToken.returns(Promise.resolve('rubbish'));
- auth.fetch('/url', {bar: 'bar'}).then(() => {
- const [url, options] = fetch.lastCall.args;
- assert.equal(url, '/url');
- assert.equal(options.bar, 'bar');
- done();
- });
- });
-
- test('_isTokenValid', () => {
- assert.isFalse(auth._isTokenValid());
- assert.isFalse(auth._isTokenValid({}));
- assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
- assert.isFalse(auth._isTokenValid({
- access_token: 'foo',
- expires_at: Date.now()/1000 - 1,
- }));
- assert.isTrue(auth._isTokenValid({
- access_token: 'foo',
- expires_at: Date.now()/1000 + 1,
- }));
- });
-
- test('HTTP PUT with content type', done => {
- const originalOptions = {
- method: 'PUT',
- headers: new Headers({'Content-Type': 'mail/pigeon'}),
- };
- auth.fetch('/url', originalOptions).then(() => {
- assert.isTrue(getToken.called);
- const [url, options] = fetch.lastCall.args;
- assert.include(url, '$ct=mail%2Fpigeon');
- assert.include(url, '$m=PUT');
- assert.include(url, 'access_token=zbaz');
- assert.equal(options.method, 'POST');
- assert.equal(options.headers.get('Content-Type'), 'text/plain');
- done();
- });
- });
-
- test('HTTP PUT without content type', done => {
- const originalOptions = {
- method: 'PUT',
- };
- auth.fetch('/url', originalOptions).then(() => {
- assert.isTrue(getToken.called);
- const [url, options] = fetch.lastCall.args;
- assert.include(url, '$ct=text%2Fplain');
- assert.include(url, '$m=PUT');
- assert.include(url, 'access_token=zbaz');
- assert.equal(options.method, 'POST');
- assert.equal(options.headers.get('Content-Type'), 'text/plain');
done();
});
});
});
});
+
+ suite('default (xsrf token header)', () => {
+ setup(() => {
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
+ });
+
+ test('GET', done => {
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.credentials, 'same-origin');
+ done();
+ });
+ });
+
+ test('POST', done => {
+ sandbox.stub(auth, '_getCookie')
+ .withArgs('XSRF_TOKEN')
+ .returns('foobar');
+ auth.fetch('/url', {method: 'POST'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.credentials, 'same-origin');
+ assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
+ done();
+ });
+ });
+ });
+
+ suite('cors (access token)', () => {
+ setup(() => {
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
+ });
+
+ let getToken;
+
+ const makeToken = opt_accessToken => {
+ return {
+ access_token: opt_accessToken || 'zbaz',
+ expires_at: new Date(Date.now() + 10e8).getTime(),
+ };
+ };
+
+ setup(() => {
+ getToken = sandbox.stub();
+ getToken.returns(Promise.resolve(makeToken()));
+ auth.setup(getToken);
+ });
+
+ test('base url support', done => {
+ const baseUrl = 'http://foo';
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl);
+ auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
+ const [url] = fetch.lastCall.args;
+ assert.equal(url, 'http://foo/a/url?access_token=zbaz');
+ done();
+ });
+ });
+
+ test('fetch not signed in', done => {
+ getToken.returns(Promise.resolve());
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.bar, 'bar');
+ assert.equal(Object.keys(options.headers).length, 0);
+ done();
+ });
+ });
+
+ test('fetch signed in', done => {
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/a/url?access_token=zbaz');
+ assert.equal(options.bar, 'bar');
+ done();
+ });
+ });
+
+ test('getToken calls are cached', done => {
+ Promise.all([
+ auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
+ assert.equal(getToken.callCount, 1);
+ done();
+ });
+ });
+
+ test('getToken refreshes token', done => {
+ sandbox.stub(auth, '_isTokenValid');
+ auth._isTokenValid
+ .onFirstCall().returns(true)
+ .onSecondCall()
+ .returns(false)
+ .onThirdCall()
+ .returns(true);
+ auth.fetch('/url-one')
+ .then(() => {
+ getToken.returns(Promise.resolve(makeToken('bzzbb')));
+ return auth.fetch('/url-two');
+ })
+ .then(() => {
+ const [[firstUrl], [secondUrl]] = fetch.args;
+ assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
+ assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
+ done();
+ });
+ });
+
+ test('signed in token error falls back to anonymous', done => {
+ getToken.returns(Promise.resolve('rubbish'));
+ auth.fetch('/url', {bar: 'bar'}).then(() => {
+ const [url, options] = fetch.lastCall.args;
+ assert.equal(url, '/url');
+ assert.equal(options.bar, 'bar');
+ done();
+ });
+ });
+
+ test('_isTokenValid', () => {
+ assert.isFalse(auth._isTokenValid());
+ assert.isFalse(auth._isTokenValid({}));
+ assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
+ assert.isFalse(auth._isTokenValid({
+ access_token: 'foo',
+ expires_at: Date.now()/1000 - 1,
+ }));
+ assert.isTrue(auth._isTokenValid({
+ access_token: 'foo',
+ expires_at: Date.now()/1000 + 1,
+ }));
+ });
+
+ test('HTTP PUT with content type', done => {
+ const originalOptions = {
+ method: 'PUT',
+ headers: new Headers({'Content-Type': 'mail/pigeon'}),
+ };
+ auth.fetch('/url', originalOptions).then(() => {
+ assert.isTrue(getToken.called);
+ const [url, options] = fetch.lastCall.args;
+ assert.include(url, '$ct=mail%2Fpigeon');
+ assert.include(url, '$m=PUT');
+ assert.include(url, 'access_token=zbaz');
+ assert.equal(options.method, 'POST');
+ assert.equal(options.headers.get('Content-Type'), 'text/plain');
+ done();
+ });
+ });
+
+ test('HTTP PUT without content type', done => {
+ const originalOptions = {
+ method: 'PUT',
+ };
+ auth.fetch('/url', originalOptions).then(() => {
+ assert.isTrue(getToken.called);
+ const [url, options] = fetch.lastCall.args;
+ assert.include(url, '$ct=text%2Fplain');
+ assert.include(url, '$m=PUT');
+ assert.include(url, 'access_token=zbaz');
+ assert.equal(options.method, 'POST');
+ assert.equal(options.headers.get('Content-Type'), 'text/plain');
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
deleted file mode 100644
index d3500d8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-@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.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-
-<dom-module id="gr-etag-decorator">
- <script src="gr-etag-decorator.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 7022d23..33c8d8e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -14,6 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../../../scripts/bundled-polymer.js';
+
+const $_documentContainer = document.createElement('template');
+
+$_documentContainer.innerHTML = `<dom-module id="gr-etag-decorator">
+
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
(function(window) {
'use strict';
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 21cbe89e..3eae300 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -19,83 +19,85 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-etag-decorator</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>
-<link rel="import" href="./gr-etag-decorator.html">
+<script type="module" src="./gr-etag-decorator.js"></script>
-<script>
- suite('gr-etag-decorator', async () => {
- await readyToTest();
- let etag;
- let sandbox;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-etag-decorator.js';
+suite('gr-etag-decorator', () => {
+ let etag;
+ let sandbox;
- const fakeRequest = (opt_etag, opt_status) => {
- const headers = new Headers();
- if (opt_etag) {
- headers.set('etag', opt_etag);
- }
- const status = opt_status || 200;
- return {ok: true, status, headers};
- };
+ const fakeRequest = (opt_etag, opt_status) => {
+ const headers = new Headers();
+ if (opt_etag) {
+ headers.set('etag', opt_etag);
+ }
+ const status = opt_status || 200;
+ return {ok: true, status, headers};
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
- etag = new GrEtagDecorator();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('exists', () => {
- assert.isOk(etag);
- });
-
- test('works', () => {
- etag.collect('/foo', fakeRequest('bar'));
- const options = etag.getOptions('/foo');
- assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
- });
-
- test('updates etags', () => {
- etag.collect('/foo', fakeRequest('bar'));
- etag.collect('/foo', fakeRequest('baz'));
- const options = etag.getOptions('/foo');
- assert.strictEqual(options.headers.get('If-None-Match'), 'baz');
- });
-
- test('discards empty etags', () => {
- etag.collect('/foo', fakeRequest('bar'));
- etag.collect('/foo', fakeRequest());
- const options = etag.getOptions('/foo', {headers: new Headers()});
- assert.isNull(options.headers.get('If-None-Match'));
- });
-
- test('discards etags in order used', () => {
- etag.collect('/foo', fakeRequest('bar'));
- _.times(29, i => {
- etag.collect('/qaz/' + i, fakeRequest('qaz'));
- });
- let options = etag.getOptions('/foo');
- assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
- etag.collect('/zaq', fakeRequest('zaq'));
- options = etag.getOptions('/foo', {headers: new Headers()});
- assert.isNull(options.headers.get('If-None-Match'));
- });
-
- test('getCachedPayload', () => {
- const payload = 'payload';
- etag.collect('/foo', fakeRequest('bar'), payload);
- assert.strictEqual(etag.getCachedPayload('/foo'), payload);
- etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
- assert.strictEqual(etag.getCachedPayload('/foo'), payload);
- etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
- assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ etag = new GrEtagDecorator();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(etag);
+ });
+
+ test('works', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ const options = etag.getOptions('/foo');
+ assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
+ });
+
+ test('updates etags', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ etag.collect('/foo', fakeRequest('baz'));
+ const options = etag.getOptions('/foo');
+ assert.strictEqual(options.headers.get('If-None-Match'), 'baz');
+ });
+
+ test('discards empty etags', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ etag.collect('/foo', fakeRequest());
+ const options = etag.getOptions('/foo', {headers: new Headers()});
+ assert.isNull(options.headers.get('If-None-Match'));
+ });
+
+ test('discards etags in order used', () => {
+ etag.collect('/foo', fakeRequest('bar'));
+ _.times(29, i => {
+ etag.collect('/qaz/' + i, fakeRequest('qaz'));
+ });
+ let options = etag.getOptions('/foo');
+ assert.strictEqual(options.headers.get('If-None-Match'), 'bar');
+ etag.collect('/zaq', fakeRequest('zaq'));
+ options = etag.getOptions('/foo', {headers: new Headers()});
+ assert.isNull(options.headers.get('If-None-Match'));
+ });
+
+ test('getCachedPayload', () => {
+ const payload = 'payload';
+ etag.collect('/foo', fakeRequest('bar'), payload);
+ assert.strictEqual(etag.getCachedPayload('/foo'), payload);
+ etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
+ assert.strictEqual(etag.getCachedPayload('/foo'), payload);
+ etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
+ assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
deleted file mode 100644
index 2047f91..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ /dev/null
@@ -1,37 +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">
-<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-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
-<link rel="import" href="gr-etag-decorator.html">
-
-<!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 -->
-<script src="/bower_components/es6-promise/dist/es6-promise.min.js"></script>
-<script src="/bower_components/fetch/fetch.js"></script>
-
-<!-- NB: Order is important, because of namespaced classes. -->
-<script src="gr-rest-apis/gr-rest-api-helper.js"></script>
-<script src="gr-auth.js"></script>
-<script src="gr-reviewer-updates-parser.js"></script>
-
-<dom-module id="gr-rest-api-interface">
- <script src="gr-rest-api-interface.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 5b88d34..3e78bd3 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,2756 +14,2777 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+/* NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */
+/* NB: Order is important, because of namespaced classes. */
+/*
+ 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 '../../../scripts/bundled-polymer.js';
- const DiffViewMode = {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- };
- const JSON_PREFIX = ')]}\'';
- const MAX_PROJECT_RESULTS = 25;
- // This value is somewhat arbitrary and not based on research or calculations.
- const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
- const PARENT_PATCH_NUM = 'PARENT';
+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-path-list-behavior/gr-path-list-behavior.js';
+import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import './gr-etag-decorator.js';
+import './gr-rest-apis/gr-rest-api-helper.js';
+import './gr-auth.js';
+import './gr-reviewer-updates-parser.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 'es6-promise/lib/es6-promise.js';
+import 'whatwg-fetch/fetch.js';
- const Requests = {
- SEND_DIFF_DRAFT: 'sendDiffDraft',
- };
+const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+};
+const JSON_PREFIX = ')]}\'';
+const MAX_PROJECT_RESULTS = 25;
+// This value is somewhat arbitrary and not based on research or calculations.
+const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
+const PARENT_PATCH_NUM = 'PARENT';
- const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE =
- 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
- const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
+const Requests = {
+ SEND_DIFF_DRAFT: 'sendDiffDraft',
+};
- const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*';
- const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
- '/revisions/*';
+const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE =
+ 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
+const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
+
+const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*';
+const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
+ '/revisions/*';
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.PathListMixin
+ * @appliesMixin Gerrit.PatchSetMixin
+ * @appliesMixin Gerrit.RESTClientMixin
+ * @extends Polymer.Element
+ */
+class GrRestApiInterface extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.PathListBehavior,
+ Gerrit.PatchSetBehavior,
+ Gerrit.RESTClientBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get is() { return 'gr-rest-api-interface'; }
+ /**
+ * Fired when an server error occurs.
+ *
+ * @event server-error
+ */
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.PathListMixin
- * @appliesMixin Gerrit.PatchSetMixin
- * @appliesMixin Gerrit.RESTClientMixin
- * @extends Polymer.Element
+ * Fired when a network error occurs.
+ *
+ * @event network-error
*/
- class GrRestApiInterface extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.PathListBehavior,
- Gerrit.PatchSetBehavior,
- Gerrit.RESTClientBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-rest-api-interface'; }
- /**
- * Fired when an server error occurs.
- *
- * @event server-error
- */
- /**
- * Fired when a network error occurs.
- *
- * @event network-error
- */
+ /**
+ * Fired after an RPC completes.
+ *
+ * @event rpc-log
+ */
- /**
- * Fired after an RPC completes.
- *
- * @event rpc-log
- */
+ constructor() {
+ super();
+ this.JSON_PREFIX = JSON_PREFIX;
+ }
- constructor() {
- super();
- this.JSON_PREFIX = JSON_PREFIX;
+ static get properties() {
+ return {
+ _cache: {
+ type: Object,
+ value: new SiteBasedCache(), // Shared across instances.
+ },
+ _sharedFetchPromises: {
+ type: Object,
+ value: new FetchPromisesCache(), // Shared across instances.
+ },
+ _pendingRequests: {
+ type: Object,
+ value: {}, // Intentional to share the object across instances.
+ },
+ _etags: {
+ type: Object,
+ value: new GrEtagDecorator(), // Share across instances.
+ },
+ /**
+ * Used to maintain a mapping of changeNums to project names.
+ */
+ _projectLookup: {
+ type: Object,
+ value: {}, // Intentional to share the object across instances.
+ },
+ };
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this._auth = Gerrit.Auth;
+ this._initRestApiHelper();
+ }
+
+ _initRestApiHelper() {
+ if (this._restApiHelper) {
+ return;
}
-
- static get properties() {
- return {
- _cache: {
- type: Object,
- value: new SiteBasedCache(), // Shared across instances.
- },
- _sharedFetchPromises: {
- type: Object,
- value: new FetchPromisesCache(), // Shared across instances.
- },
- _pendingRequests: {
- type: Object,
- value: {}, // Intentional to share the object across instances.
- },
- _etags: {
- type: Object,
- value: new GrEtagDecorator(), // Share across instances.
- },
- /**
- * Used to maintain a mapping of changeNums to project names.
- */
- _projectLookup: {
- type: Object,
- value: {}, // Intentional to share the object across instances.
- },
- };
+ if (this._cache && this._auth && this._sharedFetchPromises) {
+ this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
+ this._sharedFetchPromises, this);
}
+ }
- /** @override */
- created() {
- super.created();
- this._auth = Gerrit.Auth;
- this._initRestApiHelper();
- }
+ _fetchSharedCacheURL(req) {
+ // Cache is shared across instances
+ return this._restApiHelper.fetchCacheURL(req);
+ }
- _initRestApiHelper() {
- if (this._restApiHelper) {
- return;
- }
- if (this._cache && this._auth && this._sharedFetchPromises) {
- this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
- this._sharedFetchPromises, this);
- }
- }
+ /**
+ * @param {!Object} response
+ * @return {?}
+ */
+ getResponseObject(response) {
+ return this._restApiHelper.getResponseObject(response);
+ }
- _fetchSharedCacheURL(req) {
- // Cache is shared across instances
- return this._restApiHelper.fetchCacheURL(req);
- }
-
- /**
- * @param {!Object} response
- * @return {?}
- */
- getResponseObject(response) {
- return this._restApiHelper.getResponseObject(response);
- }
-
- getConfig(noCache) {
- if (!noCache) {
- return this._fetchSharedCacheURL({
- url: '/config/server/info',
- reportUrlAsIs: true,
- });
- }
-
- return this._restApiHelper.fetchJSON({
+ getConfig(noCache) {
+ if (!noCache) {
+ return this._fetchSharedCacheURL({
url: '/config/server/info',
reportUrlAsIs: true,
});
}
- getRepo(repo, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: '/projects/' + encodeURIComponent(repo),
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*',
- });
- }
+ return this._restApiHelper.fetchJSON({
+ url: '/config/server/info',
+ reportUrlAsIs: true,
+ });
+ }
- getProjectConfig(repo, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: '/projects/' + encodeURIComponent(repo) + '/config',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/config',
- });
- }
+ getRepo(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/projects/' + encodeURIComponent(repo),
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*',
+ });
+ }
- getRepoAccess(repo) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: '/access/?project=' + encodeURIComponent(repo),
- anonymizedUrl: '/access/?project=*',
- });
- }
+ getProjectConfig(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/projects/' + encodeURIComponent(repo) + '/config',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/config',
+ });
+ }
- getRepoDashboards(repo, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/dashboards?inherited',
- });
- }
+ getRepoAccess(repo) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: '/access/?project=' + encodeURIComponent(repo),
+ anonymizedUrl: '/access/?project=*',
+ });
+ }
- saveRepoConfig(repo, config, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const url = `/projects/${encodeURIComponent(repo)}/config`;
- this._cache.delete(url);
- return this._restApiHelper.send({
- method: 'PUT',
- url,
- body: config,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/config',
- });
- }
+ getRepoDashboards(repo, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/dashboards?inherited',
+ });
+ }
- runRepoGC(repo, opt_errFn) {
- if (!repo) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(repo);
- return this._restApiHelper.send({
- method: 'POST',
- url: `/projects/${encodeName}/gc`,
- body: '',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/gc',
- });
- }
+ saveRepoConfig(repo, config, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const url = `/projects/${encodeURIComponent(repo)}/config`;
+ this._cache.delete(url);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/config',
+ });
+ }
- /**
- * @param {?Object} config
- * @param {function(?Response, string=)=} opt_errFn
- */
- createRepo(config, opt_errFn) {
- if (!config.name) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(config.name);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeName}`,
- body: config,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*',
- });
- }
+ runRepoGC(repo, opt_errFn) {
+ if (!repo) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: `/projects/${encodeName}/gc`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/gc',
+ });
+ }
- /**
- * @param {?Object} config
- * @param {function(?Response, string=)=} opt_errFn
- */
- createGroup(config, opt_errFn) {
- if (!config.name) { return ''; }
- const encodeName = encodeURIComponent(config.name);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeName}`,
- body: config,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*',
- });
- }
+ /**
+ * @param {?Object} config
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createRepo(config, opt_errFn) {
+ if (!config.name) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(config.name);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeName}`,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*',
+ });
+ }
- getGroupConfig(group, opt_errFn) {
- return this._restApiHelper.fetchJSON({
- url: `/groups/${encodeURIComponent(group)}/detail`,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/detail',
- });
- }
+ /**
+ * @param {?Object} config
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createGroup(config, opt_errFn) {
+ if (!config.name) { return ''; }
+ const encodeName = encodeURIComponent(config.name);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeName}`,
+ body: config,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*',
+ });
+ }
- /**
- * @param {string} repo
- * @param {string} ref
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteRepoBranches(repo, ref, opt_errFn) {
- if (!repo || !ref) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(repo);
- const encodeRef = encodeURIComponent(ref);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/projects/${encodeName}/branches/${encodeRef}`,
- body: '',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/branches/*',
- });
- }
+ getGroupConfig(group, opt_errFn) {
+ return this._restApiHelper.fetchJSON({
+ url: `/groups/${encodeURIComponent(group)}/detail`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/detail',
+ });
+ }
- /**
- * @param {string} repo
- * @param {string} ref
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteRepoTags(repo, ref, opt_errFn) {
- if (!repo || !ref) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(repo);
- const encodeRef = encodeURIComponent(ref);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/projects/${encodeName}/tags/${encodeRef}`,
- body: '',
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/tags/*',
- });
- }
+ /**
+ * @param {string} repo
+ * @param {string} ref
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteRepoBranches(repo, ref, opt_errFn) {
+ if (!repo || !ref) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ const encodeRef = encodeURIComponent(ref);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/projects/${encodeName}/branches/${encodeRef}`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches/*',
+ });
+ }
- /**
- * @param {string} name
- * @param {string} branch
- * @param {string} revision
- * @param {function(?Response, string=)=} opt_errFn
- */
- createRepoBranch(name, branch, revision, opt_errFn) {
- if (!name || !branch || !revision) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(name);
- const encodeBranch = encodeURIComponent(branch);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeName}/branches/${encodeBranch}`,
- body: revision,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/branches/*',
- });
- }
+ /**
+ * @param {string} repo
+ * @param {string} ref
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteRepoTags(repo, ref, opt_errFn) {
+ if (!repo || !ref) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(repo);
+ const encodeRef = encodeURIComponent(ref);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/projects/${encodeName}/tags/${encodeRef}`,
+ body: '',
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags/*',
+ });
+ }
- /**
- * @param {string} name
- * @param {string} tag
- * @param {string} revision
- * @param {function(?Response, string=)=} opt_errFn
- */
- createRepoTag(name, tag, revision, opt_errFn) {
- if (!name || !tag || !revision) { return ''; }
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- const encodeName = encodeURIComponent(name);
- const encodeTag = encodeURIComponent(tag);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeName}/tags/${encodeTag}`,
- body: revision,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/tags/*',
- });
- }
+ /**
+ * @param {string} name
+ * @param {string} branch
+ * @param {string} revision
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createRepoBranch(name, branch, revision, opt_errFn) {
+ if (!name || !branch || !revision) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(name);
+ const encodeBranch = encodeURIComponent(branch);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeName}/branches/${encodeBranch}`,
+ body: revision,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches/*',
+ });
+ }
- /**
- * @param {!string} groupName
- * @returns {!Promise<boolean>}
- */
- getIsGroupOwner(groupName) {
- const encodeName = encodeURIComponent(groupName);
- const req = {
- url: `/groups/?owned&g=${encodeName}`,
- anonymizedUrl: '/groups/owned&g=*',
- };
- return this._fetchSharedCacheURL(req)
- .then(configs => configs.hasOwnProperty(groupName));
- }
+ /**
+ * @param {string} name
+ * @param {string} tag
+ * @param {string} revision
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ createRepoTag(name, tag, revision, opt_errFn) {
+ if (!name || !tag || !revision) { return ''; }
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ const encodeName = encodeURIComponent(name);
+ const encodeTag = encodeURIComponent(tag);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeName}/tags/${encodeTag}`,
+ body: revision,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags/*',
+ });
+ }
- getGroupMembers(groupName, opt_errFn) {
- const encodeName = encodeURIComponent(groupName);
- return this._restApiHelper.fetchJSON({
- url: `/groups/${encodeName}/members/`,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/members',
- });
- }
+ /**
+ * @param {!string} groupName
+ * @returns {!Promise<boolean>}
+ */
+ getIsGroupOwner(groupName) {
+ const encodeName = encodeURIComponent(groupName);
+ const req = {
+ url: `/groups/?owned&g=${encodeName}`,
+ anonymizedUrl: '/groups/owned&g=*',
+ };
+ return this._fetchSharedCacheURL(req)
+ .then(configs => configs.hasOwnProperty(groupName));
+ }
- getIncludedGroup(groupName) {
- return this._restApiHelper.fetchJSON({
- url: `/groups/${encodeURIComponent(groupName)}/groups/`,
- anonymizedUrl: '/groups/*/groups',
- });
- }
+ getGroupMembers(groupName, opt_errFn) {
+ const encodeName = encodeURIComponent(groupName);
+ return this._restApiHelper.fetchJSON({
+ url: `/groups/${encodeName}/members/`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/members',
+ });
+ }
- saveGroupName(groupId, name) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/name`,
- body: {name},
- anonymizedUrl: '/groups/*/name',
- });
- }
+ getIncludedGroup(groupName) {
+ return this._restApiHelper.fetchJSON({
+ url: `/groups/${encodeURIComponent(groupName)}/groups/`,
+ anonymizedUrl: '/groups/*/groups',
+ });
+ }
- saveGroupOwner(groupId, ownerId) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/owner`,
- body: {owner: ownerId},
- anonymizedUrl: '/groups/*/owner',
- });
- }
+ saveGroupName(groupId, name) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/name`,
+ body: {name},
+ anonymizedUrl: '/groups/*/name',
+ });
+ }
- saveGroupDescription(groupId, description) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/description`,
- body: {description},
- anonymizedUrl: '/groups/*/description',
- });
- }
+ saveGroupOwner(groupId, ownerId) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/owner`,
+ body: {owner: ownerId},
+ anonymizedUrl: '/groups/*/owner',
+ });
+ }
- saveGroupOptions(groupId, options) {
- const encodeId = encodeURIComponent(groupId);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeId}/options`,
- body: options,
- anonymizedUrl: '/groups/*/options',
- });
- }
+ saveGroupDescription(groupId, description) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/description`,
+ body: {description},
+ anonymizedUrl: '/groups/*/description',
+ });
+ }
- getGroupAuditLog(group, opt_errFn) {
- return this._fetchSharedCacheURL({
- url: '/groups/' + group + '/log.audit',
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/log.audit',
- });
- }
+ saveGroupOptions(groupId, options) {
+ const encodeId = encodeURIComponent(groupId);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeId}/options`,
+ body: options,
+ anonymizedUrl: '/groups/*/options',
+ });
+ }
- saveGroupMembers(groupName, groupMembers) {
- const encodeName = encodeURIComponent(groupName);
- const encodeMember = encodeURIComponent(groupMembers);
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/groups/${encodeName}/members/${encodeMember}`,
- parseResponse: true,
- anonymizedUrl: '/groups/*/members/*',
- });
- }
+ getGroupAuditLog(group, opt_errFn) {
+ return this._fetchSharedCacheURL({
+ url: '/groups/' + group + '/log.audit',
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/log.audit',
+ });
+ }
- saveIncludedGroup(groupName, includedGroup, opt_errFn) {
- const encodeName = encodeURIComponent(groupName);
- const encodeIncludedGroup = encodeURIComponent(includedGroup);
- const req = {
- method: 'PUT',
- url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
- errFn: opt_errFn,
- anonymizedUrl: '/groups/*/groups/*',
- };
- return this._restApiHelper.send(req).then(response => {
- if (response.ok) {
- return this.getResponseObject(response);
- }
- });
- }
+ saveGroupMembers(groupName, groupMembers) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeMember = encodeURIComponent(groupMembers);
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/groups/${encodeName}/members/${encodeMember}`,
+ parseResponse: true,
+ anonymizedUrl: '/groups/*/members/*',
+ });
+ }
- deleteGroupMembers(groupName, groupMembers) {
- const encodeName = encodeURIComponent(groupName);
- const encodeMember = encodeURIComponent(groupMembers);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/groups/${encodeName}/members/${encodeMember}`,
- anonymizedUrl: '/groups/*/members/*',
- });
- }
-
- deleteIncludedGroup(groupName, includedGroup) {
- const encodeName = encodeURIComponent(groupName);
- const encodeIncludedGroup = encodeURIComponent(includedGroup);
- return this._restApiHelper.send({
- method: 'DELETE',
- url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
- anonymizedUrl: '/groups/*/groups/*',
- });
- }
-
- getVersion() {
- return this._fetchSharedCacheURL({
- url: '/config/server/version',
- reportUrlAsIs: true,
- });
- }
-
- getDiffPreferences() {
- return this.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/preferences.diff',
- reportUrlAsIs: true,
- });
- }
- // These defaults should match the defaults in
- // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
- // NOTE: There are some settings that don't apply to PolyGerrit
- // (Render mode being at least one of them).
- return Promise.resolve({
- auto_hide_diff_table_header: true,
- context: 10,
- cursor_blink_rate: 0,
- font_size: 12,
- ignore_whitespace: 'IGNORE_NONE',
- intraline_difference: true,
- line_length: 100,
- line_wrapping: false,
- show_line_endings: true,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- });
- });
- }
-
- getEditPreferences() {
- return this.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/preferences.edit',
- reportUrlAsIs: true,
- });
- }
- // These defaults should match the defaults in
- // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
- return Promise.resolve({
- auto_close_brackets: false,
- cursor_blink_rate: 0,
- hide_line_numbers: false,
- hide_top_menu: false,
- indent_unit: 2,
- indent_with_tabs: false,
- key_map_type: 'DEFAULT',
- line_length: 100,
- line_wrapping: false,
- match_brackets: true,
- show_base: false,
- show_tabs: true,
- show_whitespace_errors: true,
- syntax_highlighting: true,
- tab_size: 8,
- theme: 'DEFAULT',
- });
- });
- }
-
- /**
- * @param {?Object} prefs
- * @param {function(?Response, string=)=} opt_errFn
- */
- savePreferences(prefs, opt_errFn) {
- // Note (Issue 5142): normalize the download scheme with lower case before
- // saving.
- if (prefs.download_scheme) {
- prefs.download_scheme = prefs.download_scheme.toLowerCase();
+ saveIncludedGroup(groupName, includedGroup, opt_errFn) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeIncludedGroup = encodeURIComponent(includedGroup);
+ const req = {
+ method: 'PUT',
+ url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/groups/*/groups/*',
+ };
+ return this._restApiHelper.send(req).then(response => {
+ if (response.ok) {
+ return this.getResponseObject(response);
}
+ });
+ }
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/preferences',
- body: prefs,
- errFn: opt_errFn,
- reportUrlAsIs: true,
+ deleteGroupMembers(groupName, groupMembers) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeMember = encodeURIComponent(groupMembers);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/groups/${encodeName}/members/${encodeMember}`,
+ anonymizedUrl: '/groups/*/members/*',
+ });
+ }
+
+ deleteIncludedGroup(groupName, includedGroup) {
+ const encodeName = encodeURIComponent(groupName);
+ const encodeIncludedGroup = encodeURIComponent(includedGroup);
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
+ anonymizedUrl: '/groups/*/groups/*',
+ });
+ }
+
+ getVersion() {
+ return this._fetchSharedCacheURL({
+ url: '/config/server/version',
+ reportUrlAsIs: true,
+ });
+ }
+
+ getDiffPreferences() {
+ return this.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/preferences.diff',
+ reportUrlAsIs: true,
+ });
+ }
+ // These defaults should match the defaults in
+ // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+ // NOTE: There are some settings that don't apply to PolyGerrit
+ // (Render mode being at least one of them).
+ return Promise.resolve({
+ auto_hide_diff_table_header: true,
+ context: 10,
+ cursor_blink_rate: 0,
+ font_size: 12,
+ ignore_whitespace: 'IGNORE_NONE',
+ intraline_difference: true,
+ line_length: 100,
+ line_wrapping: false,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
});
+ });
+ }
+
+ getEditPreferences() {
+ return this.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/preferences.edit',
+ reportUrlAsIs: true,
+ });
+ }
+ // These defaults should match the defaults in
+ // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+ return Promise.resolve({
+ auto_close_brackets: false,
+ cursor_blink_rate: 0,
+ hide_line_numbers: false,
+ hide_top_menu: false,
+ indent_unit: 2,
+ indent_with_tabs: false,
+ key_map_type: 'DEFAULT',
+ line_length: 100,
+ line_wrapping: false,
+ match_brackets: true,
+ show_base: false,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ });
+ });
+ }
+
+ /**
+ * @param {?Object} prefs
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ savePreferences(prefs, opt_errFn) {
+ // Note (Issue 5142): normalize the download scheme with lower case before
+ // saving.
+ if (prefs.download_scheme) {
+ prefs.download_scheme = prefs.download_scheme.toLowerCase();
}
- /**
- * @param {?Object} prefs
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveDiffPreferences(prefs, opt_errFn) {
- // Invalidate the cache.
- this._cache.delete('/accounts/self/preferences.diff');
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/preferences.diff',
- body: prefs,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/preferences',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
- /**
- * @param {?Object} prefs
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveEditPreferences(prefs, opt_errFn) {
- // Invalidate the cache.
- this._cache.delete('/accounts/self/preferences.edit');
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/preferences.edit',
- body: prefs,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
+ /**
+ * @param {?Object} prefs
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveDiffPreferences(prefs, opt_errFn) {
+ // Invalidate the cache.
+ this._cache.delete('/accounts/self/preferences.diff');
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/preferences.diff',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
- getAccount() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/detail',
- reportUrlAsIs: true,
- errFn: resp => {
- if (!resp || resp.status === 403) {
- this._cache.delete('/accounts/self/detail');
- }
- },
- });
- }
+ /**
+ * @param {?Object} prefs
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveEditPreferences(prefs, opt_errFn) {
+ // Invalidate the cache.
+ this._cache.delete('/accounts/self/preferences.edit');
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/preferences.edit',
+ body: prefs,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
- getAvatarChangeUrl() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/avatar.change.url',
- reportUrlAsIs: true,
- errFn: resp => {
- if (!resp || resp.status === 403) {
- this._cache.delete('/accounts/self/avatar.change.url');
- }
- },
- });
- }
-
- getExternalIds() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/external.ids',
- reportUrlAsIs: true,
- });
- }
-
- deleteAccountIdentity(id) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/external.ids:delete',
- body: id,
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} userId the ID of the user usch as an email address.
- * @return {!Promise<!Object>}
- */
- getAccountDetails(userId) {
- return this._restApiHelper.fetchJSON({
- url: `/accounts/${encodeURIComponent(userId)}/detail`,
- anonymizedUrl: '/accounts/*/detail',
- });
- }
-
- getAccountEmails() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/emails',
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} email
- * @param {function(?Response, string=)=} opt_errFn
- */
- addAccountEmail(email, opt_errFn) {
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/emails/' + encodeURIComponent(email),
- errFn: opt_errFn,
- anonymizedUrl: '/account/self/emails/*',
- });
- }
-
- /**
- * @param {string} email
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteAccountEmail(email, opt_errFn) {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/emails/' + encodeURIComponent(email),
- errFn: opt_errFn,
- anonymizedUrl: '/accounts/self/email/*',
- });
- }
-
- /**
- * @param {string} email
- * @param {function(?Response, string=)=} opt_errFn
- */
- setPreferredAccountEmail(email, opt_errFn) {
- const encodedEmail = encodeURIComponent(email);
- const req = {
- method: 'PUT',
- url: `/accounts/self/emails/${encodedEmail}/preferred`,
- errFn: opt_errFn,
- anonymizedUrl: '/accounts/self/emails/*/preferred',
- };
- return this._restApiHelper.send(req).then(() => {
- // If result of getAccountEmails is in cache, update it in the cache
- // so we don't have to invalidate it.
- const cachedEmails = this._cache.get('/accounts/self/emails');
- if (cachedEmails) {
- const emails = cachedEmails.map(entry => {
- if (entry.email === email) {
- return {email, preferred: true};
- } else {
- return {email};
- }
- });
- this._cache.set('/accounts/self/emails', emails);
+ getAccount() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/detail',
+ reportUrlAsIs: true,
+ errFn: resp => {
+ if (!resp || resp.status === 403) {
+ this._cache.delete('/accounts/self/detail');
}
- });
- }
+ },
+ });
+ }
- /**
- * @param {?Object} obj
- */
- _updateCachedAccount(obj) {
- // If result of getAccount is in cache, update it in the cache
+ getAvatarChangeUrl() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/avatar.change.url',
+ reportUrlAsIs: true,
+ errFn: resp => {
+ if (!resp || resp.status === 403) {
+ this._cache.delete('/accounts/self/avatar.change.url');
+ }
+ },
+ });
+ }
+
+ getExternalIds() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/external.ids',
+ reportUrlAsIs: true,
+ });
+ }
+
+ deleteAccountIdentity(id) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/external.ids:delete',
+ body: id,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} userId the ID of the user usch as an email address.
+ * @return {!Promise<!Object>}
+ */
+ getAccountDetails(userId) {
+ return this._restApiHelper.fetchJSON({
+ url: `/accounts/${encodeURIComponent(userId)}/detail`,
+ anonymizedUrl: '/accounts/*/detail',
+ });
+ }
+
+ getAccountEmails() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/emails',
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} email
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ addAccountEmail(email, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/emails/' + encodeURIComponent(email),
+ errFn: opt_errFn,
+ anonymizedUrl: '/account/self/emails/*',
+ });
+ }
+
+ /**
+ * @param {string} email
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteAccountEmail(email, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/emails/' + encodeURIComponent(email),
+ errFn: opt_errFn,
+ anonymizedUrl: '/accounts/self/email/*',
+ });
+ }
+
+ /**
+ * @param {string} email
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setPreferredAccountEmail(email, opt_errFn) {
+ const encodedEmail = encodeURIComponent(email);
+ const req = {
+ method: 'PUT',
+ url: `/accounts/self/emails/${encodedEmail}/preferred`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/accounts/self/emails/*/preferred',
+ };
+ return this._restApiHelper.send(req).then(() => {
+ // If result of getAccountEmails is in cache, update it in the cache
// so we don't have to invalidate it.
- const cachedAccount = this._cache.get('/accounts/self/detail');
- if (cachedAccount) {
- // Replace object in cache with new object to force UI updates.
- this._cache.set('/accounts/self/detail',
- Object.assign({}, cachedAccount, obj));
- }
- }
-
- /**
- * @param {string} name
- * @param {function(?Response, string=)=} opt_errFn
- */
- setAccountName(name, opt_errFn) {
- const req = {
- method: 'PUT',
- url: '/accounts/self/name',
- body: {name},
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(newName => this._updateCachedAccount({name: newName}));
- }
-
- /**
- * @param {string} username
- * @param {function(?Response, string=)=} opt_errFn
- */
- setAccountUsername(username, opt_errFn) {
- const req = {
- method: 'PUT',
- url: '/accounts/self/username',
- body: {username},
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(newName => this._updateCachedAccount({username: newName}));
- }
-
- /**
- * @param {string} status
- * @param {function(?Response, string=)=} opt_errFn
- */
- setAccountStatus(status, opt_errFn) {
- const req = {
- method: 'PUT',
- url: '/accounts/self/status',
- body: {status},
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(newStatus => this._updateCachedAccount({status: newStatus}));
- }
-
- getAccountStatus(userId) {
- return this._restApiHelper.fetchJSON({
- url: `/accounts/${encodeURIComponent(userId)}/status`,
- anonymizedUrl: '/accounts/*/status',
- });
- }
-
- getAccountGroups() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/groups',
- reportUrlAsIs: true,
- });
- }
-
- getAccountAgreements() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/agreements',
- reportUrlAsIs: true,
- });
- }
-
- saveAccountAgreement(name) {
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/agreements',
- body: name,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string=} opt_params
- */
- getAccountCapabilities(opt_params) {
- let queryString = '';
- if (opt_params) {
- queryString = '?q=' + opt_params
- .map(param => encodeURIComponent(param))
- .join('&q=');
- }
- return this._fetchSharedCacheURL({
- url: '/accounts/self/capabilities' + queryString,
- anonymizedUrl: '/accounts/self/capabilities?q=*',
- });
- }
-
- getLoggedIn() {
- return this._auth.authCheck();
- }
-
- getIsAdmin() {
- return this.getLoggedIn()
- .then(isLoggedIn => {
- if (isLoggedIn) {
- return this.getAccountCapabilities();
- } else {
- return Promise.resolve();
- }
- })
- .then(
- capabilities => capabilities && capabilities.administrateServer
- );
- }
-
- getDefaultPreferences() {
- return this._fetchSharedCacheURL({
- url: '/config/server/preferences',
- reportUrlAsIs: true,
- });
- }
-
- getPreferences() {
- return this.getLoggedIn().then(loggedIn => {
- if (loggedIn) {
- const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
- return this._fetchSharedCacheURL(req).then(res => {
- if (this._isNarrowScreen()) {
- // Note that this can be problematic, because the diff will stay
- // unified even after increasing the window width.
- res.default_diff_view = DiffViewMode.UNIFIED;
- } else {
- res.default_diff_view = res.diff_view;
- }
- return Promise.resolve(res);
- });
- }
-
- return Promise.resolve({
- changes_per_page: 25,
- default_diff_view: this._isNarrowScreen() ?
- DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE,
- diff_view: 'SIDE_BY_SIDE',
- size_bar_in_change_table: true,
- });
- });
- }
-
- getWatchedProjects() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/watched.projects',
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} projects
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveWatchedProjects(projects, opt_errFn) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/watched.projects',
- body: projects,
- errFn: opt_errFn,
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} projects
- * @param {function(?Response, string=)=} opt_errFn
- */
- deleteWatchedProjects(projects, opt_errFn) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/watched.projects:delete',
- body: projects,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- _isNarrowScreen() {
- return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
- }
-
- /**
- * @param {number=} opt_changesPerPage
- * @param {string|!Array<string>=} opt_query A query or an array of queries.
- * @param {number|string=} opt_offset
- * @param {!Object=} opt_options
- * @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an
- * array, _fetchJSON will return an array of arrays of changeInfos. If it
- * is unspecified or a string, _fetchJSON will return an array of
- * changeInfos.
- */
- getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) {
- const options = opt_options || this.listChangesOptionsToHex(
- this.ListChangesOption.LABELS,
- this.ListChangesOption.DETAILED_ACCOUNTS
- );
- // Issue 4524: respect legacy token with max sortkey.
- if (opt_offset === 'n,z') {
- opt_offset = 0;
- }
- const params = {
- O: options,
- S: opt_offset || 0,
- };
- if (opt_changesPerPage) { params.n = opt_changesPerPage; }
- if (opt_query && opt_query.length > 0) {
- params.q = opt_query;
- }
- const iterateOverChanges = arr => {
- for (const change of (arr || [])) {
- this._maybeInsertInLookup(change);
- }
- };
- const req = {
- url: '/changes/',
- params,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.fetchJSON(req).then(response => {
- // Response may be an array of changes OR an array of arrays of
- // changes.
- if (opt_query instanceof Array) {
- // Normalize the response to look like a multi-query response
- // when there is only one query.
- if (opt_query.length === 1) {
- response = [response];
- }
- for (const arr of response) {
- iterateOverChanges(arr);
- }
- } else {
- iterateOverChanges(response);
- }
- return response;
- });
- }
-
- /**
- * Inserts a change into _projectLookup iff it has a valid structure.
- *
- * @param {?{ _number: (number|string) }} change
- */
- _maybeInsertInLookup(change) {
- if (change && change.project && change._number) {
- this.setInProjectLookup(change._number, change.project);
- }
- }
-
- /**
- * TODO (beckysiegel) this needs to be rewritten with the optional param
- * at the end.
- *
- * @param {number|string} changeNum
- * @param {?number|string=} opt_patchNum passed as null sometimes.
- * @param {?=} endpoint
- * @return {!Promise<string>}
- */
- getChangeActionURL(changeNum, opt_patchNum, endpoint) {
- return this._changeBaseURL(changeNum, opt_patchNum)
- .then(url => url + endpoint);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
- */
- getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
- return this.getConfig(false).then(config => {
- const optionsHex = this._getChangeOptionsHex(config);
- return this._getChangeDetail(
- changeNum, optionsHex, opt_errFn, opt_cancelCondition)
- .then(GrReviewerUpdatesParser.parse);
- });
- }
-
- _getChangeOptionsHex(config) {
- if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage
- && !(config.receive && config.receive.enable_signed_push)) {
- return window.DEFAULT_DETAIL_HEXES.changePage;
- }
-
- // This list MUST be kept in sync with
- // ChangeIT#changeDetailsDoesNotRequireIndex
- const options = [
- this.ListChangesOption.ALL_COMMITS,
- this.ListChangesOption.ALL_REVISIONS,
- this.ListChangesOption.CHANGE_ACTIONS,
- this.ListChangesOption.DETAILED_LABELS,
- this.ListChangesOption.DOWNLOAD_COMMANDS,
- this.ListChangesOption.MESSAGES,
- this.ListChangesOption.SUBMITTABLE,
- this.ListChangesOption.WEB_LINKS,
- this.ListChangesOption.SKIP_DIFFSTAT,
- ];
- if (config.receive && config.receive.enable_signed_push) {
- options.push(this.ListChangesOption.PUSH_CERTIFICATES);
- }
- return this.listChangesOptionsToHex(...options);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
- */
- getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
- let optionsHex = '';
- if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
- optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
- } else {
- optionsHex = this.listChangesOptionsToHex(
- this.ListChangesOption.ALL_COMMITS,
- this.ListChangesOption.ALL_REVISIONS,
- this.ListChangesOption.SKIP_DIFFSTAT
- );
- }
- return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
- opt_cancelCondition);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string|undefined} optionsHex list changes options in hex
- * @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
- */
- _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
- return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
- const urlWithParams = this._restApiHelper
- .urlWithParams(url, optionsHex);
- const params = {O: optionsHex};
- const req = {
- url,
- errFn: opt_errFn,
- cancelCondition: opt_cancelCondition,
- params,
- fetchOptions: this._etags.getOptions(urlWithParams),
- anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
- };
- return this._restApiHelper.fetchRawJSON(req).then(response => {
- if (response && response.status === 304) {
- return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
- this._etags.getCachedPayload(urlWithParams)));
- }
-
- if (response && !response.ok) {
- if (opt_errFn) {
- opt_errFn.call(null, response);
- } else {
- this.fire('server-error', {request: req, response});
- }
- return;
- }
-
- const payloadPromise = response ?
- this._restApiHelper.readResponsePayload(response) :
- Promise.resolve(null);
-
- return payloadPromise.then(payload => {
- if (!payload) { return null; }
- this._etags.collect(urlWithParams, response, payload.raw);
- this._maybeInsertInLookup(payload.parsed);
-
- return payload.parsed;
- });
- });
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- */
- getChangeCommitInfo(changeNum, patchNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/commit?links',
- patchNum,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {Gerrit.PatchRange} patchRange
- * @param {number=} opt_parentIndex
- */
- getChangeFiles(changeNum, patchRange, opt_parentIndex) {
- let params = undefined;
- if (this.isMergeParent(patchRange.basePatchNum)) {
- params = {parent: this.getParentIndex(patchRange.basePatchNum)};
- } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) {
- params = {base: patchRange.basePatchNum};
- }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/files',
- patchNum: patchRange.patchNum,
- params,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {Gerrit.PatchRange} patchRange
- */
- getChangeEditFiles(changeNum, patchRange) {
- let endpoint = '/edit?list';
- let anonymizedEndpoint = endpoint;
- if (patchRange.basePatchNum !== 'PARENT') {
- endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + '');
- anonymizedEndpoint += '&base=*';
- }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint,
- anonymizedEndpoint,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- * @param {string} query
- * @return {!Promise<!Object>}
- */
- queryChangeFiles(changeNum, patchNum, query) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/files?q=${encodeURIComponent(query)}`,
- patchNum,
- anonymizedEndpoint: '/files?q=*',
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {Gerrit.PatchRange} patchRange
- * @return {!Promise<!Array<!Object>>}
- */
- getChangeOrEditFiles(changeNum, patchRange) {
- if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) {
- return this.getChangeEditFiles(changeNum, patchRange).then(res =>
- res.files);
- }
- return this.getChangeFiles(changeNum, patchRange);
- }
-
- getChangeRevisionActions(changeNum, patchNum) {
- const req = {
- changeNum,
- endpoint: '/actions',
- patchNum,
- reportEndpointAsIs: true,
- };
- return this._getChangeURLAndFetch(req).then(revisionActions => {
- // The rebase button on change screen is always enabled.
- if (revisionActions.rebase) {
- revisionActions.rebase.rebaseOnCurrent =
- !!revisionActions.rebase.enabled;
- revisionActions.rebase.enabled = true;
- }
- return revisionActions;
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} inputVal
- * @param {function(?Response, string=)=} opt_errFn
- */
- getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
- return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal,
- opt_errFn);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} inputVal
- * @param {function(?Response, string=)=} opt_errFn
- */
- getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) {
- return this._getChangeSuggestedGroup('CC', changeNum, inputVal,
- opt_errFn);
- }
-
- _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) {
- // More suggestions may obscure content underneath in the reply dialog,
- // see issue 10793.
- const params = {'n': 6, 'reviewer-state': reviewerState};
- if (inputVal) { params.q = inputVal; }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/suggest_reviewers',
- errFn: opt_errFn,
- params,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- */
- getChangeIncludedIn(changeNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/in',
- reportEndpointAsIs: true,
- });
- }
-
- _computeFilter(filter) {
- if (filter && filter.startsWith('^')) {
- filter = '&r=' + encodeURIComponent(filter);
- } else if (filter) {
- filter = '&m=' + encodeURIComponent(filter);
- } else {
- filter = '';
- }
- return filter;
- }
-
- /**
- * @param {string} filter
- * @param {number} groupsPerPage
- * @param {number=} opt_offset
- */
- _getGroupsUrl(filter, groupsPerPage, opt_offset) {
- const offset = opt_offset || 0;
-
- return `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
- this._computeFilter(filter);
- }
-
- /**
- * @param {string} filter
- * @param {number} reposPerPage
- * @param {number=} opt_offset
- */
- _getReposUrl(filter, reposPerPage, opt_offset) {
- const defaultFilter = 'state:active OR state:read-only';
- const namePartDelimiters = /[@.\-\s\/_]/g;
- const offset = opt_offset || 0;
-
- if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) {
- // The query language specifies hyphens as operators. Split the string
- // by hyphens and 'AND' the parts together as 'inname:' queries.
- // If the filter includes a semicolon, the user is using a more complex
- // query so we trust them and don't do any magic under the hood.
- const originalFilter = filter;
- filter = '';
- originalFilter.split(namePartDelimiters).forEach(part => {
- if (part) {
- filter += (filter === '' ? 'inname:' : ' AND inname:') + part;
+ const cachedEmails = this._cache.get('/accounts/self/emails');
+ if (cachedEmails) {
+ const emails = cachedEmails.map(entry => {
+ if (entry.email === email) {
+ return {email, preferred: true};
+ } else {
+ return {email};
}
});
+ this._cache.set('/accounts/self/emails', emails);
}
- // Check if filter is now empty which could be either because the user did
- // not provide it or because the user provided only a split character.
- if (!filter) {
- filter = defaultFilter;
- }
+ });
+ }
- filter = filter.trim();
- const encodedFilter = encodeURIComponent(filter);
-
- return `/projects/?n=${reposPerPage + 1}&S=${offset}` +
- `&query=${encodedFilter}`;
- }
-
- invalidateGroupsCache() {
- this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?');
- }
-
- invalidateReposCache() {
- this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
- }
-
- invalidateAccountsCache() {
- this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/');
- }
-
- /**
- * @param {string} filter
- * @param {number} groupsPerPage
- * @param {number=} opt_offset
- * @return {!Promise<?Object>}
- */
- getGroups(filter, groupsPerPage, opt_offset) {
- const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset);
-
- return this._fetchSharedCacheURL({
- url,
- anonymizedUrl: '/groups/?*',
- });
- }
-
- /**
- * @param {string} filter
- * @param {number} reposPerPage
- * @param {number=} opt_offset
- * @return {!Promise<?Object>}
- */
- getRepos(filter, reposPerPage, opt_offset) {
- const url = this._getReposUrl(filter, reposPerPage, opt_offset);
-
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url,
- anonymizedUrl: '/projects/?*',
- });
- }
-
- setRepoHead(repo, ref) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeURIComponent(repo)}/HEAD`,
- body: {ref},
- anonymizedUrl: '/projects/*/HEAD',
- });
- }
-
- /**
- * @param {string} filter
- * @param {string} repo
- * @param {number} reposBranchesPerPage
- * @param {number=} opt_offset
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>}
- */
- getRepoBranches(filter, repo, reposBranchesPerPage, opt_offset, opt_errFn) {
- const offset = opt_offset || 0;
- const count = reposBranchesPerPage + 1;
- filter = this._computeFilter(filter);
- repo = encodeURIComponent(repo);
- const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.fetchJSON({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/branches?*',
- });
- }
-
- /**
- * @param {string} filter
- * @param {string} repo
- * @param {number} reposTagsPerPage
- * @param {number=} opt_offset
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>}
- */
- getRepoTags(filter, repo, reposTagsPerPage, opt_offset, opt_errFn) {
- const offset = opt_offset || 0;
- const encodedRepo = encodeURIComponent(repo);
- const n = reposTagsPerPage + 1;
- const encodedFilter = this._computeFilter(filter);
- const url = `/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` +
- encodedFilter;
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.fetchJSON({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/tags',
- });
- }
-
- /**
- * @param {string} filter
- * @param {number} pluginsPerPage
- * @param {number=} opt_offset
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>}
- */
- getPlugins(filter, pluginsPerPage, opt_offset, opt_errFn) {
- const offset = opt_offset || 0;
- const encodedFilter = this._computeFilter(filter);
- const n = pluginsPerPage + 1;
- const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
- return this._restApiHelper.fetchJSON({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/plugins/?all',
- });
- }
-
- getRepoAccessRights(repoName, opt_errFn) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.fetchJSON({
- url: `/projects/${encodeURIComponent(repoName)}/access`,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/access',
- });
- }
-
- setRepoAccessRights(repoName, repoInfo) {
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._restApiHelper.send({
- method: 'POST',
- url: `/projects/${encodeURIComponent(repoName)}/access`,
- body: repoInfo,
- anonymizedUrl: '/projects/*/access',
- });
- }
-
- setRepoAccessRightsForReview(projectName, projectInfo) {
- return this._restApiHelper.send({
- method: 'PUT',
- url: `/projects/${encodeURIComponent(projectName)}/access:review`,
- body: projectInfo,
- parseResponse: true,
- anonymizedUrl: '/projects/*/access:review',
- });
- }
-
- /**
- * @param {string} inputVal
- * @param {number} opt_n
- * @param {function(?Response, string=)=} opt_errFn
- */
- getSuggestedGroups(inputVal, opt_n, opt_errFn) {
- const params = {s: inputVal};
- if (opt_n) { params.n = opt_n; }
- return this._restApiHelper.fetchJSON({
- url: '/groups/',
- errFn: opt_errFn,
- params,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} inputVal
- * @param {number} opt_n
- * @param {function(?Response, string=)=} opt_errFn
- */
- getSuggestedProjects(inputVal, opt_n, opt_errFn) {
- const params = {
- m: inputVal,
- n: MAX_PROJECT_RESULTS,
- type: 'ALL',
- };
- if (opt_n) { params.n = opt_n; }
- return this._restApiHelper.fetchJSON({
- url: '/projects/',
- errFn: opt_errFn,
- params,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {string} inputVal
- * @param {number} opt_n
- * @param {function(?Response, string=)=} opt_errFn
- */
- getSuggestedAccounts(inputVal, opt_n, opt_errFn) {
- if (!inputVal) {
- return Promise.resolve([]);
- }
- const params = {suggest: null, q: inputVal};
- if (opt_n) { params.n = opt_n; }
- return this._restApiHelper.fetchJSON({
- url: '/accounts/',
- errFn: opt_errFn,
- params,
- anonymizedUrl: '/accounts/?n=*',
- });
- }
-
- addChangeReviewer(changeNum, reviewerID) {
- return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
- }
-
- removeChangeReviewer(changeNum, reviewerID) {
- return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID);
- }
-
- _sendChangeReviewerRequest(method, changeNum, reviewerID) {
- return this.getChangeActionURL(changeNum, null, '/reviewers')
- .then(url => {
- let body;
- switch (method) {
- case 'POST':
- body = {reviewer: reviewerID};
- break;
- case 'DELETE':
- url += '/' + encodeURIComponent(reviewerID);
- break;
- default:
- throw Error('Unsupported HTTP method: ' + method);
- }
-
- return this._restApiHelper.send({method, url, body});
- });
- }
-
- getRelatedChanges(changeNum, patchNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/related',
- patchNum,
- reportEndpointAsIs: true,
- });
- }
-
- getChangesSubmittedTogether(changeNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES',
- reportEndpointAsIs: true,
- });
- }
-
- getChangeConflicts(changeNum) {
- const options = this.listChangesOptionsToHex(
- this.ListChangesOption.CURRENT_REVISION,
- this.ListChangesOption.CURRENT_COMMIT
- );
- const params = {
- O: options,
- q: 'status:open conflicts:' + changeNum,
- };
- return this._restApiHelper.fetchJSON({
- url: '/changes/',
- params,
- anonymizedUrl: '/changes/conflicts:*',
- });
- }
-
- getChangeCherryPicks(project, changeID, changeNum) {
- const options = this.listChangesOptionsToHex(
- this.ListChangesOption.CURRENT_REVISION,
- this.ListChangesOption.CURRENT_COMMIT
- );
- const query = [
- 'project:' + project,
- 'change:' + changeID,
- '-change:' + changeNum,
- '-is:abandoned',
- ].join(' ');
- const params = {
- O: options,
- q: query,
- };
- return this._restApiHelper.fetchJSON({
- url: '/changes/',
- params,
- anonymizedUrl: '/changes/change:*',
- });
- }
-
- getChangesWithSameTopic(topic, changeNum) {
- const options = this.listChangesOptionsToHex(
- this.ListChangesOption.LABELS,
- this.ListChangesOption.CURRENT_REVISION,
- this.ListChangesOption.CURRENT_COMMIT,
- this.ListChangesOption.DETAILED_LABELS
- );
- const query = [
- 'status:open',
- '-change:' + changeNum,
- `topic:"${topic}"`,
- ].join(' ');
- const params = {
- O: options,
- q: query,
- };
- return this._restApiHelper.fetchJSON({
- url: '/changes/',
- params,
- anonymizedUrl: '/changes/topic:*',
- });
- }
-
- getReviewedFiles(changeNum, patchNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/files?reviewed',
- patchNum,
- reportEndpointAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- * @param {string} path
- * @param {boolean} reviewed
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method: reviewed ? 'PUT' : 'DELETE',
- patchNum,
- endpoint: `/files/${encodeURIComponent(path)}/reviewed`,
- errFn: opt_errFn,
- anonymizedEndpoint: '/files/*/reviewed',
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} patchNum
- * @param {!Object} review
- * @param {function(?Response, string=)=} opt_errFn
- */
- saveChangeReview(changeNum, patchNum, review, opt_errFn) {
- const promises = [
- this.awaitPendingDiffDrafts(),
- this.getChangeActionURL(changeNum, patchNum, '/review'),
- ];
- return Promise.all(promises).then(([, url]) => this._restApiHelper.send({
- method: 'POST',
- url,
- body: review,
- errFn: opt_errFn,
- }));
- }
-
- getChangeEdit(changeNum, opt_download_commands) {
- const params = opt_download_commands ? {'download-commands': true} : null;
- return this.getLoggedIn().then(loggedIn => {
- if (!loggedIn) { return false; }
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/edit/',
- params,
- reportEndpointAsIs: true,
- }, true);
- });
- }
-
- /**
- * @param {string} project
- * @param {string} branch
- * @param {string} subject
- * @param {string=} opt_topic
- * @param {boolean=} opt_isPrivate
- * @param {boolean=} opt_workInProgress
- * @param {string=} opt_baseChange
- * @param {string=} opt_baseCommit
- */
- createChange(project, branch, subject, opt_topic, opt_isPrivate,
- opt_workInProgress, opt_baseChange, opt_baseCommit) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/changes/',
- body: {
- project,
- branch,
- subject,
- topic: opt_topic,
- is_private: opt_isPrivate,
- work_in_progress: opt_workInProgress,
- base_change: opt_baseChange,
- base_commit: opt_baseCommit,
- },
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} path
- * @param {number|string} patchNum
- */
- getFileContent(changeNum, path, patchNum) {
- // 404s indicate the file does not exist yet in the revision, so suppress
- // them.
- const suppress404s = res => {
- if (res && res.status !== 404) { this.fire('server-error', {res}); }
- return res;
- };
- const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ?
- this._getFileInChangeEdit(changeNum, path) :
- this._getFileInRevision(changeNum, path, patchNum, suppress404s);
-
- return promise.then(res => {
- if (!res.ok) { return res; }
-
- // The file type (used for syntax highlighting) is identified in the
- // X-FYI-Content-Type header of the response.
- const type = res.headers.get('X-FYI-Content-Type');
- return this.getResponseObject(res).then(content => {
- return {content, type, ok: true};
- });
- });
- }
-
- /**
- * Gets a file in a specific change and revision.
- *
- * @param {number|string} changeNum
- * @param {string} path
- * @param {number|string} patchNum
- * @param {?function(?Response, string=)=} opt_errFn
- */
- _getFileInRevision(changeNum, path, patchNum, opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'GET',
- patchNum,
- endpoint: `/files/${encodeURIComponent(path)}/content`,
- errFn: opt_errFn,
- headers: {Accept: 'application/json'},
- anonymizedEndpoint: '/files/*/content',
- });
- }
-
- /**
- * Gets a file in a change edit.
- *
- * @param {number|string} changeNum
- * @param {string} path
- */
- _getFileInChangeEdit(changeNum, path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'GET',
- endpoint: '/edit/' + encodeURIComponent(path),
- headers: {Accept: 'application/json'},
- anonymizedEndpoint: '/edit/*',
- });
- }
-
- rebaseChangeEdit(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit:rebase',
- reportEndpointAsIs: true,
- });
- }
-
- deleteChangeEdit(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/edit',
- reportEndpointAsIs: true,
- });
- }
-
- restoreFileInChangeEdit(changeNum, restore_path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit',
- body: {restore_path},
- reportEndpointAsIs: true,
- });
- }
-
- renameFileInChangeEdit(changeNum, old_path, new_path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit',
- body: {old_path, new_path},
- reportEndpointAsIs: true,
- });
- }
-
- deleteFileInChangeEdit(changeNum, path) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/edit/' + encodeURIComponent(path),
- anonymizedEndpoint: '/edit/*',
- });
- }
-
- saveChangeEdit(changeNum, path, contents) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/edit/' + encodeURIComponent(path),
- body: contents,
- contentType: 'text/plain',
- anonymizedEndpoint: '/edit/*',
- });
- }
-
- getRobotCommentFixPreview(changeNum, patchNum, fixId) {
- return this._getChangeURLAndFetch({
- changeNum,
- patchNum,
- endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`,
- reportEndpointAsId: true,
- });
- }
-
- applyFixSuggestion(changeNum, patchNum, fixId) {
- return this._getChangeURLAndSend({
- method: 'POST',
- changeNum,
- patchNum,
- endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`,
- reportEndpointAsId: true,
- });
- }
-
- // Deprecated, prefer to use putChangeCommitMessage instead.
- saveChangeCommitMessageEdit(changeNum, message) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/edit:message',
- body: {message},
- reportEndpointAsIs: true,
- });
- }
-
- publishChangeEdit(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/edit:publish',
- reportEndpointAsIs: true,
- });
- }
-
- putChangeCommitMessage(changeNum, message) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/message',
- body: {message},
- reportEndpointAsIs: true,
- });
- }
-
- deleteChangeCommitMessage(changeNum, messageId) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/messages/' + messageId,
- reportEndpointAsIs: true,
- });
- }
-
- saveChangeStarred(changeNum, starred) {
- // Some servers may require the project name to be provided
- // alongside the change number, so resolve the project name
- // first.
- return this.getFromProjectLookup(changeNum).then(project => {
- const url = '/accounts/self/starred.changes/' +
- (project ? encodeURIComponent(project) + '~' : '') + changeNum;
- return this._restApiHelper.send({
- method: starred ? 'PUT' : 'DELETE',
- url,
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
- });
- }
-
- saveChangeReviewed(changeNum, reviewed) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: reviewed ? '/reviewed' : '/unreviewed',
- });
- }
-
- /**
- * Public version of the _restApiHelper.send method preserved for plugins.
- *
- * @param {string} method
- * @param {string} url
- * @param {?string|number|Object=} opt_body passed as null sometimes
- * and also apparently a number. TODO (beckysiegel) remove need for
- * number at least.
- * @param {?function(?Response, string=)=} opt_errFn
- * passed as null sometimes.
- * @param {?string=} opt_contentType
- * @param {Object=} opt_headers
- */
- send(method, url, opt_body, opt_errFn, opt_contentType,
- opt_headers) {
- return this._restApiHelper.send({
- method,
- url,
- body: opt_body,
- errFn: opt_errFn,
- contentType: opt_contentType,
- headers: opt_headers,
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string} basePatchNum Negative values specify merge parent
- * index.
- * @param {number|string} patchNum
- * @param {string} path
- * @param {string=} opt_whitespace the ignore-whitespace level for the diff
- * algorithm.
- * @param {function(?Response, string=)=} opt_errFn
- */
- getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace,
- opt_errFn) {
- const params = {
- context: 'ALL',
- intraline: null,
- whitespace: opt_whitespace || 'IGNORE_NONE',
- };
- if (this.isMergeParent(basePatchNum)) {
- params.parent = this.getParentIndex(basePatchNum);
- } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) {
- params.base = basePatchNum;
- }
- const endpoint = `/files/${encodeURIComponent(path)}/diff`;
- const req = {
- changeNum,
- endpoint,
- patchNum,
- errFn: opt_errFn,
- params,
- anonymizedEndpoint: '/files/*/diff',
- };
-
- // Invalidate the cache if its edit patch to make sure we always get latest.
- if (patchNum === this.EDIT_NAME) {
- if (!req.fetchOptions) req.fetchOptions = {};
- if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
- req.fetchOptions.headers.append('Cache-Control', 'no-cache');
- }
-
- return this._getChangeURLAndFetch(req);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
- return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
- opt_patchNum, opt_path);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
- return this._getDiffComments(changeNum, '/robotcomments',
- opt_basePatchNum, opt_patchNum, opt_path);
- }
-
- /**
- * If the user is logged in, fetch the user's draft diff comments. If there
- * is no logged in user, the request is not made and the promise yields an
- * empty object.
- *
- * @param {number|string} changeNum
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
- return this.getLoggedIn().then(loggedIn => {
- if (!loggedIn) { return Promise.resolve({}); }
- return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum,
- opt_patchNum, opt_path);
- });
- }
-
- _setRange(comments, comment) {
- if (comment.in_reply_to && !comment.range) {
- for (let i = 0; i < comments.length; i++) {
- if (comments[i].id === comment.in_reply_to) {
- comment.range = comments[i].range;
- break;
- }
- }
- }
- return comment;
- }
-
- _setRanges(comments) {
- comments = comments || [];
- comments.sort(
- (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated)
- );
- for (const comment of comments) {
- this._setRange(comments, comment);
- }
- return comments;
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} endpoint
- * @param {number|string=} opt_basePatchNum
- * @param {number|string=} opt_patchNum
- * @param {string=} opt_path
- * @return {!Promise<!Object>}
- */
- _getDiffComments(changeNum, endpoint, opt_basePatchNum,
- opt_patchNum, opt_path) {
- /**
- * Fetches the comments for a given patchNum.
- * Helper function to make promises more legible.
- *
- * @param {string|number=} opt_patchNum
- * @return {!Promise<!Object>} Diff comments response.
- */
- // We don't want to add accept header, since preloading of comments is
- // working only without accept header.
- const noAcceptHeader = true;
- const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
- changeNum,
- endpoint,
- patchNum: opt_patchNum,
- reportEndpointAsIs: true,
- }, noAcceptHeader);
-
- if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
- return fetchComments();
- }
- function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
- function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
- function setPath(c) { c.path = opt_path; }
-
- const promises = [];
- let comments;
- let baseComments;
- let fetchPromise;
- fetchPromise = fetchComments(opt_patchNum).then(response => {
- comments = response[opt_path] || [];
- // TODO(kaspern): Implement this on in the backend so this can
- // be removed.
- // Sort comments by date so that parent ranges can be propagated
- // in a single pass.
- comments = this._setRanges(comments);
-
- if (opt_basePatchNum == PARENT_PATCH_NUM) {
- baseComments = comments.filter(onlyParent);
- baseComments.forEach(setPath);
- }
- comments = comments.filter(withoutParent);
-
- comments.forEach(setPath);
- });
- promises.push(fetchPromise);
-
- if (opt_basePatchNum != PARENT_PATCH_NUM) {
- fetchPromise = fetchComments(opt_basePatchNum).then(response => {
- baseComments = (response[opt_path] || [])
- .filter(withoutParent);
- baseComments = this._setRanges(baseComments);
- baseComments.forEach(setPath);
- });
- promises.push(fetchPromise);
- }
-
- return Promise.all(promises).then(() => Promise.resolve({
- baseComments,
- comments,
- }));
- }
-
- /**
- * @param {number|string} changeNum
- * @param {string} endpoint
- * @param {number|string=} opt_patchNum
- */
- _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) {
- return this._changeBaseURL(changeNum, opt_patchNum)
- .then(url => url + endpoint);
- }
-
- saveDiffDraft(changeNum, patchNum, draft) {
- return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft);
- }
-
- deleteDiffDraft(changeNum, patchNum, draft) {
- return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft);
- }
-
- /**
- * @returns {boolean} Whether there are pending diff draft sends.
- */
- hasPendingDiffDrafts() {
- const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT];
- return promises && promises.length;
- }
-
- /**
- * @returns {!Promise<undefined>} A promise that resolves when all pending
- * diff draft sends have resolved.
- */
- awaitPendingDiffDrafts() {
- return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || [])
- .then(() => {
- this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
- });
- }
-
- _sendDiffDraftRequest(method, changeNum, patchNum, draft) {
- const isCreate = !draft.id && method === 'PUT';
- let endpoint = '/drafts';
- let anonymizedEndpoint = endpoint;
- if (draft.id) {
- endpoint += '/' + draft.id;
- anonymizedEndpoint += '/*';
- }
- let body;
- if (method === 'PUT') {
- body = draft;
- }
-
- if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) {
- this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
- }
-
- const req = {
- changeNum,
- method,
- patchNum,
- endpoint,
- body,
- anonymizedEndpoint,
- };
-
- const promise = this._getChangeURLAndSend(req);
- this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
-
- if (isCreate) {
- return this._failForCreate200(promise);
- }
-
- return promise;
- }
-
- getCommitInfo(project, commit) {
- return this._restApiHelper.fetchJSON({
- url: '/projects/' + encodeURIComponent(project) +
- '/commits/' + encodeURIComponent(commit),
- anonymizedUrl: '/projects/*/comments/*',
- });
- }
-
- _fetchB64File(url) {
- return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
- .then(response => {
- if (!response.ok) {
- return Promise.reject(new Error(response.statusText));
- }
- const type = response.headers.get('X-FYI-Content-Type');
- return response.text()
- .then(text => {
- return {body: text, type};
- });
- });
- }
-
- /**
- * @param {string} changeId
- * @param {string|number} patchNum
- * @param {string} path
- * @param {number=} opt_parentIndex
- */
- getB64FileContents(changeId, patchNum, path, opt_parentIndex) {
- const parent = typeof opt_parentIndex === 'number' ?
- '?parent=' + opt_parentIndex : '';
- return this._changeBaseURL(changeId, patchNum).then(url => {
- url = `${url}/files/${encodeURIComponent(path)}/content${parent}`;
- return this._fetchB64File(url);
- });
- }
-
- getImagesForDiff(changeNum, diff, patchRange) {
- let promiseA;
- let promiseB;
-
- if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) {
- if (patchRange.basePatchNum === 'PARENT') {
- // Note: we only attempt to get the image from the first parent.
- promiseA = this.getB64FileContents(changeNum, patchRange.patchNum,
- diff.meta_a.name, 1);
- } else {
- promiseA = this.getB64FileContents(changeNum,
- patchRange.basePatchNum, diff.meta_a.name);
- }
- } else {
- promiseA = Promise.resolve(null);
- }
-
- if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) {
- promiseB = this.getB64FileContents(changeNum, patchRange.patchNum,
- diff.meta_b.name);
- } else {
- promiseB = Promise.resolve(null);
- }
-
- return Promise.all([promiseA, promiseB]).then(results => {
- const baseImage = results[0];
- const revisionImage = results[1];
-
- // Sometimes the server doesn't send back the content type.
- if (baseImage) {
- baseImage._expectedType = diff.meta_a.content_type;
- baseImage._name = diff.meta_a.name;
- }
- if (revisionImage) {
- revisionImage._expectedType = diff.meta_b.content_type;
- revisionImage._name = diff.meta_b.name;
- }
-
- return {baseImage, revisionImage};
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {?number|string=} opt_patchNum passed as null sometimes.
- * @param {string=} opt_project
- * @return {!Promise<string>}
- */
- _changeBaseURL(changeNum, opt_patchNum, opt_project) {
- // TODO(kaspern): For full slicer migration, app should warn with a call
- // stack every time _changeBaseURL is called without a project.
- const projectPromise = opt_project ?
- Promise.resolve(opt_project) :
- this.getFromProjectLookup(changeNum);
- return projectPromise.then(project => {
- let url = `/changes/${encodeURIComponent(project)}~${changeNum}`;
- if (opt_patchNum) {
- url += `/revisions/${opt_patchNum}`;
- }
- return url;
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- setChangeTopic(changeNum, topic) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/topic',
- body: {topic},
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- setChangeHashtag(changeNum, hashtag) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/hashtags',
- body: hashtag,
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- deleteAccountHttpPassword() {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/password.http',
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- generateAccountHttpPassword() {
- return this._restApiHelper.send({
- method: 'PUT',
- url: '/accounts/self/password.http',
- body: {generate: true},
- parseResponse: true,
- reportUrlAsIs: true,
- });
- }
-
- getAccountSSHKeys() {
- return this._fetchSharedCacheURL({
- url: '/accounts/self/sshkeys',
- reportUrlAsIs: true,
- });
- }
-
- addAccountSSHKey(key) {
- const req = {
- method: 'POST',
- url: '/accounts/self/sshkeys',
- body: key,
- contentType: 'text/plain',
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(response => {
- if (response.status < 200 && response.status >= 300) {
- return Promise.reject(new Error('error'));
- }
- return this.getResponseObject(response);
- })
- .then(obj => {
- if (!obj.valid) { return Promise.reject(new Error('error')); }
- return obj;
- });
- }
-
- deleteAccountSSHKey(id) {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/sshkeys/' + id,
- anonymizedUrl: '/accounts/self/sshkeys/*',
- });
- }
-
- getAccountGPGKeys() {
- return this._restApiHelper.fetchJSON({
- url: '/accounts/self/gpgkeys',
- reportUrlAsIs: true,
- });
- }
-
- addAccountGPGKey(key) {
- const req = {
- method: 'POST',
- url: '/accounts/self/gpgkeys',
- body: key,
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req)
- .then(response => {
- if (response.status < 200 && response.status >= 300) {
- return Promise.reject(new Error('error'));
- }
- return this.getResponseObject(response);
- })
- .then(obj => {
- if (!obj) { return Promise.reject(new Error('error')); }
- return obj;
- });
- }
-
- deleteAccountGPGKey(id) {
- return this._restApiHelper.send({
- method: 'DELETE',
- url: '/accounts/self/gpgkeys/' + id,
- anonymizedUrl: '/accounts/self/gpgkeys/*',
- });
- }
-
- deleteVote(changeNum, account, label) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
- anonymizedEndpoint: '/reviewers/*/votes/*',
- });
- }
-
- setDescription(changeNum, patchNum, desc) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT', patchNum,
- endpoint: '/description',
- body: {description: desc},
- reportUrlAsIs: true,
- });
- }
-
- confirmEmail(token) {
- const req = {
- method: 'PUT',
- url: '/config/server/email.confirm',
- body: {token},
- reportUrlAsIs: true,
- };
- return this._restApiHelper.send(req).then(response => {
- if (response.status === 204) {
- return 'Email confirmed successfully.';
- }
- return null;
- });
- }
-
- getCapabilities(opt_errFn) {
- return this._restApiHelper.fetchJSON({
- url: '/config/server/capabilities',
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- getTopMenus(opt_errFn) {
- return this._fetchSharedCacheURL({
- url: '/config/server/top-menus',
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- setAssignee(changeNum, assignee) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'PUT',
- endpoint: '/assignee',
- body: {assignee},
- reportUrlAsIs: true,
- });
- }
-
- deleteAssignee(changeNum) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'DELETE',
- endpoint: '/assignee',
- reportUrlAsIs: true,
- });
- }
-
- probePath(path) {
- return fetch(new Request(path, {method: 'HEAD'}))
- .then(response => response.ok);
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_message
- */
- startWorkInProgress(changeNum, opt_message) {
- const body = {};
- if (opt_message) {
- body.message = opt_message;
- }
- const req = {
- changeNum,
- method: 'POST',
- endpoint: '/wip',
- body,
- reportUrlAsIs: true,
- };
- return this._getChangeURLAndSend(req).then(response => {
- if (response.status === 204) {
- return 'Change marked as Work In Progress.';
- }
- });
- }
-
- /**
- * @param {number|string} changeNum
- * @param {number|string=} opt_body
- * @param {function(?Response, string=)=} opt_errFn
- */
- startReview(changeNum, opt_body, opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- endpoint: '/ready',
- body: opt_body,
- errFn: opt_errFn,
- reportUrlAsIs: true,
- });
- }
-
- /**
- * @suppress {checkTypes}
- * Resulted in error: Promise.prototype.then does not match formal
- * parameter.
- */
- deleteComment(changeNum, patchNum, commentID, reason) {
- return this._getChangeURLAndSend({
- changeNum,
- method: 'POST',
- patchNum,
- endpoint: `/comments/${commentID}/delete`,
- body: {reason},
- parseResponse: true,
- anonymizedEndpoint: '/comments/*/delete',
- });
- }
-
- /**
- * Given a changeNum, gets the change.
- *
- * @param {number|string} changeNum
- * @param {function(?Response, string=)=} opt_errFn
- * @return {!Promise<?Object>} The change
- */
- getChange(changeNum, opt_errFn) {
- // Cannot use _changeBaseURL, as this function is used by _projectLookup.
- return this._restApiHelper.fetchJSON({
- url: `/changes/?q=change:${changeNum}`,
- errFn: opt_errFn,
- anonymizedUrl: '/changes/?q=change:*',
- }).then(res => {
- if (!res || !res.length) { return null; }
- return res[0];
- });
- }
-
- /**
- * @param {string|number} changeNum
- * @param {string=} project
- */
- setInProjectLookup(changeNum, project) {
- if (this._projectLookup[changeNum] &&
- this._projectLookup[changeNum] !== project) {
- console.warn('Change set with multiple project nums.' +
- 'One of them must be invalid.');
- }
- this._projectLookup[changeNum] = project;
- }
-
- /**
- * Checks in _projectLookup for the changeNum. If it exists, returns the
- * project. If not, calls the restAPI to get the change, populates
- * _projectLookup with the project for that change, and returns the project.
- *
- * @param {string|number} changeNum
- * @return {!Promise<string|undefined>}
- */
- getFromProjectLookup(changeNum) {
- const project = this._projectLookup[changeNum];
- if (project) { return Promise.resolve(project); }
-
- const onError = response => {
- // Fire a page error so that the visual 404 is displayed.
- this.fire('page-error', {response});
- };
-
- return this.getChange(changeNum, onError).then(change => {
- if (!change || !change.project) { return; }
- this.setInProjectLookup(changeNum, change.project);
- return change.project;
- });
- }
-
- /**
- * Alias for _changeBaseURL.then(send).
- *
- * @todo(beckysiegel) clean up comments
- * @param {Gerrit.ChangeSendRequest} req
- * @return {!Promise<!Object>}
- */
- _getChangeURLAndSend(req) {
- const anonymizedBaseUrl = req.patchNum ?
- ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
- const anonymizedEndpoint = req.reportEndpointAsIs ?
- req.endpoint : req.anonymizedEndpoint;
-
- return this._changeBaseURL(req.changeNum, req.patchNum)
- .then(url => this._restApiHelper.send({
- method: req.method,
- url: url + req.endpoint,
- body: req.body,
- errFn: req.errFn,
- contentType: req.contentType,
- headers: req.headers,
- parseResponse: req.parseResponse,
- anonymizedUrl: anonymizedEndpoint ?
- (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
- }));
- }
-
- /**
- * Alias for _changeBaseURL.then(_fetchJSON).
- *
- * @param {Gerrit.ChangeFetchRequest} req
- * @return {!Promise<!Object>}
- */
- _getChangeURLAndFetch(req, noAcceptHeader) {
- const anonymizedEndpoint = req.reportEndpointAsIs ?
- req.endpoint : req.anonymizedEndpoint;
- const anonymizedBaseUrl = req.patchNum ?
- ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
- return this._changeBaseURL(req.changeNum, req.patchNum)
- .then(url => this._restApiHelper.fetchJSON({
- url: url + req.endpoint,
- errFn: req.errFn,
- params: req.params,
- fetchOptions: req.fetchOptions,
- anonymizedUrl: anonymizedEndpoint ?
- (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
- }, noAcceptHeader));
- }
-
- /**
- * Execute a change action or revision action on a change.
- *
- * @param {number} changeNum
- * @param {string} method
- * @param {string} endpoint
- * @param {string|number|undefined} opt_patchNum
- * @param {Object=} opt_payload
- * @param {?function(?Response, string=)=} opt_errFn
- * @return {Promise}
- */
- executeChangeAction(changeNum, method, endpoint, opt_patchNum, opt_payload,
- opt_errFn) {
- return this._getChangeURLAndSend({
- changeNum,
- method,
- patchNum: opt_patchNum,
- endpoint,
- body: opt_payload,
- errFn: opt_errFn,
- });
- }
-
- /**
- * Get blame information for the given diff.
- *
- * @param {string|number} changeNum
- * @param {string|number} patchNum
- * @param {string} path
- * @param {boolean=} opt_base If true, requests blame for the base of the
- * diff, rather than the revision.
- * @return {!Promise<!Object>}
- */
- getBlame(changeNum, patchNum, path, opt_base) {
- const encodedPath = encodeURIComponent(path);
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: `/files/${encodedPath}/blame`,
- patchNum,
- params: opt_base ? {base: 't'} : undefined,
- anonymizedEndpoint: '/files/*/blame',
- });
- }
-
- /**
- * Modify the given create draft request promise so that it fails and throws
- * an error if the response bears HTTP status 200 instead of HTTP 201.
- *
- * @see Issue 7763
- * @param {Promise} promise The original promise.
- * @return {Promise} The modified promise.
- */
- _failForCreate200(promise) {
- return promise.then(result => {
- if (result.status === 200) {
- // Read the response headers into an object representation.
- const headers = Array.from(result.headers.entries())
- .reduce((obj, [key, val]) => {
- if (!HEADER_REPORTING_BLACKLIST.test(key)) {
- obj[key] = val;
- }
- return obj;
- }, {});
- const err = new Error([
- CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE,
- JSON.stringify(headers),
- ].join('\n'));
- // Throw the error so that it is caught by gr-reporting.
- throw err;
- }
- return result;
- });
- }
-
- /**
- * Fetch a project dashboard definition.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard
- *
- * @param {string} project
- * @param {string} dashboard
- * @param {function(?Response, string=)=} opt_errFn
- * passed as null sometimes.
- * @return {!Promise<!Object>}
- */
- getDashboard(project, dashboard, opt_errFn) {
- const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' +
- encodeURIComponent(dashboard);
- return this._fetchSharedCacheURL({
- url,
- errFn: opt_errFn,
- anonymizedUrl: '/projects/*/dashboards/*',
- });
- }
-
- /**
- * @param {string} filter
- * @return {!Promise<?Object>}
- */
- getDocumentationSearches(filter) {
- filter = filter.trim();
- const encodedFilter = encodeURIComponent(filter);
-
- // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
- // supports it.
- return this._fetchSharedCacheURL({
- url: `/Documentation/?q=${encodedFilter}`,
- anonymizedUrl: '/Documentation/?*',
- });
- }
-
- getMergeable(changeNum) {
- return this._getChangeURLAndFetch({
- changeNum,
- endpoint: '/revisions/current/mergeable',
- parseResponse: true,
- reportEndpointAsIs: true,
- });
- }
-
- deleteDraftComments(query) {
- return this._restApiHelper.send({
- method: 'POST',
- url: '/accounts/self/drafts:delete',
- body: {query},
- });
+ /**
+ * @param {?Object} obj
+ */
+ _updateCachedAccount(obj) {
+ // If result of getAccount is in cache, update it in the cache
+ // so we don't have to invalidate it.
+ const cachedAccount = this._cache.get('/accounts/self/detail');
+ if (cachedAccount) {
+ // Replace object in cache with new object to force UI updates.
+ this._cache.set('/accounts/self/detail',
+ Object.assign({}, cachedAccount, obj));
}
}
- customElements.define(GrRestApiInterface.is, GrRestApiInterface);
-})();
+ /**
+ * @param {string} name
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountName(name, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/name',
+ body: {name},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newName => this._updateCachedAccount({name: newName}));
+ }
+
+ /**
+ * @param {string} username
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountUsername(username, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/username',
+ body: {username},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newName => this._updateCachedAccount({username: newName}));
+ }
+
+ /**
+ * @param {string} status
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ setAccountStatus(status, opt_errFn) {
+ const req = {
+ method: 'PUT',
+ url: '/accounts/self/status',
+ body: {status},
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(newStatus => this._updateCachedAccount({status: newStatus}));
+ }
+
+ getAccountStatus(userId) {
+ return this._restApiHelper.fetchJSON({
+ url: `/accounts/${encodeURIComponent(userId)}/status`,
+ anonymizedUrl: '/accounts/*/status',
+ });
+ }
+
+ getAccountGroups() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/groups',
+ reportUrlAsIs: true,
+ });
+ }
+
+ getAccountAgreements() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/agreements',
+ reportUrlAsIs: true,
+ });
+ }
+
+ saveAccountAgreement(name) {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/agreements',
+ body: name,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string=} opt_params
+ */
+ getAccountCapabilities(opt_params) {
+ let queryString = '';
+ if (opt_params) {
+ queryString = '?q=' + opt_params
+ .map(param => encodeURIComponent(param))
+ .join('&q=');
+ }
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/capabilities' + queryString,
+ anonymizedUrl: '/accounts/self/capabilities?q=*',
+ });
+ }
+
+ getLoggedIn() {
+ return this._auth.authCheck();
+ }
+
+ getIsAdmin() {
+ return this.getLoggedIn()
+ .then(isLoggedIn => {
+ if (isLoggedIn) {
+ return this.getAccountCapabilities();
+ } else {
+ return Promise.resolve();
+ }
+ })
+ .then(
+ capabilities => capabilities && capabilities.administrateServer
+ );
+ }
+
+ getDefaultPreferences() {
+ return this._fetchSharedCacheURL({
+ url: '/config/server/preferences',
+ reportUrlAsIs: true,
+ });
+ }
+
+ getPreferences() {
+ return this.getLoggedIn().then(loggedIn => {
+ if (loggedIn) {
+ const req = {url: '/accounts/self/preferences', reportUrlAsIs: true};
+ return this._fetchSharedCacheURL(req).then(res => {
+ if (this._isNarrowScreen()) {
+ // Note that this can be problematic, because the diff will stay
+ // unified even after increasing the window width.
+ res.default_diff_view = DiffViewMode.UNIFIED;
+ } else {
+ res.default_diff_view = res.diff_view;
+ }
+ return Promise.resolve(res);
+ });
+ }
+
+ return Promise.resolve({
+ changes_per_page: 25,
+ default_diff_view: this._isNarrowScreen() ?
+ DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE,
+ diff_view: 'SIDE_BY_SIDE',
+ size_bar_in_change_table: true,
+ });
+ });
+ }
+
+ getWatchedProjects() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/watched.projects',
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} projects
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveWatchedProjects(projects, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/watched.projects',
+ body: projects,
+ errFn: opt_errFn,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} projects
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ deleteWatchedProjects(projects, opt_errFn) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/watched.projects:delete',
+ body: projects,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ _isNarrowScreen() {
+ return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
+ }
+
+ /**
+ * @param {number=} opt_changesPerPage
+ * @param {string|!Array<string>=} opt_query A query or an array of queries.
+ * @param {number|string=} opt_offset
+ * @param {!Object=} opt_options
+ * @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an
+ * array, _fetchJSON will return an array of arrays of changeInfos. If it
+ * is unspecified or a string, _fetchJSON will return an array of
+ * changeInfos.
+ */
+ getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) {
+ const options = opt_options || this.listChangesOptionsToHex(
+ this.ListChangesOption.LABELS,
+ this.ListChangesOption.DETAILED_ACCOUNTS
+ );
+ // Issue 4524: respect legacy token with max sortkey.
+ if (opt_offset === 'n,z') {
+ opt_offset = 0;
+ }
+ const params = {
+ O: options,
+ S: opt_offset || 0,
+ };
+ if (opt_changesPerPage) { params.n = opt_changesPerPage; }
+ if (opt_query && opt_query.length > 0) {
+ params.q = opt_query;
+ }
+ const iterateOverChanges = arr => {
+ for (const change of (arr || [])) {
+ this._maybeInsertInLookup(change);
+ }
+ };
+ const req = {
+ url: '/changes/',
+ params,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.fetchJSON(req).then(response => {
+ // Response may be an array of changes OR an array of arrays of
+ // changes.
+ if (opt_query instanceof Array) {
+ // Normalize the response to look like a multi-query response
+ // when there is only one query.
+ if (opt_query.length === 1) {
+ response = [response];
+ }
+ for (const arr of response) {
+ iterateOverChanges(arr);
+ }
+ } else {
+ iterateOverChanges(response);
+ }
+ return response;
+ });
+ }
+
+ /**
+ * Inserts a change into _projectLookup iff it has a valid structure.
+ *
+ * @param {?{ _number: (number|string) }} change
+ */
+ _maybeInsertInLookup(change) {
+ if (change && change.project && change._number) {
+ this.setInProjectLookup(change._number, change.project);
+ }
+ }
+
+ /**
+ * TODO (beckysiegel) this needs to be rewritten with the optional param
+ * at the end.
+ *
+ * @param {number|string} changeNum
+ * @param {?number|string=} opt_patchNum passed as null sometimes.
+ * @param {?=} endpoint
+ * @return {!Promise<string>}
+ */
+ getChangeActionURL(changeNum, opt_patchNum, endpoint) {
+ return this._changeBaseURL(changeNum, opt_patchNum)
+ .then(url => url + endpoint);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {function(?Response, string=)=} opt_errFn
+ * @param {function()=} opt_cancelCondition
+ */
+ getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
+ return this.getConfig(false).then(config => {
+ const optionsHex = this._getChangeOptionsHex(config);
+ return this._getChangeDetail(
+ changeNum, optionsHex, opt_errFn, opt_cancelCondition)
+ .then(GrReviewerUpdatesParser.parse);
+ });
+ }
+
+ _getChangeOptionsHex(config) {
+ if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage
+ && !(config.receive && config.receive.enable_signed_push)) {
+ return window.DEFAULT_DETAIL_HEXES.changePage;
+ }
+
+ // This list MUST be kept in sync with
+ // ChangeIT#changeDetailsDoesNotRequireIndex
+ const options = [
+ this.ListChangesOption.ALL_COMMITS,
+ this.ListChangesOption.ALL_REVISIONS,
+ this.ListChangesOption.CHANGE_ACTIONS,
+ this.ListChangesOption.DETAILED_LABELS,
+ this.ListChangesOption.DOWNLOAD_COMMANDS,
+ this.ListChangesOption.MESSAGES,
+ this.ListChangesOption.SUBMITTABLE,
+ this.ListChangesOption.WEB_LINKS,
+ this.ListChangesOption.SKIP_DIFFSTAT,
+ ];
+ if (config.receive && config.receive.enable_signed_push) {
+ options.push(this.ListChangesOption.PUSH_CERTIFICATES);
+ }
+ return this.listChangesOptionsToHex(...options);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {function(?Response, string=)=} opt_errFn
+ * @param {function()=} opt_cancelCondition
+ */
+ getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) {
+ let optionsHex = '';
+ if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
+ optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
+ } else {
+ optionsHex = this.listChangesOptionsToHex(
+ this.ListChangesOption.ALL_COMMITS,
+ this.ListChangesOption.ALL_REVISIONS,
+ this.ListChangesOption.SKIP_DIFFSTAT
+ );
+ }
+ return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
+ opt_cancelCondition);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string|undefined} optionsHex list changes options in hex
+ * @param {function(?Response, string=)=} opt_errFn
+ * @param {function()=} opt_cancelCondition
+ */
+ _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
+ return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
+ const urlWithParams = this._restApiHelper
+ .urlWithParams(url, optionsHex);
+ const params = {O: optionsHex};
+ const req = {
+ url,
+ errFn: opt_errFn,
+ cancelCondition: opt_cancelCondition,
+ params,
+ fetchOptions: this._etags.getOptions(urlWithParams),
+ anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
+ };
+ return this._restApiHelper.fetchRawJSON(req).then(response => {
+ if (response && response.status === 304) {
+ return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
+ this._etags.getCachedPayload(urlWithParams)));
+ }
+
+ if (response && !response.ok) {
+ if (opt_errFn) {
+ opt_errFn.call(null, response);
+ } else {
+ this.fire('server-error', {request: req, response});
+ }
+ return;
+ }
+
+ const payloadPromise = response ?
+ this._restApiHelper.readResponsePayload(response) :
+ Promise.resolve(null);
+
+ return payloadPromise.then(payload => {
+ if (!payload) { return null; }
+ this._etags.collect(urlWithParams, response, payload.raw);
+ this._maybeInsertInLookup(payload.parsed);
+
+ return payload.parsed;
+ });
+ });
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ */
+ getChangeCommitInfo(changeNum, patchNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/commit?links',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {Gerrit.PatchRange} patchRange
+ * @param {number=} opt_parentIndex
+ */
+ getChangeFiles(changeNum, patchRange, opt_parentIndex) {
+ let params = undefined;
+ if (this.isMergeParent(patchRange.basePatchNum)) {
+ params = {parent: this.getParentIndex(patchRange.basePatchNum)};
+ } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) {
+ params = {base: patchRange.basePatchNum};
+ }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/files',
+ patchNum: patchRange.patchNum,
+ params,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {Gerrit.PatchRange} patchRange
+ */
+ getChangeEditFiles(changeNum, patchRange) {
+ let endpoint = '/edit?list';
+ let anonymizedEndpoint = endpoint;
+ if (patchRange.basePatchNum !== 'PARENT') {
+ endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + '');
+ anonymizedEndpoint += '&base=*';
+ }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint,
+ anonymizedEndpoint,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ * @param {string} query
+ * @return {!Promise<!Object>}
+ */
+ queryChangeFiles(changeNum, patchNum, query) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: `/files?q=${encodeURIComponent(query)}`,
+ patchNum,
+ anonymizedEndpoint: '/files?q=*',
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {Gerrit.PatchRange} patchRange
+ * @return {!Promise<!Array<!Object>>}
+ */
+ getChangeOrEditFiles(changeNum, patchRange) {
+ if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) {
+ return this.getChangeEditFiles(changeNum, patchRange).then(res =>
+ res.files);
+ }
+ return this.getChangeFiles(changeNum, patchRange);
+ }
+
+ getChangeRevisionActions(changeNum, patchNum) {
+ const req = {
+ changeNum,
+ endpoint: '/actions',
+ patchNum,
+ reportEndpointAsIs: true,
+ };
+ return this._getChangeURLAndFetch(req).then(revisionActions => {
+ // The rebase button on change screen is always enabled.
+ if (revisionActions.rebase) {
+ revisionActions.rebase.rebaseOnCurrent =
+ !!revisionActions.rebase.enabled;
+ revisionActions.rebase.enabled = true;
+ }
+ return revisionActions;
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} inputVal
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) {
+ return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal,
+ opt_errFn);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} inputVal
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) {
+ return this._getChangeSuggestedGroup('CC', changeNum, inputVal,
+ opt_errFn);
+ }
+
+ _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) {
+ // More suggestions may obscure content underneath in the reply dialog,
+ // see issue 10793.
+ const params = {'n': 6, 'reviewer-state': reviewerState};
+ if (inputVal) { params.q = inputVal; }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/suggest_reviewers',
+ errFn: opt_errFn,
+ params,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ */
+ getChangeIncludedIn(changeNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/in',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ _computeFilter(filter) {
+ if (filter && filter.startsWith('^')) {
+ filter = '&r=' + encodeURIComponent(filter);
+ } else if (filter) {
+ filter = '&m=' + encodeURIComponent(filter);
+ } else {
+ filter = '';
+ }
+ return filter;
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} groupsPerPage
+ * @param {number=} opt_offset
+ */
+ _getGroupsUrl(filter, groupsPerPage, opt_offset) {
+ const offset = opt_offset || 0;
+
+ return `/groups/?n=${groupsPerPage + 1}&S=${offset}` +
+ this._computeFilter(filter);
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} reposPerPage
+ * @param {number=} opt_offset
+ */
+ _getReposUrl(filter, reposPerPage, opt_offset) {
+ const defaultFilter = 'state:active OR state:read-only';
+ const namePartDelimiters = /[@.\-\s\/_]/g;
+ const offset = opt_offset || 0;
+
+ if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) {
+ // The query language specifies hyphens as operators. Split the string
+ // by hyphens and 'AND' the parts together as 'inname:' queries.
+ // If the filter includes a semicolon, the user is using a more complex
+ // query so we trust them and don't do any magic under the hood.
+ const originalFilter = filter;
+ filter = '';
+ originalFilter.split(namePartDelimiters).forEach(part => {
+ if (part) {
+ filter += (filter === '' ? 'inname:' : ' AND inname:') + part;
+ }
+ });
+ }
+ // Check if filter is now empty which could be either because the user did
+ // not provide it or because the user provided only a split character.
+ if (!filter) {
+ filter = defaultFilter;
+ }
+
+ filter = filter.trim();
+ const encodedFilter = encodeURIComponent(filter);
+
+ return `/projects/?n=${reposPerPage + 1}&S=${offset}` +
+ `&query=${encodedFilter}`;
+ }
+
+ invalidateGroupsCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?');
+ }
+
+ invalidateReposCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
+ }
+
+ invalidateAccountsCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/');
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} groupsPerPage
+ * @param {number=} opt_offset
+ * @return {!Promise<?Object>}
+ */
+ getGroups(filter, groupsPerPage, opt_offset) {
+ const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset);
+
+ return this._fetchSharedCacheURL({
+ url,
+ anonymizedUrl: '/groups/?*',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} reposPerPage
+ * @param {number=} opt_offset
+ * @return {!Promise<?Object>}
+ */
+ getRepos(filter, reposPerPage, opt_offset) {
+ const url = this._getReposUrl(filter, reposPerPage, opt_offset);
+
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url,
+ anonymizedUrl: '/projects/?*',
+ });
+ }
+
+ setRepoHead(repo, ref) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeURIComponent(repo)}/HEAD`,
+ body: {ref},
+ anonymizedUrl: '/projects/*/HEAD',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {string} repo
+ * @param {number} reposBranchesPerPage
+ * @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>}
+ */
+ getRepoBranches(filter, repo, reposBranchesPerPage, opt_offset, opt_errFn) {
+ const offset = opt_offset || 0;
+ const count = reposBranchesPerPage + 1;
+ filter = this._computeFilter(filter);
+ repo = encodeURIComponent(repo);
+ const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/branches?*',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {string} repo
+ * @param {number} reposTagsPerPage
+ * @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>}
+ */
+ getRepoTags(filter, repo, reposTagsPerPage, opt_offset, opt_errFn) {
+ const offset = opt_offset || 0;
+ const encodedRepo = encodeURIComponent(repo);
+ const n = reposTagsPerPage + 1;
+ const encodedFilter = this._computeFilter(filter);
+ const url = `/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` +
+ encodedFilter;
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/tags',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @param {number} pluginsPerPage
+ * @param {number=} opt_offset
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>}
+ */
+ getPlugins(filter, pluginsPerPage, opt_offset, opt_errFn) {
+ const offset = opt_offset || 0;
+ const encodedFilter = this._computeFilter(filter);
+ const n = pluginsPerPage + 1;
+ const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
+ return this._restApiHelper.fetchJSON({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/plugins/?all',
+ });
+ }
+
+ getRepoAccessRights(repoName, opt_errFn) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.fetchJSON({
+ url: `/projects/${encodeURIComponent(repoName)}/access`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/access',
+ });
+ }
+
+ setRepoAccessRights(repoName, repoInfo) {
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: `/projects/${encodeURIComponent(repoName)}/access`,
+ body: repoInfo,
+ anonymizedUrl: '/projects/*/access',
+ });
+ }
+
+ setRepoAccessRightsForReview(projectName, projectInfo) {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: `/projects/${encodeURIComponent(projectName)}/access:review`,
+ body: projectInfo,
+ parseResponse: true,
+ anonymizedUrl: '/projects/*/access:review',
+ });
+ }
+
+ /**
+ * @param {string} inputVal
+ * @param {number} opt_n
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getSuggestedGroups(inputVal, opt_n, opt_errFn) {
+ const params = {s: inputVal};
+ if (opt_n) { params.n = opt_n; }
+ return this._restApiHelper.fetchJSON({
+ url: '/groups/',
+ errFn: opt_errFn,
+ params,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} inputVal
+ * @param {number} opt_n
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getSuggestedProjects(inputVal, opt_n, opt_errFn) {
+ const params = {
+ m: inputVal,
+ n: MAX_PROJECT_RESULTS,
+ type: 'ALL',
+ };
+ if (opt_n) { params.n = opt_n; }
+ return this._restApiHelper.fetchJSON({
+ url: '/projects/',
+ errFn: opt_errFn,
+ params,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {string} inputVal
+ * @param {number} opt_n
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getSuggestedAccounts(inputVal, opt_n, opt_errFn) {
+ if (!inputVal) {
+ return Promise.resolve([]);
+ }
+ const params = {suggest: null, q: inputVal};
+ if (opt_n) { params.n = opt_n; }
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/',
+ errFn: opt_errFn,
+ params,
+ anonymizedUrl: '/accounts/?n=*',
+ });
+ }
+
+ addChangeReviewer(changeNum, reviewerID) {
+ return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
+ }
+
+ removeChangeReviewer(changeNum, reviewerID) {
+ return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID);
+ }
+
+ _sendChangeReviewerRequest(method, changeNum, reviewerID) {
+ return this.getChangeActionURL(changeNum, null, '/reviewers')
+ .then(url => {
+ let body;
+ switch (method) {
+ case 'POST':
+ body = {reviewer: reviewerID};
+ break;
+ case 'DELETE':
+ url += '/' + encodeURIComponent(reviewerID);
+ break;
+ default:
+ throw Error('Unsupported HTTP method: ' + method);
+ }
+
+ return this._restApiHelper.send({method, url, body});
+ });
+ }
+
+ getRelatedChanges(changeNum, patchNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/related',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ getChangesSubmittedTogether(changeNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ getChangeConflicts(changeNum) {
+ const options = this.listChangesOptionsToHex(
+ this.ListChangesOption.CURRENT_REVISION,
+ this.ListChangesOption.CURRENT_COMMIT
+ );
+ const params = {
+ O: options,
+ q: 'status:open conflicts:' + changeNum,
+ };
+ return this._restApiHelper.fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/conflicts:*',
+ });
+ }
+
+ getChangeCherryPicks(project, changeID, changeNum) {
+ const options = this.listChangesOptionsToHex(
+ this.ListChangesOption.CURRENT_REVISION,
+ this.ListChangesOption.CURRENT_COMMIT
+ );
+ const query = [
+ 'project:' + project,
+ 'change:' + changeID,
+ '-change:' + changeNum,
+ '-is:abandoned',
+ ].join(' ');
+ const params = {
+ O: options,
+ q: query,
+ };
+ return this._restApiHelper.fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/change:*',
+ });
+ }
+
+ getChangesWithSameTopic(topic, changeNum) {
+ const options = this.listChangesOptionsToHex(
+ this.ListChangesOption.LABELS,
+ this.ListChangesOption.CURRENT_REVISION,
+ this.ListChangesOption.CURRENT_COMMIT,
+ this.ListChangesOption.DETAILED_LABELS
+ );
+ const query = [
+ 'status:open',
+ '-change:' + changeNum,
+ `topic:"${topic}"`,
+ ].join(' ');
+ const params = {
+ O: options,
+ q: query,
+ };
+ return this._restApiHelper.fetchJSON({
+ url: '/changes/',
+ params,
+ anonymizedUrl: '/changes/topic:*',
+ });
+ }
+
+ getReviewedFiles(changeNum, patchNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/files?reviewed',
+ patchNum,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ * @param {string} path
+ * @param {boolean} reviewed
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: reviewed ? 'PUT' : 'DELETE',
+ patchNum,
+ endpoint: `/files/${encodeURIComponent(path)}/reviewed`,
+ errFn: opt_errFn,
+ anonymizedEndpoint: '/files/*/reviewed',
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} patchNum
+ * @param {!Object} review
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ saveChangeReview(changeNum, patchNum, review, opt_errFn) {
+ const promises = [
+ this.awaitPendingDiffDrafts(),
+ this.getChangeActionURL(changeNum, patchNum, '/review'),
+ ];
+ return Promise.all(promises).then(([, url]) => this._restApiHelper.send({
+ method: 'POST',
+ url,
+ body: review,
+ errFn: opt_errFn,
+ }));
+ }
+
+ getChangeEdit(changeNum, opt_download_commands) {
+ const params = opt_download_commands ? {'download-commands': true} : null;
+ return this.getLoggedIn().then(loggedIn => {
+ if (!loggedIn) { return false; }
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/edit/',
+ params,
+ reportEndpointAsIs: true,
+ }, true);
+ });
+ }
+
+ /**
+ * @param {string} project
+ * @param {string} branch
+ * @param {string} subject
+ * @param {string=} opt_topic
+ * @param {boolean=} opt_isPrivate
+ * @param {boolean=} opt_workInProgress
+ * @param {string=} opt_baseChange
+ * @param {string=} opt_baseCommit
+ */
+ createChange(project, branch, subject, opt_topic, opt_isPrivate,
+ opt_workInProgress, opt_baseChange, opt_baseCommit) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/changes/',
+ body: {
+ project,
+ branch,
+ subject,
+ topic: opt_topic,
+ is_private: opt_isPrivate,
+ work_in_progress: opt_workInProgress,
+ base_change: opt_baseChange,
+ base_commit: opt_baseCommit,
+ },
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} path
+ * @param {number|string} patchNum
+ */
+ getFileContent(changeNum, path, patchNum) {
+ // 404s indicate the file does not exist yet in the revision, so suppress
+ // them.
+ const suppress404s = res => {
+ if (res && res.status !== 404) { this.fire('server-error', {res}); }
+ return res;
+ };
+ const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ?
+ this._getFileInChangeEdit(changeNum, path) :
+ this._getFileInRevision(changeNum, path, patchNum, suppress404s);
+
+ return promise.then(res => {
+ if (!res.ok) { return res; }
+
+ // The file type (used for syntax highlighting) is identified in the
+ // X-FYI-Content-Type header of the response.
+ const type = res.headers.get('X-FYI-Content-Type');
+ return this.getResponseObject(res).then(content => {
+ return {content, type, ok: true};
+ });
+ });
+ }
+
+ /**
+ * Gets a file in a specific change and revision.
+ *
+ * @param {number|string} changeNum
+ * @param {string} path
+ * @param {number|string} patchNum
+ * @param {?function(?Response, string=)=} opt_errFn
+ */
+ _getFileInRevision(changeNum, path, patchNum, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'GET',
+ patchNum,
+ endpoint: `/files/${encodeURIComponent(path)}/content`,
+ errFn: opt_errFn,
+ headers: {Accept: 'application/json'},
+ anonymizedEndpoint: '/files/*/content',
+ });
+ }
+
+ /**
+ * Gets a file in a change edit.
+ *
+ * @param {number|string} changeNum
+ * @param {string} path
+ */
+ _getFileInChangeEdit(changeNum, path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'GET',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ headers: {Accept: 'application/json'},
+ anonymizedEndpoint: '/edit/*',
+ });
+ }
+
+ rebaseChangeEdit(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit:rebase',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteChangeEdit(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/edit',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ restoreFileInChangeEdit(changeNum, restore_path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit',
+ body: {restore_path},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ renameFileInChangeEdit(changeNum, old_path, new_path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit',
+ body: {old_path, new_path},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteFileInChangeEdit(changeNum, path) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ anonymizedEndpoint: '/edit/*',
+ });
+ }
+
+ saveChangeEdit(changeNum, path, contents) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/edit/' + encodeURIComponent(path),
+ body: contents,
+ contentType: 'text/plain',
+ anonymizedEndpoint: '/edit/*',
+ });
+ }
+
+ getRobotCommentFixPreview(changeNum, patchNum, fixId) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ patchNum,
+ endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`,
+ reportEndpointAsId: true,
+ });
+ }
+
+ applyFixSuggestion(changeNum, patchNum, fixId) {
+ return this._getChangeURLAndSend({
+ method: 'POST',
+ changeNum,
+ patchNum,
+ endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`,
+ reportEndpointAsId: true,
+ });
+ }
+
+ // Deprecated, prefer to use putChangeCommitMessage instead.
+ saveChangeCommitMessageEdit(changeNum, message) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/edit:message',
+ body: {message},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ publishChangeEdit(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/edit:publish',
+ reportEndpointAsIs: true,
+ });
+ }
+
+ putChangeCommitMessage(changeNum, message) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/message',
+ body: {message},
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteChangeCommitMessage(changeNum, messageId) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/messages/' + messageId,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ saveChangeStarred(changeNum, starred) {
+ // Some servers may require the project name to be provided
+ // alongside the change number, so resolve the project name
+ // first.
+ return this.getFromProjectLookup(changeNum).then(project => {
+ const url = '/accounts/self/starred.changes/' +
+ (project ? encodeURIComponent(project) + '~' : '') + changeNum;
+ return this._restApiHelper.send({
+ method: starred ? 'PUT' : 'DELETE',
+ url,
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+ });
+ }
+
+ saveChangeReviewed(changeNum, reviewed) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: reviewed ? '/reviewed' : '/unreviewed',
+ });
+ }
+
+ /**
+ * Public version of the _restApiHelper.send method preserved for plugins.
+ *
+ * @param {string} method
+ * @param {string} url
+ * @param {?string|number|Object=} opt_body passed as null sometimes
+ * and also apparently a number. TODO (beckysiegel) remove need for
+ * number at least.
+ * @param {?function(?Response, string=)=} opt_errFn
+ * passed as null sometimes.
+ * @param {?string=} opt_contentType
+ * @param {Object=} opt_headers
+ */
+ send(method, url, opt_body, opt_errFn, opt_contentType,
+ opt_headers) {
+ return this._restApiHelper.send({
+ method,
+ url,
+ body: opt_body,
+ errFn: opt_errFn,
+ contentType: opt_contentType,
+ headers: opt_headers,
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string} basePatchNum Negative values specify merge parent
+ * index.
+ * @param {number|string} patchNum
+ * @param {string} path
+ * @param {string=} opt_whitespace the ignore-whitespace level for the diff
+ * algorithm.
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace,
+ opt_errFn) {
+ const params = {
+ context: 'ALL',
+ intraline: null,
+ whitespace: opt_whitespace || 'IGNORE_NONE',
+ };
+ if (this.isMergeParent(basePatchNum)) {
+ params.parent = this.getParentIndex(basePatchNum);
+ } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) {
+ params.base = basePatchNum;
+ }
+ const endpoint = `/files/${encodeURIComponent(path)}/diff`;
+ const req = {
+ changeNum,
+ endpoint,
+ patchNum,
+ errFn: opt_errFn,
+ params,
+ anonymizedEndpoint: '/files/*/diff',
+ };
+
+ // Invalidate the cache if its edit patch to make sure we always get latest.
+ if (patchNum === this.EDIT_NAME) {
+ if (!req.fetchOptions) req.fetchOptions = {};
+ if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
+ req.fetchOptions.headers.append('Cache-Control', 'no-cache');
+ }
+
+ return this._getChangeURLAndFetch(req);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
+ return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
+ opt_patchNum, opt_path);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
+ return this._getDiffComments(changeNum, '/robotcomments',
+ opt_basePatchNum, opt_patchNum, opt_path);
+ }
+
+ /**
+ * If the user is logged in, fetch the user's draft diff comments. If there
+ * is no logged in user, the request is not made and the promise yields an
+ * empty object.
+ *
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) {
+ return this.getLoggedIn().then(loggedIn => {
+ if (!loggedIn) { return Promise.resolve({}); }
+ return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum,
+ opt_patchNum, opt_path);
+ });
+ }
+
+ _setRange(comments, comment) {
+ if (comment.in_reply_to && !comment.range) {
+ for (let i = 0; i < comments.length; i++) {
+ if (comments[i].id === comment.in_reply_to) {
+ comment.range = comments[i].range;
+ break;
+ }
+ }
+ }
+ return comment;
+ }
+
+ _setRanges(comments) {
+ comments = comments || [];
+ comments.sort(
+ (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated)
+ );
+ for (const comment of comments) {
+ this._setRange(comments, comment);
+ }
+ return comments;
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} endpoint
+ * @param {number|string=} opt_basePatchNum
+ * @param {number|string=} opt_patchNum
+ * @param {string=} opt_path
+ * @return {!Promise<!Object>}
+ */
+ _getDiffComments(changeNum, endpoint, opt_basePatchNum,
+ opt_patchNum, opt_path) {
+ /**
+ * Fetches the comments for a given patchNum.
+ * Helper function to make promises more legible.
+ *
+ * @param {string|number=} opt_patchNum
+ * @return {!Promise<!Object>} Diff comments response.
+ */
+ // We don't want to add accept header, since preloading of comments is
+ // working only without accept header.
+ const noAcceptHeader = true;
+ const fetchComments = opt_patchNum => this._getChangeURLAndFetch({
+ changeNum,
+ endpoint,
+ patchNum: opt_patchNum,
+ reportEndpointAsIs: true,
+ }, noAcceptHeader);
+
+ if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
+ return fetchComments();
+ }
+ function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
+ function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
+ function setPath(c) { c.path = opt_path; }
+
+ const promises = [];
+ let comments;
+ let baseComments;
+ let fetchPromise;
+ fetchPromise = fetchComments(opt_patchNum).then(response => {
+ comments = response[opt_path] || [];
+ // TODO(kaspern): Implement this on in the backend so this can
+ // be removed.
+ // Sort comments by date so that parent ranges can be propagated
+ // in a single pass.
+ comments = this._setRanges(comments);
+
+ if (opt_basePatchNum == PARENT_PATCH_NUM) {
+ baseComments = comments.filter(onlyParent);
+ baseComments.forEach(setPath);
+ }
+ comments = comments.filter(withoutParent);
+
+ comments.forEach(setPath);
+ });
+ promises.push(fetchPromise);
+
+ if (opt_basePatchNum != PARENT_PATCH_NUM) {
+ fetchPromise = fetchComments(opt_basePatchNum).then(response => {
+ baseComments = (response[opt_path] || [])
+ .filter(withoutParent);
+ baseComments = this._setRanges(baseComments);
+ baseComments.forEach(setPath);
+ });
+ promises.push(fetchPromise);
+ }
+
+ return Promise.all(promises).then(() => Promise.resolve({
+ baseComments,
+ comments,
+ }));
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {string} endpoint
+ * @param {number|string=} opt_patchNum
+ */
+ _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) {
+ return this._changeBaseURL(changeNum, opt_patchNum)
+ .then(url => url + endpoint);
+ }
+
+ saveDiffDraft(changeNum, patchNum, draft) {
+ return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft);
+ }
+
+ deleteDiffDraft(changeNum, patchNum, draft) {
+ return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft);
+ }
+
+ /**
+ * @returns {boolean} Whether there are pending diff draft sends.
+ */
+ hasPendingDiffDrafts() {
+ const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT];
+ return promises && promises.length;
+ }
+
+ /**
+ * @returns {!Promise<undefined>} A promise that resolves when all pending
+ * diff draft sends have resolved.
+ */
+ awaitPendingDiffDrafts() {
+ return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || [])
+ .then(() => {
+ this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
+ });
+ }
+
+ _sendDiffDraftRequest(method, changeNum, patchNum, draft) {
+ const isCreate = !draft.id && method === 'PUT';
+ let endpoint = '/drafts';
+ let anonymizedEndpoint = endpoint;
+ if (draft.id) {
+ endpoint += '/' + draft.id;
+ anonymizedEndpoint += '/*';
+ }
+ let body;
+ if (method === 'PUT') {
+ body = draft;
+ }
+
+ if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) {
+ this._pendingRequests[Requests.SEND_DIFF_DRAFT] = [];
+ }
+
+ const req = {
+ changeNum,
+ method,
+ patchNum,
+ endpoint,
+ body,
+ anonymizedEndpoint,
+ };
+
+ const promise = this._getChangeURLAndSend(req);
+ this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
+
+ if (isCreate) {
+ return this._failForCreate200(promise);
+ }
+
+ return promise;
+ }
+
+ getCommitInfo(project, commit) {
+ return this._restApiHelper.fetchJSON({
+ url: '/projects/' + encodeURIComponent(project) +
+ '/commits/' + encodeURIComponent(commit),
+ anonymizedUrl: '/projects/*/comments/*',
+ });
+ }
+
+ _fetchB64File(url) {
+ return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
+ .then(response => {
+ if (!response.ok) {
+ return Promise.reject(new Error(response.statusText));
+ }
+ const type = response.headers.get('X-FYI-Content-Type');
+ return response.text()
+ .then(text => {
+ return {body: text, type};
+ });
+ });
+ }
+
+ /**
+ * @param {string} changeId
+ * @param {string|number} patchNum
+ * @param {string} path
+ * @param {number=} opt_parentIndex
+ */
+ getB64FileContents(changeId, patchNum, path, opt_parentIndex) {
+ const parent = typeof opt_parentIndex === 'number' ?
+ '?parent=' + opt_parentIndex : '';
+ return this._changeBaseURL(changeId, patchNum).then(url => {
+ url = `${url}/files/${encodeURIComponent(path)}/content${parent}`;
+ return this._fetchB64File(url);
+ });
+ }
+
+ getImagesForDiff(changeNum, diff, patchRange) {
+ let promiseA;
+ let promiseB;
+
+ if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) {
+ if (patchRange.basePatchNum === 'PARENT') {
+ // Note: we only attempt to get the image from the first parent.
+ promiseA = this.getB64FileContents(changeNum, patchRange.patchNum,
+ diff.meta_a.name, 1);
+ } else {
+ promiseA = this.getB64FileContents(changeNum,
+ patchRange.basePatchNum, diff.meta_a.name);
+ }
+ } else {
+ promiseA = Promise.resolve(null);
+ }
+
+ if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) {
+ promiseB = this.getB64FileContents(changeNum, patchRange.patchNum,
+ diff.meta_b.name);
+ } else {
+ promiseB = Promise.resolve(null);
+ }
+
+ return Promise.all([promiseA, promiseB]).then(results => {
+ const baseImage = results[0];
+ const revisionImage = results[1];
+
+ // Sometimes the server doesn't send back the content type.
+ if (baseImage) {
+ baseImage._expectedType = diff.meta_a.content_type;
+ baseImage._name = diff.meta_a.name;
+ }
+ if (revisionImage) {
+ revisionImage._expectedType = diff.meta_b.content_type;
+ revisionImage._name = diff.meta_b.name;
+ }
+
+ return {baseImage, revisionImage};
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {?number|string=} opt_patchNum passed as null sometimes.
+ * @param {string=} opt_project
+ * @return {!Promise<string>}
+ */
+ _changeBaseURL(changeNum, opt_patchNum, opt_project) {
+ // TODO(kaspern): For full slicer migration, app should warn with a call
+ // stack every time _changeBaseURL is called without a project.
+ const projectPromise = opt_project ?
+ Promise.resolve(opt_project) :
+ this.getFromProjectLookup(changeNum);
+ return projectPromise.then(project => {
+ let url = `/changes/${encodeURIComponent(project)}~${changeNum}`;
+ if (opt_patchNum) {
+ url += `/revisions/${opt_patchNum}`;
+ }
+ return url;
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ setChangeTopic(changeNum, topic) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/topic',
+ body: {topic},
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ setChangeHashtag(changeNum, hashtag) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/hashtags',
+ body: hashtag,
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ deleteAccountHttpPassword() {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/password.http',
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ generateAccountHttpPassword() {
+ return this._restApiHelper.send({
+ method: 'PUT',
+ url: '/accounts/self/password.http',
+ body: {generate: true},
+ parseResponse: true,
+ reportUrlAsIs: true,
+ });
+ }
+
+ getAccountSSHKeys() {
+ return this._fetchSharedCacheURL({
+ url: '/accounts/self/sshkeys',
+ reportUrlAsIs: true,
+ });
+ }
+
+ addAccountSSHKey(key) {
+ const req = {
+ method: 'POST',
+ url: '/accounts/self/sshkeys',
+ body: key,
+ contentType: 'text/plain',
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(response => {
+ if (response.status < 200 && response.status >= 300) {
+ return Promise.reject(new Error('error'));
+ }
+ return this.getResponseObject(response);
+ })
+ .then(obj => {
+ if (!obj.valid) { return Promise.reject(new Error('error')); }
+ return obj;
+ });
+ }
+
+ deleteAccountSSHKey(id) {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/sshkeys/' + id,
+ anonymizedUrl: '/accounts/self/sshkeys/*',
+ });
+ }
+
+ getAccountGPGKeys() {
+ return this._restApiHelper.fetchJSON({
+ url: '/accounts/self/gpgkeys',
+ reportUrlAsIs: true,
+ });
+ }
+
+ addAccountGPGKey(key) {
+ const req = {
+ method: 'POST',
+ url: '/accounts/self/gpgkeys',
+ body: key,
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req)
+ .then(response => {
+ if (response.status < 200 && response.status >= 300) {
+ return Promise.reject(new Error('error'));
+ }
+ return this.getResponseObject(response);
+ })
+ .then(obj => {
+ if (!obj) { return Promise.reject(new Error('error')); }
+ return obj;
+ });
+ }
+
+ deleteAccountGPGKey(id) {
+ return this._restApiHelper.send({
+ method: 'DELETE',
+ url: '/accounts/self/gpgkeys/' + id,
+ anonymizedUrl: '/accounts/self/gpgkeys/*',
+ });
+ }
+
+ deleteVote(changeNum, account, label) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`,
+ anonymizedEndpoint: '/reviewers/*/votes/*',
+ });
+ }
+
+ setDescription(changeNum, patchNum, desc) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT', patchNum,
+ endpoint: '/description',
+ body: {description: desc},
+ reportUrlAsIs: true,
+ });
+ }
+
+ confirmEmail(token) {
+ const req = {
+ method: 'PUT',
+ url: '/config/server/email.confirm',
+ body: {token},
+ reportUrlAsIs: true,
+ };
+ return this._restApiHelper.send(req).then(response => {
+ if (response.status === 204) {
+ return 'Email confirmed successfully.';
+ }
+ return null;
+ });
+ }
+
+ getCapabilities(opt_errFn) {
+ return this._restApiHelper.fetchJSON({
+ url: '/config/server/capabilities',
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ getTopMenus(opt_errFn) {
+ return this._fetchSharedCacheURL({
+ url: '/config/server/top-menus',
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ setAssignee(changeNum, assignee) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'PUT',
+ endpoint: '/assignee',
+ body: {assignee},
+ reportUrlAsIs: true,
+ });
+ }
+
+ deleteAssignee(changeNum) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'DELETE',
+ endpoint: '/assignee',
+ reportUrlAsIs: true,
+ });
+ }
+
+ probePath(path) {
+ return fetch(new Request(path, {method: 'HEAD'}))
+ .then(response => response.ok);
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_message
+ */
+ startWorkInProgress(changeNum, opt_message) {
+ const body = {};
+ if (opt_message) {
+ body.message = opt_message;
+ }
+ const req = {
+ changeNum,
+ method: 'POST',
+ endpoint: '/wip',
+ body,
+ reportUrlAsIs: true,
+ };
+ return this._getChangeURLAndSend(req).then(response => {
+ if (response.status === 204) {
+ return 'Change marked as Work In Progress.';
+ }
+ });
+ }
+
+ /**
+ * @param {number|string} changeNum
+ * @param {number|string=} opt_body
+ * @param {function(?Response, string=)=} opt_errFn
+ */
+ startReview(changeNum, opt_body, opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ endpoint: '/ready',
+ body: opt_body,
+ errFn: opt_errFn,
+ reportUrlAsIs: true,
+ });
+ }
+
+ /**
+ * @suppress {checkTypes}
+ * Resulted in error: Promise.prototype.then does not match formal
+ * parameter.
+ */
+ deleteComment(changeNum, patchNum, commentID, reason) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method: 'POST',
+ patchNum,
+ endpoint: `/comments/${commentID}/delete`,
+ body: {reason},
+ parseResponse: true,
+ anonymizedEndpoint: '/comments/*/delete',
+ });
+ }
+
+ /**
+ * Given a changeNum, gets the change.
+ *
+ * @param {number|string} changeNum
+ * @param {function(?Response, string=)=} opt_errFn
+ * @return {!Promise<?Object>} The change
+ */
+ getChange(changeNum, opt_errFn) {
+ // Cannot use _changeBaseURL, as this function is used by _projectLookup.
+ return this._restApiHelper.fetchJSON({
+ url: `/changes/?q=change:${changeNum}`,
+ errFn: opt_errFn,
+ anonymizedUrl: '/changes/?q=change:*',
+ }).then(res => {
+ if (!res || !res.length) { return null; }
+ return res[0];
+ });
+ }
+
+ /**
+ * @param {string|number} changeNum
+ * @param {string=} project
+ */
+ setInProjectLookup(changeNum, project) {
+ if (this._projectLookup[changeNum] &&
+ this._projectLookup[changeNum] !== project) {
+ console.warn('Change set with multiple project nums.' +
+ 'One of them must be invalid.');
+ }
+ this._projectLookup[changeNum] = project;
+ }
+
+ /**
+ * Checks in _projectLookup for the changeNum. If it exists, returns the
+ * project. If not, calls the restAPI to get the change, populates
+ * _projectLookup with the project for that change, and returns the project.
+ *
+ * @param {string|number} changeNum
+ * @return {!Promise<string|undefined>}
+ */
+ getFromProjectLookup(changeNum) {
+ const project = this._projectLookup[changeNum];
+ if (project) { return Promise.resolve(project); }
+
+ const onError = response => {
+ // Fire a page error so that the visual 404 is displayed.
+ this.fire('page-error', {response});
+ };
+
+ return this.getChange(changeNum, onError).then(change => {
+ if (!change || !change.project) { return; }
+ this.setInProjectLookup(changeNum, change.project);
+ return change.project;
+ });
+ }
+
+ /**
+ * Alias for _changeBaseURL.then(send).
+ *
+ * @todo(beckysiegel) clean up comments
+ * @param {Gerrit.ChangeSendRequest} req
+ * @return {!Promise<!Object>}
+ */
+ _getChangeURLAndSend(req) {
+ const anonymizedBaseUrl = req.patchNum ?
+ ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
+ const anonymizedEndpoint = req.reportEndpointAsIs ?
+ req.endpoint : req.anonymizedEndpoint;
+
+ return this._changeBaseURL(req.changeNum, req.patchNum)
+ .then(url => this._restApiHelper.send({
+ method: req.method,
+ url: url + req.endpoint,
+ body: req.body,
+ errFn: req.errFn,
+ contentType: req.contentType,
+ headers: req.headers,
+ parseResponse: req.parseResponse,
+ anonymizedUrl: anonymizedEndpoint ?
+ (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
+ }));
+ }
+
+ /**
+ * Alias for _changeBaseURL.then(_fetchJSON).
+ *
+ * @param {Gerrit.ChangeFetchRequest} req
+ * @return {!Promise<!Object>}
+ */
+ _getChangeURLAndFetch(req, noAcceptHeader) {
+ const anonymizedEndpoint = req.reportEndpointAsIs ?
+ req.endpoint : req.anonymizedEndpoint;
+ const anonymizedBaseUrl = req.patchNum ?
+ ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
+ return this._changeBaseURL(req.changeNum, req.patchNum)
+ .then(url => this._restApiHelper.fetchJSON({
+ url: url + req.endpoint,
+ errFn: req.errFn,
+ params: req.params,
+ fetchOptions: req.fetchOptions,
+ anonymizedUrl: anonymizedEndpoint ?
+ (anonymizedBaseUrl + anonymizedEndpoint) : undefined,
+ }, noAcceptHeader));
+ }
+
+ /**
+ * Execute a change action or revision action on a change.
+ *
+ * @param {number} changeNum
+ * @param {string} method
+ * @param {string} endpoint
+ * @param {string|number|undefined} opt_patchNum
+ * @param {Object=} opt_payload
+ * @param {?function(?Response, string=)=} opt_errFn
+ * @return {Promise}
+ */
+ executeChangeAction(changeNum, method, endpoint, opt_patchNum, opt_payload,
+ opt_errFn) {
+ return this._getChangeURLAndSend({
+ changeNum,
+ method,
+ patchNum: opt_patchNum,
+ endpoint,
+ body: opt_payload,
+ errFn: opt_errFn,
+ });
+ }
+
+ /**
+ * Get blame information for the given diff.
+ *
+ * @param {string|number} changeNum
+ * @param {string|number} patchNum
+ * @param {string} path
+ * @param {boolean=} opt_base If true, requests blame for the base of the
+ * diff, rather than the revision.
+ * @return {!Promise<!Object>}
+ */
+ getBlame(changeNum, patchNum, path, opt_base) {
+ const encodedPath = encodeURIComponent(path);
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: `/files/${encodedPath}/blame`,
+ patchNum,
+ params: opt_base ? {base: 't'} : undefined,
+ anonymizedEndpoint: '/files/*/blame',
+ });
+ }
+
+ /**
+ * Modify the given create draft request promise so that it fails and throws
+ * an error if the response bears HTTP status 200 instead of HTTP 201.
+ *
+ * @see Issue 7763
+ * @param {Promise} promise The original promise.
+ * @return {Promise} The modified promise.
+ */
+ _failForCreate200(promise) {
+ return promise.then(result => {
+ if (result.status === 200) {
+ // Read the response headers into an object representation.
+ const headers = Array.from(result.headers.entries())
+ .reduce((obj, [key, val]) => {
+ if (!HEADER_REPORTING_BLACKLIST.test(key)) {
+ obj[key] = val;
+ }
+ return obj;
+ }, {});
+ const err = new Error([
+ CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE,
+ JSON.stringify(headers),
+ ].join('\n'));
+ // Throw the error so that it is caught by gr-reporting.
+ throw err;
+ }
+ return result;
+ });
+ }
+
+ /**
+ * Fetch a project dashboard definition.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard
+ *
+ * @param {string} project
+ * @param {string} dashboard
+ * @param {function(?Response, string=)=} opt_errFn
+ * passed as null sometimes.
+ * @return {!Promise<!Object>}
+ */
+ getDashboard(project, dashboard, opt_errFn) {
+ const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' +
+ encodeURIComponent(dashboard);
+ return this._fetchSharedCacheURL({
+ url,
+ errFn: opt_errFn,
+ anonymizedUrl: '/projects/*/dashboards/*',
+ });
+ }
+
+ /**
+ * @param {string} filter
+ * @return {!Promise<?Object>}
+ */
+ getDocumentationSearches(filter) {
+ filter = filter.trim();
+ const encodedFilter = encodeURIComponent(filter);
+
+ // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
+ // supports it.
+ return this._fetchSharedCacheURL({
+ url: `/Documentation/?q=${encodedFilter}`,
+ anonymizedUrl: '/Documentation/?*',
+ });
+ }
+
+ getMergeable(changeNum) {
+ return this._getChangeURLAndFetch({
+ changeNum,
+ endpoint: '/revisions/current/mergeable',
+ parseResponse: true,
+ reportEndpointAsIs: true,
+ });
+ }
+
+ deleteDraftComments(query) {
+ return this._restApiHelper.send({
+ method: 'POST',
+ url: '/accounts/self/drafts:delete',
+ body: {query},
+ });
+ }
+}
+
+customElements.define(GrRestApiInterface.is, GrRestApiInterface);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 1088f7e..13ed562 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -19,17 +19,23 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rest-api-interface</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="../../../scripts/util.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 type="module" src="../../../scripts/util.js"></script>
-<link rel="import" href="gr-rest-api-interface.html">
+<script type="module" src="./gr-rest-api-interface.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-rest-api-interface.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,92 +43,151 @@
</template>
</test-fixture>
-<script>
- suite('gr-rest-api-interface tests', async () => {
- await readyToTest();
- let element;
- let sandbox;
- let ctr = 0;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-rest-api-interface.js';
+suite('gr-rest-api-interface tests', () => {
+ let element;
+ let sandbox;
+ let ctr = 0;
- setup(() => {
- // Modify CANONICAL_PATH to effectively reset cache.
- ctr += 1;
- window.CANONICAL_PATH = `test${ctr}`;
+ setup(() => {
+ // Modify CANONICAL_PATH to effectively reset cache.
+ ctr += 1;
+ window.CANONICAL_PATH = `test${ctr}`;
- sandbox = sinon.sandbox.create();
- const testJSON = ')]}\'\n{"hello": "bonjour"}';
- sandbox.stub(window, 'fetch').returns(Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(testJSON);
- },
- }));
- // fake auth
- sandbox.stub(Gerrit.Auth, 'authCheck').returns(Promise.resolve(true));
- element = fixture('basic');
- element._projectLookup = {};
- });
+ sandbox = sinon.sandbox.create();
+ const testJSON = ')]}\'\n{"hello": "bonjour"}';
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(testJSON);
+ },
+ }));
+ // fake auth
+ sandbox.stub(Gerrit.Auth, 'authCheck').returns(Promise.resolve(true));
+ element = fixture('basic');
+ element._projectLookup = {};
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('parent diff comments are properly grouped', done => {
- sandbox.stub(element._restApiHelper, 'fetchJSON', () => Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- updated: '2017-02-03 22:32:28.000000000',
- message: 'this isn’t quite right',
- },
- {
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- }));
- element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
- obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
- side: 'PARENT',
- message: 'how did this work in the first place?',
- path: 'sieve.go',
- updated: '2017-02-03 22:33:28.000000000',
- });
- assert.equal(obj.comments.length, 1);
- assert.deepEqual(obj.comments[0], {
- message: 'this isn’t quite right',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- done();
- });
- });
-
- test('_setRange', () => {
- const comments = [
+ test('parent diff comments are properly grouped', done => {
+ sandbox.stub(element._restApiHelper, 'fetchJSON', () => Promise.resolve({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
{
- id: 1,
+ updated: '2017-02-03 22:32:28.000000000',
+ message: 'this isn’t quite right',
+ },
+ {
side: 'PARENT',
message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
updated: '2017-02-03 22:33:28.000000000',
},
- ];
- const expectedResult = {
+ ],
+ }));
+ element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
+ obj => {
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:33:28.000000000',
+ });
+ assert.equal(obj.comments.length, 1);
+ assert.deepEqual(obj.comments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000',
+ });
+ done();
+ });
+ });
+
+ test('_setRange', () => {
+ const comments = [
+ {
+ id: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: 2,
+ in_reply_to: 1,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ];
+ const expectedResult = {
+ id: 2,
+ in_reply_to: 1,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ };
+ const comment = comments[1];
+ assert.deepEqual(element._setRange(comments, comment), expectedResult);
+ });
+
+ test('_setRanges', () => {
+ const comments = [
+ {
+ id: 3,
+ in_reply_to: 2,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000',
+ },
+ {
+ id: 2,
+ in_reply_to: 1,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ {
+ id: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ const expectedResult = [
+ {
+ id: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
id: 2,
in_reply_to: 1,
message: 'this isn’t quite right',
@@ -133,1347 +198,1291 @@
end_line: 2,
end_character: 1,
},
- };
- const comment = comments[1];
- assert.deepEqual(element._setRange(comments, comment), expectedResult);
- });
+ },
+ {
+ id: 3,
+ in_reply_to: 2,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ assert.deepEqual(element._setRanges(comments), expectedResult);
+ });
- test('_setRanges', () => {
- const comments = [
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- id: 1,
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- const expectedResult = [
- {
- id: 1,
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- assert.deepEqual(element._setRanges(comments), expectedResult);
- });
-
- test('differing patch diff comments are properly grouped', done => {
- sandbox.stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sandbox.stub(element._restApiHelper, 'fetchJSON', request => {
- const url = request.url;
- if (url === '/changes/test~42/revisions/1') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: 'PARENT',
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- });
- } else if (url === '/changes/test~42/revisions/2') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'What on earth are you thinking, here?',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: 'PARENT',
- message: 'Yeah not sure how this worked either?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- message: '¯\\_(ツ)_/¯',
- updated: '2017-02-04 22:33:28.000000000',
- },
- ],
- });
- }
- });
- element._getDiffComments('42', '', 1, 2, 'sieve.go').then(
- obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
+ test('differing patch diff comments are properly grouped', done => {
+ sandbox.stub(element, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ sandbox.stub(element._restApiHelper, 'fetchJSON', request => {
+ const url = request.url;
+ if (url === '/changes/test~42/revisions/1') {
+ return Promise.resolve({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
message: 'this isn’t quite right',
- path: 'sieve.go',
updated: '2017-02-03 22:32:28.000000000',
- });
- assert.equal(obj.comments.length, 2);
- assert.deepEqual(obj.comments[0], {
+ },
+ {
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ],
+ });
+ } else if (url === '/changes/test~42/revisions/2') {
+ return Promise.resolve({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
message: 'What on earth are you thinking, here?',
- path: 'sieve.go',
updated: '2017-02-03 22:32:28.000000000',
- });
- assert.deepEqual(obj.comments[1], {
+ },
+ {
+ side: 'PARENT',
+ message: 'Yeah not sure how this worked either?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ {
message: '¯\\_(ツ)_/¯',
- path: 'sieve.go',
updated: '2017-02-04 22:33:28.000000000',
- });
+ },
+ ],
+ });
+ }
+ });
+ element._getDiffComments('42', '', 1, 2, 'sieve.go').then(
+ obj => {
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000',
+ });
+ assert.equal(obj.comments.length, 2);
+ assert.deepEqual(obj.comments[0], {
+ message: 'What on earth are you thinking, here?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000',
+ });
+ assert.deepEqual(obj.comments[1], {
+ message: '¯\\_(ツ)_/¯',
+ path: 'sieve.go',
+ updated: '2017-02-04 22:33:28.000000000',
+ });
+ done();
+ });
+ });
+
+ test('special file path sorting', () => {
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.a', '.b', 'file']);
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
+
+ assert.deepEqual(
+ ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
+ element.specialFilePathCompare),
+ ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
+
+ assert.deepEqual(
+ ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
+ element.specialFilePathCompare),
+ ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
+
+ // Regression test for Issue 4448.
+ assert.deepEqual(
+ [
+ 'minidump/minidump_memory_writer.cc',
+ 'minidump/minidump_memory_writer.h',
+ 'minidump/minidump_thread_writer.cc',
+ 'minidump/minidump_thread_writer.h',
+ ].sort(element.specialFilePathCompare),
+ [
+ 'minidump/minidump_memory_writer.h',
+ 'minidump/minidump_memory_writer.cc',
+ 'minidump/minidump_thread_writer.h',
+ 'minidump/minidump_thread_writer.cc',
+ ]);
+
+ // Regression test for Issue 4545.
+ assert.deepEqual(
+ [
+ 'task_test.go',
+ 'task.go',
+ ].sort(element.specialFilePathCompare),
+ [
+ 'task.go',
+ 'task_test.go',
+ ]);
+ });
+
+ suite('rebase action', () => {
+ let resolve_fetchJSON;
+ setup(() => {
+ sandbox.stub(element._restApiHelper, 'fetchJSON').returns(
+ new Promise(resolve => {
+ resolve_fetchJSON = resolve;
+ }));
+ });
+
+ test('no rebase on current', done => {
+ element.getChangeRevisionActions('42', '1337').then(
+ response => {
+ assert.isTrue(response.rebase.enabled);
+ assert.isFalse(response.rebase.rebaseOnCurrent);
done();
});
+ resolve_fetchJSON({rebase: {}});
});
- test('special file path sorting', () => {
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.a', '.b', 'file']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
- element.specialFilePathCompare),
- ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
-
- assert.deepEqual(
- ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
- element.specialFilePathCompare),
- ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
-
- // Regression test for Issue 4448.
- assert.deepEqual(
- [
- 'minidump/minidump_memory_writer.cc',
- 'minidump/minidump_memory_writer.h',
- 'minidump/minidump_thread_writer.cc',
- 'minidump/minidump_thread_writer.h',
- ].sort(element.specialFilePathCompare),
- [
- 'minidump/minidump_memory_writer.h',
- 'minidump/minidump_memory_writer.cc',
- 'minidump/minidump_thread_writer.h',
- 'minidump/minidump_thread_writer.cc',
- ]);
-
- // Regression test for Issue 4545.
- assert.deepEqual(
- [
- 'task_test.go',
- 'task.go',
- ].sort(element.specialFilePathCompare),
- [
- 'task.go',
- 'task_test.go',
- ]);
- });
-
- suite('rebase action', () => {
- let resolve_fetchJSON;
- setup(() => {
- sandbox.stub(element._restApiHelper, 'fetchJSON').returns(
- new Promise(resolve => {
- resolve_fetchJSON = resolve;
- }));
- });
-
- test('no rebase on current', done => {
- element.getChangeRevisionActions('42', '1337').then(
- response => {
- assert.isTrue(response.rebase.enabled);
- assert.isFalse(response.rebase.rebaseOnCurrent);
- done();
- });
- resolve_fetchJSON({rebase: {}});
- });
-
- test('rebase on current', done => {
- element.getChangeRevisionActions('42', '1337').then(
- response => {
- assert.isTrue(response.rebase.enabled);
- assert.isTrue(response.rebase.rebaseOnCurrent);
- done();
- });
- resolve_fetchJSON({rebase: {enabled: true}});
- });
- });
-
- test('server error', done => {
- const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
- window.fetch.returns(Promise.resolve({ok: false}));
- const serverErrorEventPromise = new Promise(resolve => {
- element.addEventListener('server-error', resolve);
- });
-
- element._restApiHelper.fetchJSON({}).then(response => {
- assert.isUndefined(response);
- assert.isTrue(getResponseObjectStub.notCalled);
- serverErrorEventPromise.then(() => done());
- });
- });
-
- test('legacy n,z key in change url is replaced', () => {
- const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([]));
- element.getChanges(1, null, 'n,z');
- assert.equal(stub.lastCall.args[0].params.S, 0);
- });
-
- test('saveDiffPreferences invalidates cache line', () => {
- const cacheKey = '/accounts/self/preferences.diff';
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element._cache.set(cacheKey, {tab_size: 4});
- element.saveDiffPreferences({tab_size: 8});
- assert.isTrue(sendStub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
- });
-
- test('getAccount when resp is null does not add anything to the cache',
- done => {
- const cacheKey = '/accounts/self/detail';
- const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
- () => Promise.resolve());
-
- element.getAccount().then(() => {
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+ test('rebase on current', done => {
+ element.getChangeRevisionActions('42', '1337').then(
+ response => {
+ assert.isTrue(response.rebase.enabled);
+ assert.isTrue(response.rebase.rebaseOnCurrent);
done();
});
+ resolve_fetchJSON({rebase: {enabled: true}});
+ });
+ });
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn();
+ test('server error', done => {
+ const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
+ window.fetch.returns(Promise.resolve({ok: false}));
+ const serverErrorEventPromise = new Promise(resolve => {
+ element.addEventListener('server-error', resolve);
+ });
+
+ element._restApiHelper.fetchJSON({}).then(response => {
+ assert.isUndefined(response);
+ assert.isTrue(getResponseObjectStub.notCalled);
+ serverErrorEventPromise.then(() => done());
+ });
+ });
+
+ test('legacy n,z key in change url is replaced', () => {
+ const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve([]));
+ element.getChanges(1, null, 'n,z');
+ assert.equal(stub.lastCall.args[0].params.S, 0);
+ });
+
+ test('saveDiffPreferences invalidates cache line', () => {
+ const cacheKey = '/accounts/self/preferences.diff';
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element._cache.set(cacheKey, {tab_size: 4});
+ element.saveDiffPreferences({tab_size: 8});
+ assert.isTrue(sendStub.called);
+ assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+ });
+
+ test('getAccount when resp is null does not add anything to the cache',
+ done => {
+ const cacheKey = '/accounts/self/detail';
+ const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+ () => Promise.resolve());
+
+ element.getAccount().then(() => {
+ assert.isTrue(stub.called);
+ assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+ done();
});
- test('getAccount does not add to the cache when resp.status is 403',
- done => {
- const cacheKey = '/accounts/self/detail';
- const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
- () => Promise.resolve());
-
- element.getAccount().then(() => {
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
- done();
- });
- element._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn({status: 403});
- });
-
- test('getAccount when resp is successful', done => {
- const cacheKey = '/accounts/self/detail';
- const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
- () => Promise.resolve());
-
- element.getAccount().then(response => {
- assert.isTrue(stub.called);
- assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
- done();
+ element._restApiHelper._cache.set(cacheKey, 'fake cache');
+ stub.lastCall.args[0].errFn();
});
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn({});
- });
+ test('getAccount does not add to the cache when resp.status is 403',
+ done => {
+ const cacheKey = '/accounts/self/detail';
+ const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+ () => Promise.resolve());
- const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
- sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(loggedIn));
- sandbox.stub(element, '_isNarrowScreen', () => smallScreen);
- sandbox.stub(
- element._restApiHelper,
- 'fetchCacheURL',
- () => Promise.resolve(testJSON));
- };
-
- test('getPreferences returns correctly on small screens logged in',
- done => {
- const testJSON = {diff_view: 'SIDE_BY_SIDE'};
- const loggedIn = true;
- const smallScreen = true;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
-
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- done();
- });
+ element.getAccount().then(() => {
+ assert.isTrue(stub.called);
+ assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+ done();
});
-
- test('getPreferences returns correctly on small screens not logged in',
- done => {
- const testJSON = {diff_view: 'SIDE_BY_SIDE'};
- const loggedIn = false;
- const smallScreen = true;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('getPreferences returns correctly on larger screens logged in',
- done => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = true;
- const smallScreen = false;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
-
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
- assert.equal(obj.diff_view, 'UNIFIED_DIFF');
- done();
- });
- });
-
- test('getPreferences returns correctly on larger screens not logged in',
- done => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = false;
- const smallScreen = false;
-
- preferenceSetup(testJSON, loggedIn, smallScreen);
-
- element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- done();
- });
- });
-
- test('savPreferences normalizes download scheme', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element.savePreferences({download_scheme: 'HTTP'});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
- });
-
- test('getDiffPreferences returns correct defaults', done => {
- sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
-
- element.getDiffPreferences().then(obj => {
- assert.equal(obj.auto_hide_diff_table_header, true);
- assert.equal(obj.context, 10);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.font_size, 12);
- assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
- assert.equal(obj.intraline_difference, true);
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.show_line_endings, true);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- assert.equal(obj.theme, 'DEFAULT');
- done();
+ element._cache.set(cacheKey, 'fake cache');
+ stub.lastCall.args[0].errFn({status: 403});
});
+
+ test('getAccount when resp is successful', done => {
+ const cacheKey = '/accounts/self/detail';
+ const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+ () => Promise.resolve());
+
+ element.getAccount().then(response => {
+ assert.isTrue(stub.called);
+ assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
+ done();
});
+ element._restApiHelper._cache.set(cacheKey, 'fake cache');
- test('saveDiffPreferences set show_tabs to false', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element.saveDiffPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
+ stub.lastCall.args[0].errFn({});
+ });
- test('getEditPreferences returns correct defaults', done => {
- sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+ const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
+ sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(loggedIn));
+ sandbox.stub(element, '_isNarrowScreen', () => smallScreen);
+ sandbox.stub(
+ element._restApiHelper,
+ 'fetchCacheURL',
+ () => Promise.resolve(testJSON));
+ };
- element.getEditPreferences().then(obj => {
- assert.equal(obj.auto_close_brackets, false);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.hide_line_numbers, false);
- assert.equal(obj.hide_top_menu, false);
- assert.equal(obj.indent_unit, 2);
- assert.equal(obj.indent_with_tabs, false);
- assert.equal(obj.key_map_type, 'DEFAULT');
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.match_brackets, true);
- assert.equal(obj.show_base, false);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- assert.equal(obj.theme, 'DEFAULT');
- done();
+ test('getPreferences returns correctly on small screens logged in',
+ done => {
+ const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+ const loggedIn = true;
+ const smallScreen = true;
+
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+ assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+ done();
+ });
});
- });
- test('saveEditPreferences set show_tabs to false', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send');
- element.saveEditPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
+ test('getPreferences returns correctly on small screens not logged in',
+ done => {
+ const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+ const loggedIn = false;
+ const smallScreen = true;
- test('confirmEmail', () => {
- const sendStub = sandbox.spy(element._restApiHelper, 'send');
- element.confirmEmail('foo');
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+ assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('getPreferences returns correctly on larger screens logged in',
+ done => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = true;
+ const smallScreen = false;
+
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
+ assert.equal(obj.diff_view, 'UNIFIED_DIFF');
+ done();
+ });
+ });
+
+ test('getPreferences returns correctly on larger screens not logged in',
+ done => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = false;
+ const smallScreen = false;
+
+ preferenceSetup(testJSON, loggedIn, smallScreen);
+
+ element.getPreferences().then(obj => {
+ assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
+ assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('savPreferences normalizes download scheme', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element.savePreferences({download_scheme: 'HTTP'});
+ assert.isTrue(sendStub.called);
+ assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
+ });
+
+ test('getDiffPreferences returns correct defaults', done => {
+ sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+
+ element.getDiffPreferences().then(obj => {
+ assert.equal(obj.auto_hide_diff_table_header, true);
+ assert.equal(obj.context, 10);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.font_size, 12);
+ assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
+ assert.equal(obj.intraline_difference, true);
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.show_line_endings, true);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ assert.equal(obj.theme, 'DEFAULT');
+ done();
+ });
+ });
+
+ test('saveDiffPreferences set show_tabs to false', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element.saveDiffPreferences({show_tabs: false});
+ assert.isTrue(sendStub.called);
+ assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
+ });
+
+ test('getEditPreferences returns correct defaults', done => {
+ sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false));
+
+ element.getEditPreferences().then(obj => {
+ assert.equal(obj.auto_close_brackets, false);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.hide_line_numbers, false);
+ assert.equal(obj.hide_top_menu, false);
+ assert.equal(obj.indent_unit, 2);
+ assert.equal(obj.indent_with_tabs, false);
+ assert.equal(obj.key_map_type, 'DEFAULT');
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.match_brackets, true);
+ assert.equal(obj.show_base, false);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ assert.equal(obj.theme, 'DEFAULT');
+ done();
+ });
+ });
+
+ test('saveEditPreferences set show_tabs to false', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send');
+ element.saveEditPreferences({show_tabs: false});
+ assert.isTrue(sendStub.called);
+ assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
+ });
+
+ test('confirmEmail', () => {
+ const sendStub = sandbox.spy(element._restApiHelper, 'send');
+ element.confirmEmail('foo');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, 'PUT');
+ assert.equal(sendStub.lastCall.args[0].url,
+ '/config/server/email.confirm');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
+ });
+
+ test('setAccountStatus', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send')
+ .returns(Promise.resolve('OOO'));
+ element._cache.set('/accounts/self/detail', {});
+ return element.setAccountStatus('OOO').then(() => {
assert.isTrue(sendStub.calledOnce);
assert.equal(sendStub.lastCall.args[0].method, 'PUT');
assert.equal(sendStub.lastCall.args[0].url,
- '/config/server/email.confirm');
- assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
- });
-
- test('setAccountStatus', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send')
- .returns(Promise.resolve('OOO'));
- element._cache.set('/accounts/self/detail', {});
- return element.setAccountStatus('OOO').then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'PUT');
- assert.equal(sendStub.lastCall.args[0].url,
- '/accounts/self/status');
- assert.deepEqual(sendStub.lastCall.args[0].body,
- {status: 'OOO'});
- assert.deepEqual(element._restApiHelper
- ._cache.get('/accounts/self/detail'),
- {status: 'OOO'});
- });
- });
-
- suite('draft comments', () => {
- test('_sendDiffDraftRequest pending requests tracked', () => {
- const obj = element._pendingRequests;
- sandbox.stub(element, '_getChangeURLAndSend', () => mockPromise());
- assert.notOk(element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 1);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 2);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- for (const promise of obj.sendDiffDraft) { promise.resolve(); }
-
- return element.awaitPendingDiffDrafts().then(() => {
- assert.equal(obj.sendDiffDraft.length, 0);
- assert.isFalse(!!element.hasPendingDiffDrafts());
- });
- });
-
- suite('_failForCreate200', () => {
- test('_sendDiffDraftRequest checks for 200 on create', () => {
- const sendPromise = Promise.resolve();
- sandbox.stub(element, '_getChangeURLAndSend').returns(sendPromise);
- const failStub = sandbox.stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
- assert.isTrue(failStub.calledOnce);
- assert.isTrue(failStub.calledWithExactly(sendPromise));
- });
- });
-
- test('_sendDiffDraftRequest no checks for 200 on non create', () => {
- sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve());
- const failStub = sandbox.stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
- .then(() => {
- assert.isFalse(failStub.called);
- });
- });
-
- test('_failForCreate200 fails on 200', done => {
- const result = {
- ok: true,
- status: 200,
- headers: {entries: () => [
- ['Set-CoOkiE', 'secret'],
- ['Innocuous', 'hello'],
- ]},
- };
- element._failForCreate200(Promise.resolve(result))
- .then(() => {
- assert.isTrue(false, 'Promise should not resolve');
- })
- .catch(e => {
- assert.isOk(e);
- assert.include(e.message, 'Saving draft resulted in HTTP 200');
- assert.include(e.message, 'hello');
- assert.notInclude(e.message, 'secret');
- done();
- });
- });
-
- test('_failForCreate200 does not fail on 201', done => {
- const result = {
- ok: true,
- status: 201,
- headers: {entries: () => []},
- };
- element._failForCreate200(Promise.resolve(result))
- .then(() => {
- done();
- })
- .catch(e => {
- assert.isTrue(false, 'Promise should not fail');
- });
- });
- });
- });
-
- test('saveChangeEdit', () => {
- element._projectLookup = {1: 'test'};
- const change_num = '1';
- const file_name = 'index.php';
- const file_contents = '<?php';
- sandbox.stub(element._restApiHelper, 'send').returns(
- Promise.resolve([change_num, file_name, file_contents]));
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, file_name, file_contents]));
- element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
- return element.saveChangeEdit(change_num, file_name, file_contents)
- .then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(element._restApiHelper.send.lastCall.args[0].method,
- 'PUT');
- assert.equal(element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/edit/' + file_name);
- assert.equal(element._restApiHelper.send.lastCall.args[0].body,
- file_contents);
- });
- });
-
- test('putChangeCommitMessage', () => {
- element._projectLookup = {1: 'test'};
- const change_num = '1';
- const message = 'this is a commit message';
- sandbox.stub(element._restApiHelper, 'send').returns(
- Promise.resolve([change_num, message]));
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, message]));
- element._cache.set('/changes/' + change_num + '/message', {});
- return element.putChangeCommitMessage(change_num, message).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
- assert.equal(element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/message');
- assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body,
- {message});
- });
- });
-
- test('deleteChangeCommitMessage', () => {
- element._projectLookup = {1: 'test'};
- const change_num = '1';
- const messageId = 'abc';
- sandbox.stub(element._restApiHelper, 'send').returns(
- Promise.resolve([change_num, messageId]));
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, messageId]));
- return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].method,
- 'DELETE'
- );
- assert.equal(element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/messages/abc');
- });
- });
-
- test('startWorkInProgress', () => {
- const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('ok'));
- element.startWorkInProgress('42');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {});
-
- element.startWorkInProgress('42', 'revising...');
- assert.isTrue(sendStub.calledTwice);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ '/accounts/self/status');
assert.deepEqual(sendStub.lastCall.args[0].body,
- {message: 'revising...'});
+ {status: 'OOO'});
+ assert.deepEqual(element._restApiHelper
+ ._cache.get('/accounts/self/detail'),
+ {status: 'OOO'});
});
+ });
- test('startReview', () => {
- const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve({}));
- element.startReview('42', {message: 'Please review.'});
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/ready');
- assert.deepEqual(sendStub.lastCall.args[0].body,
- {message: 'Please review.'});
- });
+ suite('draft comments', () => {
+ test('_sendDiffDraftRequest pending requests tracked', () => {
+ const obj = element._pendingRequests;
+ sandbox.stub(element, '_getChangeURLAndSend', () => mockPromise());
+ assert.notOk(element.hasPendingDiffDrafts());
- test('deleteComment', () => {
- const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('some response'));
- return element.deleteComment('foo', 'bar', '01234', 'removal reason')
- .then(response => {
- assert.equal(response, 'some response');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, 'foo');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(sendStub.lastCall.args[0].patchNum, 'bar');
- assert.equal(sendStub.lastCall.args[0].endpoint,
- '/comments/01234/delete');
- assert.deepEqual(sendStub.lastCall.args[0].body,
- {reason: 'removal reason'});
- });
- });
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 1);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
- test('createRepo encodes name', () => {
- const sendStub = sandbox.stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
- return element.createRepo({name: 'x/y'}).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 2);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
+
+ for (const promise of obj.sendDiffDraft) { promise.resolve(); }
+
+ return element.awaitPendingDiffDrafts().then(() => {
+ assert.equal(obj.sendDiffDraft.length, 0);
+ assert.isFalse(!!element.hasPendingDiffDrafts());
});
});
- test('queryChangeFiles', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
- assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
- assert.equal(fetchStub.lastCall.args[0].endpoint,
- '/files?q=test%2Fpath.js');
- assert.equal(fetchStub.lastCall.args[0].patchNum, 'edit');
- });
- });
-
- test('normal use', () => {
- const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
-
- assert.equal(element._getReposUrl('test', 25),
- '/projects/?n=26&S=0&query=test');
-
- assert.equal(element._getReposUrl(null, 25),
- `/projects/?n=26&S=0&query=${defaultQuery}`);
-
- assert.equal(element._getReposUrl('test', 25, 25),
- '/projects/?n=26&S=25&query=test');
- });
-
- test('invalidateReposCache', () => {
- const url = '/projects/?n=26&S=0&query=test';
-
- element._cache.set(url, {});
-
- element.invalidateReposCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- test('invalidateAccountsCache', () => {
- const url = '/accounts/self/detail';
-
- element._cache.set(url, {});
-
- element.invalidateAccountsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getRepos', () => {
- const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub =
- sandbox.stub(element._restApiHelper, 'fetchCacheURL');
- });
-
- test('normal use', () => {
- element.getRepos('test', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=test');
-
- element.getRepos(null, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&query=${defaultQuery}`);
-
- element.getRepos('test', 25, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=25&query=test');
- });
-
- test('with blank', () => {
- element.getRepos('test/test', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
- });
-
- test('with hyphen', () => {
- element.getRepos('foo-bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('with leading hyphen', () => {
- element.getRepos('-bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Abar');
- });
-
- test('with trailing hyphen', () => {
- element.getRepos('foo-bar-', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
- });
-
- test('hyphen only', () => {
- element.getRepos('-', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&query=${defaultQuery}`);
- });
- });
-
- test('_getGroupsUrl normal use', () => {
- assert.equal(element._getGroupsUrl('test', 25),
- '/groups/?n=26&S=0&m=test');
-
- assert.equal(element._getGroupsUrl(null, 25),
- '/groups/?n=26&S=0');
-
- assert.equal(element._getGroupsUrl('test', 25, 25),
- '/groups/?n=26&S=25&m=test');
- });
-
- test('invalidateGroupsCache', () => {
- const url = '/groups/?n=26&S=0&m=test';
-
- element._cache.set(url, {});
-
- element.invalidateGroupsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getGroups', () => {
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub =
- sandbox.stub(element._restApiHelper, 'fetchCacheURL');
- });
-
- test('normal use', () => {
- element.getGroups('test', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&m=test');
-
- element.getGroups(null, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0');
-
- element.getGroups('test', 25, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&m=test');
- });
-
- test('regex', () => {
- element.getGroups('^test.*', 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&r=%5Etest.*');
-
- element.getGroups('^test.*', 25, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&r=%5Etest.*');
- });
- });
-
- test('gerrit auth is used', () => {
- sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve());
- element._restApiHelper.fetchJSON({url: 'foo'});
- assert(Gerrit.Auth.fetch.called);
- });
-
- test('getSuggestedAccounts does not return _fetchJSON', () => {
- const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON');
- return element.getSuggestedAccounts().then(accts => {
- assert.isFalse(_fetchJSONSpy.called);
- assert.equal(accts.length, 0);
- });
- });
-
- test('_fetchJSON gets called by getSuggestedAccounts', () => {
- const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON',
- () => Promise.resolve());
- return element.getSuggestedAccounts('own').then(() => {
- assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
- q: 'own',
- suggest: null,
- });
- });
- });
-
- suite('getChangeDetail', () => {
- suite('change detail options', () => {
- let toHexStub;
-
- setup(() => {
- toHexStub = sandbox.stub(element, 'listChangesOptionsToHex',
- options => 'deadbeef');
- sandbox.stub(element, '_getChangeDetail',
- async (changeNum, options) => { return {changeNum, options}; });
- });
-
- test('signed pushes disabled', async () => {
- const {PUSH_CERTIFICATES} = element.ListChangesOption;
- sandbox.stub(element, 'getConfig', async () => { return {}; });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.strictEqual('deadbeef', options);
- assert.isTrue(toHexStub.calledOnce);
- assert.isFalse(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
- });
-
- test('signed pushes enabled', async () => {
- const {PUSH_CERTIFICATES} = element.ListChangesOption;
- sandbox.stub(element, 'getConfig', async () => {
- return {receive: {enable_signed_push: true}};
- });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.strictEqual('deadbeef', options);
- assert.isTrue(toHexStub.calledOnce);
- assert.isTrue(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+ suite('_failForCreate200', () => {
+ test('_sendDiffDraftRequest checks for 200 on create', () => {
+ const sendPromise = Promise.resolve();
+ sandbox.stub(element, '_getChangeURLAndSend').returns(sendPromise);
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
+ assert.isTrue(failStub.calledOnce);
+ assert.isTrue(failStub.calledWithExactly(sendPromise));
});
});
- test('GrReviewerUpdatesParser.parse is used', () => {
- sandbox.stub(GrReviewerUpdatesParser, 'parse').returns(
- Promise.resolve('foo'));
- return element.getChangeDetail(42).then(result => {
- assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
- assert.equal(result, 'foo');
- });
- });
-
- test('_getChangeDetail passes params to ETags decorator', () => {
- const changeNum = 4321;
- element._projectLookup[changeNum] = 'test';
- const expectedUrl =
- window.CANONICAL_PATH + '/changes/test~4321/detail?'+
- '0=5&1=1&2=6&3=7&4=1&5=4';
- sandbox.stub(element._etags, 'getOptions');
- sandbox.stub(element._etags, 'collect');
- return element._getChangeDetail(changeNum, '516714').then(() => {
- assert.isTrue(element._etags.getOptions.calledWithExactly(
- expectedUrl));
- assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
- });
- });
-
- test('_getChangeDetail calls errFn on 500', () => {
- const errFn = sinon.stub();
- sandbox.stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(''));
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({ok: false, status: 500}));
- return element._getChangeDetail(123, '516714', errFn).then(() => {
- assert.isTrue(errFn.called);
- });
- });
-
- test('_getChangeDetail populates _projectLookup', () => {
- sandbox.stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(''));
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({ok: true}));
-
- const mockResponse = {_number: 1, project: 'test'};
- sandbox.stub(element._restApiHelper, 'readResponsePayload')
- .returns(Promise.resolve({
- parsed: mockResponse,
- raw: JSON.stringify(mockResponse),
- }));
- return element._getChangeDetail(1, '516714').then(() => {
- assert.equal(Object.keys(element._projectLookup).length, 1);
- assert.equal(element._projectLookup[1], 'test');
- });
- });
-
- suite('_getChangeDetail ETag cache', () => {
- let requestUrl;
- let mockResponseSerial;
- let collectSpy;
- let getPayloadSpy;
-
- setup(() => {
- requestUrl = '/foo/bar';
- const mockResponse = {foo: 'bar', baz: 42};
- mockResponseSerial = element.JSON_PREFIX +
- JSON.stringify(mockResponse);
- sandbox.stub(element._restApiHelper, 'urlWithParams')
- .returns(requestUrl);
- sandbox.stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(requestUrl));
- collectSpy = sandbox.spy(element._etags, 'collect');
- getPayloadSpy = sandbox.spy(element._etags, 'getCachedPayload');
- });
-
- test('contributes to cache', () => {
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({
- text: () => Promise.resolve(mockResponseSerial),
- status: 200,
- ok: true,
- }));
-
- return element._getChangeDetail(123, '516714').then(detail => {
- assert.isFalse(getPayloadSpy.called);
- assert.isTrue(collectSpy.calledOnce);
- const cachedResponse = element._etags.getCachedPayload(requestUrl);
- assert.equal(cachedResponse, mockResponseSerial);
- });
- });
-
- test('uses cache on HTTP 304', () => {
- sandbox.stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({
- text: () => Promise.resolve(mockResponseSerial),
- status: 304,
- ok: true,
- }));
-
- return element._getChangeDetail(123, {}).then(detail => {
- assert.isFalse(collectSpy.called);
- assert.isTrue(getPayloadSpy.calledOnce);
- });
- });
- });
- });
-
- test('setInProjectLookup', () => {
- element.setInProjectLookup('test', 'project');
- assert.deepEqual(element._projectLookup, {test: 'project'});
- });
-
- suite('getFromProjectLookup', () => {
- test('getChange fails', () => {
- sandbox.stub(element, 'getChange')
- .returns(Promise.resolve(null));
- return element.getFromProjectLookup().then(val => {
- assert.strictEqual(val, undefined);
- assert.deepEqual(element._projectLookup, {});
- });
- });
-
- test('getChange succeeds, no project', () => {
- sandbox.stub(element, 'getChange').returns(Promise.resolve(null));
- return element.getFromProjectLookup().then(val => {
- assert.strictEqual(val, undefined);
- assert.deepEqual(element._projectLookup, {});
- });
- });
-
- test('getChange succeeds with project', () => {
- sandbox.stub(element, 'getChange')
- .returns(Promise.resolve({project: 'project'}));
- return element.getFromProjectLookup('test').then(val => {
- assert.equal(val, 'project');
- assert.deepEqual(element._projectLookup, {test: 'project'});
- });
- });
- });
-
- suite('getChanges populates _projectLookup', () => {
- test('multiple queries', () => {
- sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([
- [
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- ], [
- {_number: 3, project: 'test/test'},
- ],
- ]));
- // When opt_query instanceof Array, _fetchJSON returns
- // Array<Array<Object>>.
- return element.getChanges(null, []).then(() => {
- assert.equal(Object.keys(element._projectLookup).length, 3);
- assert.equal(element._projectLookup[1], 'test');
- assert.equal(element._projectLookup[2], 'test');
- assert.equal(element._projectLookup[3], 'test/test');
- });
- });
-
- test('no query', () => {
- sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- {_number: 3, project: 'test/test'},
- ]));
-
- // When opt_query !instanceof Array, _fetchJSON returns
- // Array<Object>.
- return element.getChanges().then(() => {
- assert.equal(Object.keys(element._projectLookup).length, 3);
- assert.equal(element._projectLookup[1], 'test');
- assert.equal(element._projectLookup[2], 'test');
- assert.equal(element._projectLookup[3], 'test/test');
- });
- });
- });
-
- test('_getChangeURLAndFetch', () => {
- element._projectLookup = {1: 'test'};
- const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve());
- const req = {changeNum: 1, endpoint: '/test', patchNum: 1};
- return element._getChangeURLAndFetch(req).then(() => {
- assert.equal(fetchStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test');
- });
- });
-
- test('_getChangeURLAndSend', () => {
- element._projectLookup = {1: 'test'};
- const sendStub = sandbox.stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
-
- const req = {
- changeNum: 1,
- method: 'POST',
- patchNum: 1,
- endpoint: '/test',
- };
- return element._getChangeURLAndSend(req).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(sendStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test');
- });
- });
-
- suite('reading responses', () => {
- test('_readResponsePayload', () => {
- const mockObject = {foo: 'bar', baz: 'foo'};
- const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
- const mockResponse = {text: () => Promise.resolve(serial)};
- return element._restApiHelper.readResponsePayload(mockResponse)
- .then(payload => {
- assert.deepEqual(payload.parsed, mockObject);
- assert.equal(payload.raw, serial);
+ test('_sendDiffDraftRequest no checks for 200 on non create', () => {
+ sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve());
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
+ .then(() => {
+ assert.isFalse(failStub.called);
});
});
- test('_parsePrefixedJSON', () => {
- const obj = {x: 3, y: {z: 4}, w: 23};
- const serial = element.JSON_PREFIX + JSON.stringify(obj);
- const result = element._restApiHelper.parsePrefixedJSON(serial);
- assert.deepEqual(result, obj);
- });
- });
-
- test('setChangeTopic', () => {
- const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
- return element.setChangeTopic(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
- });
- });
-
- test('setChangeHashtag', () => {
- const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
- return element.setChangeHashtag(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
- });
- });
-
- test('generateAccountHttpPassword', () => {
- const sendSpy = sandbox.spy(element._restApiHelper, 'send');
- return element.generateAccountHttpPassword().then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
- });
- });
-
- suite('getChangeFiles', () => {
- test('patch only', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: 'PARENT', patchNum: 2};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
- assert.isNotOk(fetchStub.lastCall.args[0].params);
- });
+ test('_failForCreate200 fails on 200', done => {
+ const result = {
+ ok: true,
+ status: 200,
+ headers: {entries: () => [
+ ['Set-CoOkiE', 'secret'],
+ ['Innocuous', 'hello'],
+ ]},
+ };
+ element._failForCreate200(Promise.resolve(result))
+ .then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ })
+ .catch(e => {
+ assert.isOk(e);
+ assert.include(e.message, 'Saving draft resulted in HTTP 200');
+ assert.include(e.message, 'hello');
+ assert.notInclude(e.message, 'secret');
+ done();
+ });
});
- test('simple range', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: 4, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: -3, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- suite('getDiff', () => {
- test('patchOnly', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- });
- });
-
- test('simple range', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- test('getDashboard', () => {
- const fetchCacheURLStub = sandbox.stub(element._restApiHelper,
- 'fetchCacheURL');
- element.getDashboard('gerrit/project', 'default:main');
- assert.isTrue(fetchCacheURLStub.calledOnce);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/gerrit%2Fproject/dashboards/default%3Amain');
- });
-
- test('getFileContent', () => {
- sandbox.stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve({
- ok: 'true',
- headers: {
- get(header) {
- if (header === 'X-FYI-Content-Type') {
- return 'text/java';
- }
- },
- },
- }));
-
- sandbox.stub(element, 'getResponseObject')
- .returns(Promise.resolve('new content'));
-
- const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
- assert.deepEqual(res,
- {content: 'new content', type: 'text/java', ok: true});
- });
-
- const normal = element.getFileContent('1', 'tst/path', '3').then(res => {
- assert.deepEqual(res,
- {content: 'new content', type: 'text/java', ok: true});
- });
-
- return Promise.all([edit, normal]);
- });
-
- test('getFileContent suppresses 404s', done => {
- const res = {status: 404};
- const handler = e => {
- assert.isFalse(e.detail.res.status === 404);
- done();
- };
- element.addEventListener('server-error', handler);
- sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve(res));
- sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
- element.getFileContent('1', 'tst/path', '1').then(() => {
- flushAsynchronousOperations();
-
- res.status = 500;
- element.getFileContent('1', 'tst/path', '1');
- });
- });
-
- test('getChangeFilesOrEditFiles is edit-sensitive', () => {
- const fn = element.getChangeOrEditFiles.bind(element);
- const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles')
- .returns(Promise.resolve({}));
- const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles')
- .returns(Promise.resolve({}));
-
- return fn('1', {patchNum: 'edit'}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isFalse(getChangeFilesStub.called);
- return fn('1', {patchNum: '1'}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isTrue(getChangeFilesStub.calledOnce);
- });
- });
- });
-
- test('_fetch forwards request and logs', () => {
- const logStub = sandbox.stub(element._restApiHelper, '_logCall');
- const response = {status: 404, text: sinon.stub()};
- const url = 'my url';
- const fetchOptions = {method: 'DELETE'};
- sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
- const startTime = 123;
- sandbox.stub(Date, 'now').returns(startTime);
- const req = {url, fetchOptions};
- return element._restApiHelper.fetch(req).then(() => {
- assert.isTrue(logStub.calledOnce);
- assert.isTrue(logStub.calledWith(req, startTime, response.status));
- assert.isFalse(response.text.called);
- });
- });
-
- test('_logCall only reports requests with anonymized URLss', () => {
- sandbox.stub(Date, 'now').returns(200);
- const handler = sinon.stub();
- element.addEventListener('rpc-log', handler);
-
- element._restApiHelper._logCall({url: 'url'}, 100, 200);
- assert.isFalse(handler.called);
-
- element._restApiHelper
- ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
- flushAsynchronousOperations();
- assert.isTrue(handler.calledOnce);
- });
-
- test('saveChangeStarred', async () => {
- sandbox.stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- const sendStub =
- sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve());
-
- await element.saveChangeStarred(123, true);
- assert.isTrue(sendStub.calledOnce);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'PUT',
- url: '/accounts/self/starred.changes/test~123',
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
-
- await element.saveChangeStarred(456, false);
- assert.isTrue(sendStub.calledTwice);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'DELETE',
- url: '/accounts/self/starred.changes/test~456',
- anonymizedUrl: '/accounts/self/starred.changes/*',
+ test('_failForCreate200 does not fail on 201', done => {
+ const result = {
+ ok: true,
+ status: 201,
+ headers: {entries: () => []},
+ };
+ element._failForCreate200(Promise.resolve(result))
+ .then(() => {
+ done();
+ })
+ .catch(e => {
+ assert.isTrue(false, 'Promise should not fail');
+ });
});
});
});
+
+ test('saveChangeEdit', () => {
+ element._projectLookup = {1: 'test'};
+ const change_num = '1';
+ const file_name = 'index.php';
+ const file_contents = '<?php';
+ sandbox.stub(element._restApiHelper, 'send').returns(
+ Promise.resolve([change_num, file_name, file_contents]));
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve([change_num, file_name, file_contents]));
+ element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
+ return element.saveChangeEdit(change_num, file_name, file_contents)
+ .then(() => {
+ assert.isTrue(element._restApiHelper.send.calledOnce);
+ assert.equal(element._restApiHelper.send.lastCall.args[0].method,
+ 'PUT');
+ assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+ '/changes/test~1/edit/' + file_name);
+ assert.equal(element._restApiHelper.send.lastCall.args[0].body,
+ file_contents);
+ });
+ });
+
+ test('putChangeCommitMessage', () => {
+ element._projectLookup = {1: 'test'};
+ const change_num = '1';
+ const message = 'this is a commit message';
+ sandbox.stub(element._restApiHelper, 'send').returns(
+ Promise.resolve([change_num, message]));
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve([change_num, message]));
+ element._cache.set('/changes/' + change_num + '/message', {});
+ return element.putChangeCommitMessage(change_num, message).then(() => {
+ assert.isTrue(element._restApiHelper.send.calledOnce);
+ assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
+ assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+ '/changes/test~1/message');
+ assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body,
+ {message});
+ });
+ });
+
+ test('deleteChangeCommitMessage', () => {
+ element._projectLookup = {1: 'test'};
+ const change_num = '1';
+ const messageId = 'abc';
+ sandbox.stub(element._restApiHelper, 'send').returns(
+ Promise.resolve([change_num, messageId]));
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve([change_num, messageId]));
+ return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
+ assert.isTrue(element._restApiHelper.send.calledOnce);
+ assert.equal(
+ element._restApiHelper.send.lastCall.args[0].method,
+ 'DELETE'
+ );
+ assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+ '/changes/test~1/messages/abc');
+ });
+ });
+
+ test('startWorkInProgress', () => {
+ const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve('ok'));
+ element.startWorkInProgress('42');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, '42');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {});
+
+ element.startWorkInProgress('42', 'revising...');
+ assert.isTrue(sendStub.calledTwice);
+ assert.equal(sendStub.lastCall.args[0].changeNum, '42');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body,
+ {message: 'revising...'});
+ });
+
+ test('startReview', () => {
+ const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve({}));
+ element.startReview('42', {message: 'Please review.'});
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, '42');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/ready');
+ assert.deepEqual(sendStub.lastCall.args[0].body,
+ {message: 'Please review.'});
+ });
+
+ test('deleteComment', () => {
+ const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve('some response'));
+ return element.deleteComment('foo', 'bar', '01234', 'removal reason')
+ .then(response => {
+ assert.equal(response, 'some response');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 'foo');
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.equal(sendStub.lastCall.args[0].patchNum, 'bar');
+ assert.equal(sendStub.lastCall.args[0].endpoint,
+ '/comments/01234/delete');
+ assert.deepEqual(sendStub.lastCall.args[0].body,
+ {reason: 'removal reason'});
+ });
+ });
+
+ test('createRepo encodes name', () => {
+ const sendStub = sandbox.stub(element._restApiHelper, 'send')
+ .returns(Promise.resolve());
+ return element.createRepo({name: 'x/y'}).then(() => {
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+ });
+ });
+
+ test('queryChangeFiles', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
+ assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
+ assert.equal(fetchStub.lastCall.args[0].endpoint,
+ '/files?q=test%2Fpath.js');
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 'edit');
+ });
+ });
+
+ test('normal use', () => {
+ const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
+
+ assert.equal(element._getReposUrl('test', 25),
+ '/projects/?n=26&S=0&query=test');
+
+ assert.equal(element._getReposUrl(null, 25),
+ `/projects/?n=26&S=0&query=${defaultQuery}`);
+
+ assert.equal(element._getReposUrl('test', 25, 25),
+ '/projects/?n=26&S=25&query=test');
+ });
+
+ test('invalidateReposCache', () => {
+ const url = '/projects/?n=26&S=0&query=test';
+
+ element._cache.set(url, {});
+
+ element.invalidateReposCache();
+
+ assert.isUndefined(element._sharedFetchPromises[url]);
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ test('invalidateAccountsCache', () => {
+ const url = '/accounts/self/detail';
+
+ element._cache.set(url, {});
+
+ element.invalidateAccountsCache();
+
+ assert.isUndefined(element._sharedFetchPromises[url]);
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getRepos', () => {
+ const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
+ let fetchCacheURLStub;
+ setup(() => {
+ fetchCacheURLStub =
+ sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+ });
+
+ test('normal use', () => {
+ element.getRepos('test', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=test');
+
+ element.getRepos(null, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ `/projects/?n=26&S=0&query=${defaultQuery}`);
+
+ element.getRepos('test', 25, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=25&query=test');
+ });
+
+ test('with blank', () => {
+ element.getRepos('test/test', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
+ });
+
+ test('with hyphen', () => {
+ element.getRepos('foo-bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('with leading hyphen', () => {
+ element.getRepos('-bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Abar');
+ });
+
+ test('with trailing hyphen', () => {
+ element.getRepos('foo-bar-', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
+ });
+
+ test('hyphen only', () => {
+ element.getRepos('-', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ `/projects/?n=26&S=0&query=${defaultQuery}`);
+ });
+ });
+
+ test('_getGroupsUrl normal use', () => {
+ assert.equal(element._getGroupsUrl('test', 25),
+ '/groups/?n=26&S=0&m=test');
+
+ assert.equal(element._getGroupsUrl(null, 25),
+ '/groups/?n=26&S=0');
+
+ assert.equal(element._getGroupsUrl('test', 25, 25),
+ '/groups/?n=26&S=25&m=test');
+ });
+
+ test('invalidateGroupsCache', () => {
+ const url = '/groups/?n=26&S=0&m=test';
+
+ element._cache.set(url, {});
+
+ element.invalidateGroupsCache();
+
+ assert.isUndefined(element._sharedFetchPromises[url]);
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getGroups', () => {
+ let fetchCacheURLStub;
+ setup(() => {
+ fetchCacheURLStub =
+ sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+ });
+
+ test('normal use', () => {
+ element.getGroups('test', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&m=test');
+
+ element.getGroups(null, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0');
+
+ element.getGroups('test', 25, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&m=test');
+ });
+
+ test('regex', () => {
+ element.getGroups('^test.*', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&r=%5Etest.*');
+
+ element.getGroups('^test.*', 25, 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&r=%5Etest.*');
+ });
+ });
+
+ test('gerrit auth is used', () => {
+ sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve());
+ element._restApiHelper.fetchJSON({url: 'foo'});
+ assert(Gerrit.Auth.fetch.called);
+ });
+
+ test('getSuggestedAccounts does not return _fetchJSON', () => {
+ const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON');
+ return element.getSuggestedAccounts().then(accts => {
+ assert.isFalse(_fetchJSONSpy.called);
+ assert.equal(accts.length, 0);
+ });
+ });
+
+ test('_fetchJSON gets called by getSuggestedAccounts', () => {
+ const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON',
+ () => Promise.resolve());
+ return element.getSuggestedAccounts('own').then(() => {
+ assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
+ q: 'own',
+ suggest: null,
+ });
+ });
+ });
+
+ suite('getChangeDetail', () => {
+ suite('change detail options', () => {
+ let toHexStub;
+
+ setup(() => {
+ toHexStub = sandbox.stub(element, 'listChangesOptionsToHex',
+ options => 'deadbeef');
+ sandbox.stub(element, '_getChangeDetail',
+ async (changeNum, options) => { return {changeNum, options}; });
+ });
+
+ test('signed pushes disabled', async () => {
+ const {PUSH_CERTIFICATES} = element.ListChangesOption;
+ sandbox.stub(element, 'getConfig', async () => { return {}; });
+ const {changeNum, options} = await element.getChangeDetail(123);
+ assert.strictEqual(123, changeNum);
+ assert.strictEqual('deadbeef', options);
+ assert.isTrue(toHexStub.calledOnce);
+ assert.isFalse(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+ });
+
+ test('signed pushes enabled', async () => {
+ const {PUSH_CERTIFICATES} = element.ListChangesOption;
+ sandbox.stub(element, 'getConfig', async () => {
+ return {receive: {enable_signed_push: true}};
+ });
+ const {changeNum, options} = await element.getChangeDetail(123);
+ assert.strictEqual(123, changeNum);
+ assert.strictEqual('deadbeef', options);
+ assert.isTrue(toHexStub.calledOnce);
+ assert.isTrue(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+ });
+ });
+
+ test('GrReviewerUpdatesParser.parse is used', () => {
+ sandbox.stub(GrReviewerUpdatesParser, 'parse').returns(
+ Promise.resolve('foo'));
+ return element.getChangeDetail(42).then(result => {
+ assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
+ assert.equal(result, 'foo');
+ });
+ });
+
+ test('_getChangeDetail passes params to ETags decorator', () => {
+ const changeNum = 4321;
+ element._projectLookup[changeNum] = 'test';
+ const expectedUrl =
+ window.CANONICAL_PATH + '/changes/test~4321/detail?'+
+ '0=5&1=1&2=6&3=7&4=1&5=4';
+ sandbox.stub(element._etags, 'getOptions');
+ sandbox.stub(element._etags, 'collect');
+ return element._getChangeDetail(changeNum, '516714').then(() => {
+ assert.isTrue(element._etags.getOptions.calledWithExactly(
+ expectedUrl));
+ assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
+ });
+ });
+
+ test('_getChangeDetail calls errFn on 500', () => {
+ const errFn = sinon.stub();
+ sandbox.stub(element, 'getChangeActionURL')
+ .returns(Promise.resolve(''));
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({ok: false, status: 500}));
+ return element._getChangeDetail(123, '516714', errFn).then(() => {
+ assert.isTrue(errFn.called);
+ });
+ });
+
+ test('_getChangeDetail populates _projectLookup', () => {
+ sandbox.stub(element, 'getChangeActionURL')
+ .returns(Promise.resolve(''));
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({ok: true}));
+
+ const mockResponse = {_number: 1, project: 'test'};
+ sandbox.stub(element._restApiHelper, 'readResponsePayload')
+ .returns(Promise.resolve({
+ parsed: mockResponse,
+ raw: JSON.stringify(mockResponse),
+ }));
+ return element._getChangeDetail(1, '516714').then(() => {
+ assert.equal(Object.keys(element._projectLookup).length, 1);
+ assert.equal(element._projectLookup[1], 'test');
+ });
+ });
+
+ suite('_getChangeDetail ETag cache', () => {
+ let requestUrl;
+ let mockResponseSerial;
+ let collectSpy;
+ let getPayloadSpy;
+
+ setup(() => {
+ requestUrl = '/foo/bar';
+ const mockResponse = {foo: 'bar', baz: 42};
+ mockResponseSerial = element.JSON_PREFIX +
+ JSON.stringify(mockResponse);
+ sandbox.stub(element._restApiHelper, 'urlWithParams')
+ .returns(requestUrl);
+ sandbox.stub(element, 'getChangeActionURL')
+ .returns(Promise.resolve(requestUrl));
+ collectSpy = sandbox.spy(element._etags, 'collect');
+ getPayloadSpy = sandbox.spy(element._etags, 'getCachedPayload');
+ });
+
+ test('contributes to cache', () => {
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({
+ text: () => Promise.resolve(mockResponseSerial),
+ status: 200,
+ ok: true,
+ }));
+
+ return element._getChangeDetail(123, '516714').then(detail => {
+ assert.isFalse(getPayloadSpy.called);
+ assert.isTrue(collectSpy.calledOnce);
+ const cachedResponse = element._etags.getCachedPayload(requestUrl);
+ assert.equal(cachedResponse, mockResponseSerial);
+ });
+ });
+
+ test('uses cache on HTTP 304', () => {
+ sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+ .returns(Promise.resolve({
+ text: () => Promise.resolve(mockResponseSerial),
+ status: 304,
+ ok: true,
+ }));
+
+ return element._getChangeDetail(123, {}).then(detail => {
+ assert.isFalse(collectSpy.called);
+ assert.isTrue(getPayloadSpy.calledOnce);
+ });
+ });
+ });
+ });
+
+ test('setInProjectLookup', () => {
+ element.setInProjectLookup('test', 'project');
+ assert.deepEqual(element._projectLookup, {test: 'project'});
+ });
+
+ suite('getFromProjectLookup', () => {
+ test('getChange fails', () => {
+ sandbox.stub(element, 'getChange')
+ .returns(Promise.resolve(null));
+ return element.getFromProjectLookup().then(val => {
+ assert.strictEqual(val, undefined);
+ assert.deepEqual(element._projectLookup, {});
+ });
+ });
+
+ test('getChange succeeds, no project', () => {
+ sandbox.stub(element, 'getChange').returns(Promise.resolve(null));
+ return element.getFromProjectLookup().then(val => {
+ assert.strictEqual(val, undefined);
+ assert.deepEqual(element._projectLookup, {});
+ });
+ });
+
+ test('getChange succeeds with project', () => {
+ sandbox.stub(element, 'getChange')
+ .returns(Promise.resolve({project: 'project'}));
+ return element.getFromProjectLookup('test').then(val => {
+ assert.equal(val, 'project');
+ assert.deepEqual(element._projectLookup, {test: 'project'});
+ });
+ });
+ });
+
+ suite('getChanges populates _projectLookup', () => {
+ test('multiple queries', () => {
+ sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve([
+ [
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ ], [
+ {_number: 3, project: 'test/test'},
+ ],
+ ]));
+ // When opt_query instanceof Array, _fetchJSON returns
+ // Array<Array<Object>>.
+ return element.getChanges(null, []).then(() => {
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ assert.equal(element._projectLookup[1], 'test');
+ assert.equal(element._projectLookup[2], 'test');
+ assert.equal(element._projectLookup[3], 'test/test');
+ });
+ });
+
+ test('no query', () => {
+ sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve([
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ {_number: 3, project: 'test/test'},
+ ]));
+
+ // When opt_query !instanceof Array, _fetchJSON returns
+ // Array<Object>.
+ return element.getChanges().then(() => {
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ assert.equal(element._projectLookup[1], 'test');
+ assert.equal(element._projectLookup[2], 'test');
+ assert.equal(element._projectLookup[3], 'test/test');
+ });
+ });
+ });
+
+ test('_getChangeURLAndFetch', () => {
+ element._projectLookup = {1: 'test'};
+ const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+ .returns(Promise.resolve());
+ const req = {changeNum: 1, endpoint: '/test', patchNum: 1};
+ return element._getChangeURLAndFetch(req).then(() => {
+ assert.equal(fetchStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test');
+ });
+ });
+
+ test('_getChangeURLAndSend', () => {
+ element._projectLookup = {1: 'test'};
+ const sendStub = sandbox.stub(element._restApiHelper, 'send')
+ .returns(Promise.resolve());
+
+ const req = {
+ changeNum: 1,
+ method: 'POST',
+ patchNum: 1,
+ endpoint: '/test',
+ };
+ return element._getChangeURLAndSend(req).then(() => {
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, 'POST');
+ assert.equal(sendStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test');
+ });
+ });
+
+ suite('reading responses', () => {
+ test('_readResponsePayload', () => {
+ const mockObject = {foo: 'bar', baz: 'foo'};
+ const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
+ const mockResponse = {text: () => Promise.resolve(serial)};
+ return element._restApiHelper.readResponsePayload(mockResponse)
+ .then(payload => {
+ assert.deepEqual(payload.parsed, mockObject);
+ assert.equal(payload.raw, serial);
+ });
+ });
+
+ test('_parsePrefixedJSON', () => {
+ const obj = {x: 3, y: {z: 4}, w: 23};
+ const serial = element.JSON_PREFIX + JSON.stringify(obj);
+ const result = element._restApiHelper.parsePrefixedJSON(serial);
+ assert.deepEqual(result, obj);
+ });
+ });
+
+ test('setChangeTopic', () => {
+ const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
+ return element.setChangeTopic(123, 'foo-bar').then(() => {
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
+ });
+ });
+
+ test('setChangeHashtag', () => {
+ const sendSpy = sandbox.spy(element, '_getChangeURLAndSend');
+ return element.setChangeHashtag(123, 'foo-bar').then(() => {
+ assert.isTrue(sendSpy.calledOnce);
+ assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
+ });
+ });
+
+ test('generateAccountHttpPassword', () => {
+ const sendSpy = sandbox.spy(element._restApiHelper, 'send');
+ return element.generateAccountHttpPassword().then(() => {
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
+ });
+ });
+
+ suite('getChangeFiles', () => {
+ test('patch only', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: 'PARENT', patchNum: 2};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
+ assert.isNotOk(fetchStub.lastCall.args[0].params);
+ });
+ });
+
+ test('simple range', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: 4, patchNum: 5};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.equal(fetchStub.lastCall.args[0].params.base, 4);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+ });
+ });
+
+ test('parent index', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ const range = {basePatchNum: -3, patchNum: 5};
+ return element.getChangeFiles(123, range).then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+ assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
+ });
+ });
+ });
+
+ suite('getDiff', () => {
+ test('patchOnly', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 2);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+ });
+ });
+
+ test('simple range', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
+ assert.equal(fetchStub.lastCall.args[0].params.base, 4);
+ });
+ });
+
+ test('parent index', () => {
+ const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
+ .returns(Promise.resolve());
+ return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].patchNum, 5);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params.base);
+ assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
+ });
+ });
+ });
+
+ test('getDashboard', () => {
+ const fetchCacheURLStub = sandbox.stub(element._restApiHelper,
+ 'fetchCacheURL');
+ element.getDashboard('gerrit/project', 'default:main');
+ assert.isTrue(fetchCacheURLStub.calledOnce);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/gerrit%2Fproject/dashboards/default%3Amain');
+ });
+
+ test('getFileContent', () => {
+ sandbox.stub(element, '_getChangeURLAndSend')
+ .returns(Promise.resolve({
+ ok: 'true',
+ headers: {
+ get(header) {
+ if (header === 'X-FYI-Content-Type') {
+ return 'text/java';
+ }
+ },
+ },
+ }));
+
+ sandbox.stub(element, 'getResponseObject')
+ .returns(Promise.resolve('new content'));
+
+ const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
+ assert.deepEqual(res,
+ {content: 'new content', type: 'text/java', ok: true});
+ });
+
+ const normal = element.getFileContent('1', 'tst/path', '3').then(res => {
+ assert.deepEqual(res,
+ {content: 'new content', type: 'text/java', ok: true});
+ });
+
+ return Promise.all([edit, normal]);
+ });
+
+ test('getFileContent suppresses 404s', done => {
+ const res = {status: 404};
+ const handler = e => {
+ assert.isFalse(e.detail.res.status === 404);
+ done();
+ };
+ element.addEventListener('server-error', handler);
+ sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve(res));
+ sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
+ element.getFileContent('1', 'tst/path', '1').then(() => {
+ flushAsynchronousOperations();
+
+ res.status = 500;
+ element.getFileContent('1', 'tst/path', '1');
+ });
+ });
+
+ test('getChangeFilesOrEditFiles is edit-sensitive', () => {
+ const fn = element.getChangeOrEditFiles.bind(element);
+ const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles')
+ .returns(Promise.resolve({}));
+ const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles')
+ .returns(Promise.resolve({}));
+
+ return fn('1', {patchNum: 'edit'}).then(() => {
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isFalse(getChangeFilesStub.called);
+ return fn('1', {patchNum: '1'}).then(() => {
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isTrue(getChangeFilesStub.calledOnce);
+ });
+ });
+ });
+
+ test('_fetch forwards request and logs', () => {
+ const logStub = sandbox.stub(element._restApiHelper, '_logCall');
+ const response = {status: 404, text: sinon.stub()};
+ const url = 'my url';
+ const fetchOptions = {method: 'DELETE'};
+ sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response));
+ const startTime = 123;
+ sandbox.stub(Date, 'now').returns(startTime);
+ const req = {url, fetchOptions};
+ return element._restApiHelper.fetch(req).then(() => {
+ assert.isTrue(logStub.calledOnce);
+ assert.isTrue(logStub.calledWith(req, startTime, response.status));
+ assert.isFalse(response.text.called);
+ });
+ });
+
+ test('_logCall only reports requests with anonymized URLss', () => {
+ sandbox.stub(Date, 'now').returns(200);
+ const handler = sinon.stub();
+ element.addEventListener('rpc-log', handler);
+
+ element._restApiHelper._logCall({url: 'url'}, 100, 200);
+ assert.isFalse(handler.called);
+
+ element._restApiHelper
+ ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
+ flushAsynchronousOperations();
+ assert.isTrue(handler.calledOnce);
+ });
+
+ test('saveChangeStarred', async () => {
+ sandbox.stub(element, 'getFromProjectLookup')
+ .returns(Promise.resolve('test'));
+ const sendStub =
+ sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve());
+
+ await element.saveChangeStarred(123, true);
+ assert.isTrue(sendStub.calledOnce);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: 'PUT',
+ url: '/accounts/self/starred.changes/test~123',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+
+ await element.saveChangeStarred(456, false);
+ assert.isTrue(sendStub.calledTwice);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: 'DELETE',
+ url: '/accounts/self/starred.changes/test~456',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
index 310063c..7f86953 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
@@ -19,158 +19,169 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rest-api-helper</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="../../../../scripts/util.js"></script>
-<script src="../gr-auth.js"></script>
-<script src="gr-rest-api-helper.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 type="module" src="../../../../scripts/util.js"></script>
+<script type="module" src="../gr-auth.js"></script>
+<script type="module" src="./gr-rest-api-helper.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../../test/test-pre-setup.js';
+import '../../../../test/common-test-setup.js';
+import '../../../../scripts/util.js';
+import '../gr-auth.js';
+import './gr-rest-api-helper.js';
+void(0);
+</script>
-<script>
- suite('gr-rest-api-helper tests', async () => {
- await readyToTest();
- let helper;
- let sandbox;
- let cache;
- let fetchPromisesCache;
+<script type="module">
+import '../../../../test/test-pre-setup.js';
+import '../../../../test/common-test-setup.js';
+import '../../../../scripts/util.js';
+import '../gr-auth.js';
+import './gr-rest-api-helper.js';
+suite('gr-rest-api-helper tests', () => {
+ let helper;
+ let sandbox;
+ let cache;
+ let fetchPromisesCache;
- setup(() => {
- sandbox = sinon.sandbox.create();
- cache = new SiteBasedCache();
- fetchPromisesCache = new FetchPromisesCache();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ cache = new SiteBasedCache();
+ fetchPromisesCache = new FetchPromisesCache();
- window.CANONICAL_PATH = 'testhelper';
+ window.CANONICAL_PATH = 'testhelper';
- const mockRestApiInterface = {
- getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
- fire: sinon.stub(),
- };
+ const mockRestApiInterface = {
+ getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
+ fire: sinon.stub(),
+ };
- const testJSON = ')]}\'\n{"hello": "bonjour"}';
- sandbox.stub(window, 'fetch').returns(Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(testJSON);
- },
- }));
+ const testJSON = ')]}\'\n{"hello": "bonjour"}';
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({
+ ok: true,
+ text() {
+ return Promise.resolve(testJSON);
+ },
+ }));
- helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
- mockRestApiInterface);
+ helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
+ mockRestApiInterface);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('fetchJSON()', () => {
+ test('Sets header to accept application/json', () => {
+ const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+ .returns(Promise.resolve());
+ helper.fetchJSON({url: '/dummy/url'});
+ assert.isTrue(authFetchStub.called);
+ assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+ 'application/json');
});
- teardown(() => {
- sandbox.restore();
- });
-
- suite('fetchJSON()', () => {
- test('Sets header to accept application/json', () => {
- const authFetchStub = sandbox.stub(helper._auth, 'fetch')
- .returns(Promise.resolve());
- helper.fetchJSON({url: '/dummy/url'});
- assert.isTrue(authFetchStub.called);
- assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
- 'application/json');
- });
-
- test('Use header option accept when provided', () => {
- const authFetchStub = sandbox.stub(helper._auth, 'fetch')
- .returns(Promise.resolve());
- const headers = new Headers();
- headers.append('Accept', '*/*');
- const fetchOptions = {headers};
- helper.fetchJSON({url: '/dummy/url', fetchOptions});
- assert.isTrue(authFetchStub.called);
- assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
- '*/*');
- });
- });
-
- test('JSON prefix is properly removed', done => {
- helper.fetchJSON({url: '/dummy/url'}).then(obj => {
- assert.deepEqual(obj, {hello: 'bonjour'});
- done();
- });
- });
-
- test('cached results', done => {
- let n = 0;
- sandbox.stub(helper, 'fetchJSON', () => Promise.resolve(++n));
- const promises = [];
- promises.push(helper.fetchCacheURL('/foo'));
- promises.push(helper.fetchCacheURL('/foo'));
- promises.push(helper.fetchCacheURL('/foo'));
-
- Promise.all(promises).then(results => {
- assert.deepEqual(results, [1, 1, 1]);
- helper.fetchCacheURL('/foo').then(foo => {
- assert.equal(foo, 1);
- done();
- });
- });
- });
-
- test('cached promise', done => {
- const promise = Promise.reject(new Error('foo'));
- cache.set('/foo', promise);
- helper.fetchCacheURL({url: '/foo'}).catch(p => {
- assert.equal(p.message, 'foo');
- done();
- });
- });
-
- test('cache invalidation', () => {
- cache.set('/foo/bar', 1);
- cache.set('/bar', 2);
- fetchPromisesCache.set('/foo/bar', 3);
- fetchPromisesCache.set('/bar', 4);
- helper.invalidateFetchPromisesPrefix('/foo/');
- assert.isFalse(cache.has('/foo/bar'));
- assert.isTrue(cache.has('/bar'));
- assert.isUndefined(fetchPromisesCache.get('/foo/bar'));
- assert.strictEqual(4, fetchPromisesCache.get('/bar'));
- });
-
- test('params are properly encoded', () => {
- let url = helper.urlWithParams('/path/', {
- sp: 'hola',
- gr: 'guten tag',
- noval: null,
- });
- assert.equal(url,
- window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
-
- url = helper.urlWithParams('/path/', {
- sp: 'hola',
- en: ['hey', 'hi'],
- });
- assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
-
- // Order must be maintained with array params.
- url = helper.urlWithParams('/path/', {
- l: ['c', 'b', 'a'],
- });
- assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
- });
-
- test('request callbacks can be canceled', done => {
- let cancelCalled = false;
- window.fetch.returns(Promise.resolve({
- body: {
- cancel() { cancelCalled = true; },
- },
- }));
- const cancelCondition = () => true;
- helper.fetchJSON({url: '/dummy/url', cancelCondition}).then(
- obj => {
- assert.isUndefined(obj);
- assert.isTrue(cancelCalled);
- done();
- });
+ test('Use header option accept when provided', () => {
+ const authFetchStub = sandbox.stub(helper._auth, 'fetch')
+ .returns(Promise.resolve());
+ const headers = new Headers();
+ headers.append('Accept', '*/*');
+ const fetchOptions = {headers};
+ helper.fetchJSON({url: '/dummy/url', fetchOptions});
+ assert.isTrue(authFetchStub.called);
+ assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+ '*/*');
});
});
+
+ test('JSON prefix is properly removed', done => {
+ helper.fetchJSON({url: '/dummy/url'}).then(obj => {
+ assert.deepEqual(obj, {hello: 'bonjour'});
+ done();
+ });
+ });
+
+ test('cached results', done => {
+ let n = 0;
+ sandbox.stub(helper, 'fetchJSON', () => Promise.resolve(++n));
+ const promises = [];
+ promises.push(helper.fetchCacheURL('/foo'));
+ promises.push(helper.fetchCacheURL('/foo'));
+ promises.push(helper.fetchCacheURL('/foo'));
+
+ Promise.all(promises).then(results => {
+ assert.deepEqual(results, [1, 1, 1]);
+ helper.fetchCacheURL('/foo').then(foo => {
+ assert.equal(foo, 1);
+ done();
+ });
+ });
+ });
+
+ test('cached promise', done => {
+ const promise = Promise.reject(new Error('foo'));
+ cache.set('/foo', promise);
+ helper.fetchCacheURL({url: '/foo'}).catch(p => {
+ assert.equal(p.message, 'foo');
+ done();
+ });
+ });
+
+ test('cache invalidation', () => {
+ cache.set('/foo/bar', 1);
+ cache.set('/bar', 2);
+ fetchPromisesCache.set('/foo/bar', 3);
+ fetchPromisesCache.set('/bar', 4);
+ helper.invalidateFetchPromisesPrefix('/foo/');
+ assert.isFalse(cache.has('/foo/bar'));
+ assert.isTrue(cache.has('/bar'));
+ assert.isUndefined(fetchPromisesCache.get('/foo/bar'));
+ assert.strictEqual(4, fetchPromisesCache.get('/bar'));
+ });
+
+ test('params are properly encoded', () => {
+ let url = helper.urlWithParams('/path/', {
+ sp: 'hola',
+ gr: 'guten tag',
+ noval: null,
+ });
+ assert.equal(url,
+ window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
+
+ url = helper.urlWithParams('/path/', {
+ sp: 'hola',
+ en: ['hey', 'hi'],
+ });
+ assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
+
+ // Order must be maintained with array params.
+ url = helper.urlWithParams('/path/', {
+ l: ['c', 'b', 'a'],
+ });
+ assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
+ });
+
+ test('request callbacks can be canceled', done => {
+ let cancelCalled = false;
+ window.fetch.returns(Promise.resolve({
+ body: {
+ cancel() { cancelCalled = true; },
+ },
+ }));
+ const cancelCondition = () => true;
+ helper.fetchJSON({url: '/dummy/url', cancelCondition}).then(
+ obj => {
+ assert.isUndefined(obj);
+ assert.isTrue(cancelCalled);
+ done();
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index 678e02a..6dcdc48 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -19,289 +19,292 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-updates-parser</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="../../../scripts/util.js"></script>
-<script src="gr-reviewer-updates-parser.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 type="module" src="../../../scripts/util.js"></script>
+<script type="module" src="./gr-reviewer-updates-parser.js"></script>
-<script>
- suite('gr-reviewer-updates-parser tests', async () => {
- await readyToTest();
- let sandbox;
- let instance;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-reviewer-updates-parser.js';
+suite('gr-reviewer-updates-parser tests', () => {
+ let sandbox;
+ let instance;
- setup(() => {
- sandbox = sinon.sandbox.create();
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('ignores changes without messages', () => {
- const change = {};
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_groupUpdates');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_formatUpdates');
- assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._groupUpdates.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._formatUpdates.called);
- });
+ test('ignores changes without messages', () => {
+ const change = {};
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_groupUpdates');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_formatUpdates');
+ assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._groupUpdates.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._formatUpdates.called);
+ });
- test('ignores changes without reviewer updates', () => {
- const change = {
- messages: [],
- };
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_groupUpdates');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_formatUpdates');
- assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._groupUpdates.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._formatUpdates.called);
- });
+ test('ignores changes without reviewer updates', () => {
+ const change = {
+ messages: [],
+ };
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_groupUpdates');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_formatUpdates');
+ assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._groupUpdates.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._formatUpdates.called);
+ });
- test('ignores changes with empty reviewer updates', () => {
- const change = {
- messages: [],
- reviewer_updates: [],
- };
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_groupUpdates');
- sandbox.stub(
- GrReviewerUpdatesParser.prototype, '_formatUpdates');
- assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._groupUpdates.called);
- assert.isFalse(
- GrReviewerUpdatesParser.prototype._formatUpdates.called);
- });
+ test('ignores changes with empty reviewer updates', () => {
+ const change = {
+ messages: [],
+ reviewer_updates: [],
+ };
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_filterRemovedMessages');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_groupUpdates');
+ sandbox.stub(
+ GrReviewerUpdatesParser.prototype, '_formatUpdates');
+ assert.strictEqual(GrReviewerUpdatesParser.parse(change), change);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._filterRemovedMessages.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._groupUpdates.called);
+ assert.isFalse(
+ GrReviewerUpdatesParser.prototype._formatUpdates.called);
+ });
- test('filter removed messages', () => {
- const change = {
- messages: [
- {
- message: 'msg1',
- tag: 'autogenerated:gerrit:deleteReviewer',
- },
- {
- message: 'msg2',
- tag: 'foo',
- },
- ],
- };
- instance = new GrReviewerUpdatesParser(change);
- instance._filterRemovedMessages();
- assert.deepEqual(instance.result, {
- messages: [{
+ test('filter removed messages', () => {
+ const change = {
+ messages: [
+ {
+ message: 'msg1',
+ tag: 'autogenerated:gerrit:deleteReviewer',
+ },
+ {
message: 'msg2',
tag: 'foo',
- }],
- });
- });
-
- test('group reviewer updates', () => {
- const reviewer1 = {_account_id: 1};
- const reviewer2 = {_account_id: 2};
- const date1 = '2017-01-26 12:11:50.000000000';
- const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold.
- const date3 = '2017-01-26 12:33:50.000000000';
- const date4 = '2017-01-26 12:44:50.000000000';
- const makeItem = function(state, reviewer, opt_date, opt_author) {
- return {
- reviewer,
- updated: opt_date || date1,
- updated_by: opt_author || reviewer1,
- state,
- };
- };
- let change = {
- reviewer_updates: [
- makeItem('REVIEWER', reviewer1), // New group.
- makeItem('CC', reviewer2), // Appended.
- makeItem('REVIEWER', reviewer2, date2), // Overrides previous one.
-
- makeItem('CC', reviewer1, date2, reviewer2), // New group.
-
- makeItem('REMOVED', reviewer2, date3), // Group has no state change.
- makeItem('REVIEWER', reviewer2, date3),
-
- makeItem('CC', reviewer1, date4), // No change, removed.
- makeItem('REVIEWER', reviewer1, date4), // Forms new group
- makeItem('REMOVED', reviewer2, date4), // Should be grouped.
- ],
- };
-
- instance = new GrReviewerUpdatesParser(change);
- instance._groupUpdates();
- change = instance.result;
-
- assert.equal(change.reviewer_updates.length, 3);
- assert.equal(change.reviewer_updates[0].updates.length, 2);
- assert.equal(change.reviewer_updates[1].updates.length, 1);
- assert.equal(change.reviewer_updates[2].updates.length, 2);
-
- assert.equal(change.reviewer_updates[0].date, date1);
- assert.deepEqual(change.reviewer_updates[0].author, reviewer1);
- assert.deepEqual(change.reviewer_updates[0].updates, [
- {
- reviewer: reviewer1,
- state: 'REVIEWER',
},
- {
- reviewer: reviewer2,
- state: 'REVIEWER',
- },
- ]);
-
- assert.equal(change.reviewer_updates[1].date, date2);
- assert.deepEqual(change.reviewer_updates[1].author, reviewer2);
- assert.deepEqual(change.reviewer_updates[1].updates, [
- {
- reviewer: reviewer1,
- state: 'CC',
- prev_state: 'REVIEWER',
- },
- ]);
-
- assert.equal(change.reviewer_updates[2].date, date4);
- assert.deepEqual(change.reviewer_updates[2].author, reviewer1);
- assert.deepEqual(change.reviewer_updates[2].updates, [
- {
- reviewer: reviewer1,
- prev_state: 'CC',
- state: 'REVIEWER',
- },
- {
- reviewer: reviewer2,
- prev_state: 'REVIEWER',
- state: 'REMOVED',
- },
- ]);
- });
-
- test('format reviewer updates', () => {
- const reviewer1 = {_account_id: 1};
- const reviewer2 = {_account_id: 2};
- const makeItem = function(prev, state, opt_reviewer) {
- return {
- reviewer: opt_reviewer || reviewer1,
- prev_state: prev,
- state,
- };
- };
- const makeUpdate = function(items) {
- return {
- author: reviewer1,
- updated: '',
- updates: items,
- };
- };
- const change = {
- reviewer_updates: [
- makeUpdate([
- makeItem(undefined, 'CC'),
- makeItem(undefined, 'CC', reviewer2),
- ]),
- makeUpdate([
- makeItem('CC', 'REVIEWER'),
- makeItem('REVIEWER', 'REMOVED'),
- makeItem('REMOVED', 'REVIEWER'),
- makeItem(undefined, 'REVIEWER', reviewer2),
- ]),
- ],
- };
-
- instance = new GrReviewerUpdatesParser(change);
- instance._formatUpdates();
-
- assert.equal(change.reviewer_updates.length, 2);
- assert.equal(change.reviewer_updates[0].updates.length, 1);
- assert.equal(change.reviewer_updates[1].updates.length, 3);
-
- let items = change.reviewer_updates[0].updates;
- assert.equal(items[0].message, 'Added to cc: ');
- assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]);
-
- items = change.reviewer_updates[1].updates;
- assert.equal(items[0].message, 'Moved from cc to reviewer: ');
- assert.deepEqual(items[0].reviewers, [reviewer1]);
- assert.equal(items[1].message, 'Removed from reviewer: ');
- assert.deepEqual(items[1].reviewers, [reviewer1]);
- assert.equal(items[2].message, 'Added to reviewer: ');
- assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]);
- });
-
- test('_advanceUpdates', () => {
- const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime();
- const tplus = delta => new Date(T0 + delta)
- .toISOString()
- .replace('T', ' ')
- .replace('Z', '000000');
- const change = {
- reviewer_updates: [{
- date: tplus(0),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'same time update',
- }],
- }, {
- date: tplus(200),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'update within threshold',
- }],
- }, {
- date: tplus(600),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'update between messages',
- }],
- }, {
- date: tplus(1000),
- type: 'REVIEWER_UPDATE',
- updates: [{
- message: 'late update',
- }],
- }],
- messages: [{
- id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
- date: tplus(0),
- message: 'Uploaded patch set 1.',
- }, {
- id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
- date: tplus(800),
- message: 'Uploaded patch set 2.',
- }],
- };
- instance = new GrReviewerUpdatesParser(change);
- instance._advanceUpdates();
- const updates = instance.result.reviewer_updates;
- assert.isBelow(util.parseDate(updates[0].date).getTime(), T0);
- assert.isBelow(util.parseDate(updates[1].date).getTime(), T0);
- assert.equal(updates[2].date, tplus(100));
- assert.equal(updates[3].date, tplus(500));
+ ],
+ };
+ instance = new GrReviewerUpdatesParser(change);
+ instance._filterRemovedMessages();
+ assert.deepEqual(instance.result, {
+ messages: [{
+ message: 'msg2',
+ tag: 'foo',
+ }],
});
});
+
+ test('group reviewer updates', () => {
+ const reviewer1 = {_account_id: 1};
+ const reviewer2 = {_account_id: 2};
+ const date1 = '2017-01-26 12:11:50.000000000';
+ const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold.
+ const date3 = '2017-01-26 12:33:50.000000000';
+ const date4 = '2017-01-26 12:44:50.000000000';
+ const makeItem = function(state, reviewer, opt_date, opt_author) {
+ return {
+ reviewer,
+ updated: opt_date || date1,
+ updated_by: opt_author || reviewer1,
+ state,
+ };
+ };
+ let change = {
+ reviewer_updates: [
+ makeItem('REVIEWER', reviewer1), // New group.
+ makeItem('CC', reviewer2), // Appended.
+ makeItem('REVIEWER', reviewer2, date2), // Overrides previous one.
+
+ makeItem('CC', reviewer1, date2, reviewer2), // New group.
+
+ makeItem('REMOVED', reviewer2, date3), // Group has no state change.
+ makeItem('REVIEWER', reviewer2, date3),
+
+ makeItem('CC', reviewer1, date4), // No change, removed.
+ makeItem('REVIEWER', reviewer1, date4), // Forms new group
+ makeItem('REMOVED', reviewer2, date4), // Should be grouped.
+ ],
+ };
+
+ instance = new GrReviewerUpdatesParser(change);
+ instance._groupUpdates();
+ change = instance.result;
+
+ assert.equal(change.reviewer_updates.length, 3);
+ assert.equal(change.reviewer_updates[0].updates.length, 2);
+ assert.equal(change.reviewer_updates[1].updates.length, 1);
+ assert.equal(change.reviewer_updates[2].updates.length, 2);
+
+ assert.equal(change.reviewer_updates[0].date, date1);
+ assert.deepEqual(change.reviewer_updates[0].author, reviewer1);
+ assert.deepEqual(change.reviewer_updates[0].updates, [
+ {
+ reviewer: reviewer1,
+ state: 'REVIEWER',
+ },
+ {
+ reviewer: reviewer2,
+ state: 'REVIEWER',
+ },
+ ]);
+
+ assert.equal(change.reviewer_updates[1].date, date2);
+ assert.deepEqual(change.reviewer_updates[1].author, reviewer2);
+ assert.deepEqual(change.reviewer_updates[1].updates, [
+ {
+ reviewer: reviewer1,
+ state: 'CC',
+ prev_state: 'REVIEWER',
+ },
+ ]);
+
+ assert.equal(change.reviewer_updates[2].date, date4);
+ assert.deepEqual(change.reviewer_updates[2].author, reviewer1);
+ assert.deepEqual(change.reviewer_updates[2].updates, [
+ {
+ reviewer: reviewer1,
+ prev_state: 'CC',
+ state: 'REVIEWER',
+ },
+ {
+ reviewer: reviewer2,
+ prev_state: 'REVIEWER',
+ state: 'REMOVED',
+ },
+ ]);
+ });
+
+ test('format reviewer updates', () => {
+ const reviewer1 = {_account_id: 1};
+ const reviewer2 = {_account_id: 2};
+ const makeItem = function(prev, state, opt_reviewer) {
+ return {
+ reviewer: opt_reviewer || reviewer1,
+ prev_state: prev,
+ state,
+ };
+ };
+ const makeUpdate = function(items) {
+ return {
+ author: reviewer1,
+ updated: '',
+ updates: items,
+ };
+ };
+ const change = {
+ reviewer_updates: [
+ makeUpdate([
+ makeItem(undefined, 'CC'),
+ makeItem(undefined, 'CC', reviewer2),
+ ]),
+ makeUpdate([
+ makeItem('CC', 'REVIEWER'),
+ makeItem('REVIEWER', 'REMOVED'),
+ makeItem('REMOVED', 'REVIEWER'),
+ makeItem(undefined, 'REVIEWER', reviewer2),
+ ]),
+ ],
+ };
+
+ instance = new GrReviewerUpdatesParser(change);
+ instance._formatUpdates();
+
+ assert.equal(change.reviewer_updates.length, 2);
+ assert.equal(change.reviewer_updates[0].updates.length, 1);
+ assert.equal(change.reviewer_updates[1].updates.length, 3);
+
+ let items = change.reviewer_updates[0].updates;
+ assert.equal(items[0].message, 'Added to cc: ');
+ assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]);
+
+ items = change.reviewer_updates[1].updates;
+ assert.equal(items[0].message, 'Moved from cc to reviewer: ');
+ assert.deepEqual(items[0].reviewers, [reviewer1]);
+ assert.equal(items[1].message, 'Removed from reviewer: ');
+ assert.deepEqual(items[1].reviewers, [reviewer1]);
+ assert.equal(items[2].message, 'Added to reviewer: ');
+ assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]);
+ });
+
+ test('_advanceUpdates', () => {
+ const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime();
+ const tplus = delta => new Date(T0 + delta)
+ .toISOString()
+ .replace('T', ' ')
+ .replace('Z', '000000');
+ const change = {
+ reviewer_updates: [{
+ date: tplus(0),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'same time update',
+ }],
+ }, {
+ date: tplus(200),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'update within threshold',
+ }],
+ }, {
+ date: tplus(600),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'update between messages',
+ }],
+ }, {
+ date: tplus(1000),
+ type: 'REVIEWER_UPDATE',
+ updates: [{
+ message: 'late update',
+ }],
+ }],
+ messages: [{
+ id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
+ date: tplus(0),
+ message: 'Uploaded patch set 1.',
+ }, {
+ id: '6734489eb9d642de28dbf2bcf9bda875923800d8',
+ date: tplus(800),
+ message: 'Uploaded patch set 2.',
+ }],
+ };
+ instance = new GrReviewerUpdatesParser(change);
+ instance._advanceUpdates();
+ const updates = instance.result.reviewer_updates;
+ assert.isBelow(util.parseDate(updates[0].date).getTime(), T0);
+ assert.isBelow(util.parseDate(updates[1].date).getTime(), T0);
+ assert.equal(updates[2].date, tplus(100));
+ assert.equal(updates[3].date, tplus(500));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
new file mode 100644
index 0000000..e29d300
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
@@ -0,0 +1,167 @@
+/**
+ * @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.
+ */
+import '../../../scripts/bundled-polymer.js';
+
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+const RESPONSE = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 45,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 48,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {
+ ab: [
+ 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' +
+ 'nulla phasellus.',
+ 'Mattis lectus.',
+ 'Sodales duis.',
+ 'Orci a faucibus.',
+ ],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ },
+ {
+ ab: [
+ 'Sem nascetur, erat ut, non in.',
+ 'A donec, venenatis pellentesque dis.',
+ 'Mauris mauris.',
+ 'Quisque nisl duis, facilisis viverra.',
+ 'Justo purus, semper eget et.',
+ ],
+ },
+ {
+ a: [
+ 'Est amet, vestibulum pellentesque.',
+ 'Erat ligula.',
+ 'Justo eros.',
+ 'Fringilla quisque.',
+ ],
+ },
+ {
+ ab: [
+ 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
+ 'Eros suspendisse.',
+ ],
+ },
+ {
+ a: [
+ 'Rhoncus tempor, ultricies aliquam ipsum.',
+ ],
+ b: [
+ 'Rhoncus tempor, ultricies praesent ipsum.',
+ ],
+ edit_a: [
+ [
+ 26,
+ 7,
+ ],
+ ],
+ edit_b: [
+ [
+ 26,
+ 8,
+ ],
+ ],
+ },
+ {
+ ab: [
+ 'Sollicitudin duis.',
+ 'Blandit blandit, ante nisl fusce.',
+ 'Felis ac at, tellus consectetuer.',
+ 'Sociis ligula sapien, egestas leo.',
+ 'Cum pulvinar, sed mauris, cursus neque velit.',
+ 'Augue porta lobortis.',
+ 'Nibh lorem, amet fermentum turpis, vel pulvinar diam.',
+ 'Id quam ipsum, id urna et, massa suspendisse.',
+ 'Ac nec, nibh praesent.',
+ 'Rutrum vestibulum.',
+ 'Est tellus, bibendum habitasse.',
+ 'Justo facilisis, vel nulla.',
+ 'Donec eu, vulputate neque aliquam, nulla dui.',
+ 'Risus adipiscing in.',
+ 'Lacus arcu arcu.',
+ 'Urna velit.',
+ 'Urna a dolor.',
+ 'Lectus magna augue, convallis mattis tortor, sed tellus ' +
+ 'consequat.',
+ 'Etiam dui, blandit wisi.',
+ 'Mi nec.',
+ 'Vitae eget vestibulum.',
+ 'Ullamcorper nunc ante, nec imperdiet felis, consectetur in.',
+ 'Ac eget.',
+ 'Vel fringilla, interdum pellentesque placerat, proin ante.',
+ ],
+ },
+ {
+ b: [
+ 'Eu congue risus.',
+ 'Enim ac, quis elementum.',
+ 'Non et elit.',
+ 'Etiam aliquam, diam vel nunc.',
+ ],
+ },
+ {
+ ab: [
+ 'Nec at.',
+ 'Arcu mauris, venenatis lacus fermentum, praesent duis.',
+ 'Pellentesque amet et, tellus duis.',
+ 'Ipsum arcu vitae, justo elit, sed libero tellus.',
+ 'Metus rutrum euismod, vivamus sodales, vel arcu nisl.',
+ ],
+ },
+ ],
+};
+
+Polymer({
+ _template: html`
+
+`,
+
+ is: 'mock-diff-response',
+
+ properties: {
+ diffResponse: {
+ type: Object,
+ value() {
+ return RESPONSE;
+ },
+ },
+ },
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
deleted file mode 100644
index f1ef86a..0000000
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html
+++ /dev/null
@@ -1,24 +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">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-
-<dom-module id="gr-select">
- <slot></slot>
- <script src="gr-select.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index 3e59aee..18be73d 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -14,77 +14,89 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.FireMixin
- * @extends Polymer.Element
- */
- class GrSelect extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-select'; }
+import '../../../behaviors/fire-behavior/fire-behavior.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';
+const $_documentContainer = document.createElement('template');
- static get properties() {
- return {
- bindValue: {
- type: String,
- notify: true,
- observer: '_updateValue',
- },
- };
- }
+$_documentContainer.innerHTML = `<dom-module id="gr-select">
+ <slot></slot>
+
+</dom-module>`;
- get nativeSelect() {
- // gr-select is not a shadow component
- // TODO(taoalpha): maybe we should convert
- // it into a shadow dom component instead
- return this.querySelector('select');
- }
+document.head.appendChild($_documentContainer.content);
- _updateValue() {
- // It's possible to have a value of 0.
- if (this.bindValue !== undefined) {
- // Set for chrome/safari so it happens instantly
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @extends Polymer.Element
+ */
+class GrSelect extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get is() { return 'gr-select'; }
+
+ static get properties() {
+ return {
+ bindValue: {
+ type: String,
+ notify: true,
+ observer: '_updateValue',
+ },
+ };
+ }
+
+ get nativeSelect() {
+ // gr-select is not a shadow component
+ // TODO(taoalpha): maybe we should convert
+ // it into a shadow dom component instead
+ return this.querySelector('select');
+ }
+
+ _updateValue() {
+ // It's possible to have a value of 0.
+ if (this.bindValue !== undefined) {
+ // Set for chrome/safari so it happens instantly
+ this.nativeSelect.value = this.bindValue;
+ // Async needed for firefox to populate value. It was trying to do it
+ // before options from a dom-repeat were rendered previously.
+ // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735
+ this.async(() => {
this.nativeSelect.value = this.bindValue;
- // Async needed for firefox to populate value. It was trying to do it
- // before options from a dom-repeat were rendered previously.
- // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735
- this.async(() => {
- this.nativeSelect.value = this.bindValue;
- }, 1);
- }
- }
-
- _valueChanged() {
- this.bindValue = this.nativeSelect.value;
- }
-
- focus() {
- this.nativeSelect.focus();
- }
-
- /** @override */
- created() {
- super.created();
- this.addEventListener('change',
- () => this._valueChanged());
- this.addEventListener('dom-change',
- () => this._updateValue());
- }
-
- /** @override */
- ready() {
- super.ready();
- // If not set via the property, set bind-value to the element value.
- if (this.bindValue == undefined && this.nativeSelect.options.length > 0) {
- this.bindValue = this.nativeSelect.value;
- }
+ }, 1);
}
}
- customElements.define(GrSelect.is, GrSelect);
-})();
+ _valueChanged() {
+ this.bindValue = this.nativeSelect.value;
+ }
+
+ focus() {
+ this.nativeSelect.focus();
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('change',
+ () => this._valueChanged());
+ this.addEventListener('dom-change',
+ () => this._updateValue());
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ // If not set via the property, set bind-value to the element value.
+ if (this.bindValue == undefined && this.nativeSelect.options.length > 0) {
+ this.bindValue = this.nativeSelect.value;
+ }
+ }
+}
+
+customElements.define(GrSelect.is, GrSelect);
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
index 536f4f8..a4d28a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_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-select</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-select.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-select.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-select.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -50,71 +55,73 @@
</template>
</test-fixture>
-<script>
- suite('gr-select tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-select.js';
+suite('gr-select tests', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('bindValue must be set to the first option value', () => {
+ assert.equal(element.bindValue, '1');
+ });
+
+ test('value of 0 should still trigger value updates', () => {
+ element.bindValue = 0;
+ assert.equal(element.nativeSelect.value, 0);
+ });
+
+ test('bidirectional binding property-to-attribute', () => {
+ const changeStub = sinon.stub();
+ element.addEventListener('bind-value-changed', changeStub);
+
+ // The selected element should be the first one by default.
+ assert.equal(element.nativeSelect.value, '1');
+ assert.equal(element.bindValue, '1');
+ assert.isFalse(changeStub.called);
+
+ // Now change the value.
+ element.bindValue = '2';
+
+ // It should be updated.
+ assert.equal(element.nativeSelect.value, '2');
+ assert.equal(element.bindValue, '2');
+ assert.isTrue(changeStub.called);
+ });
+
+ test('bidirectional binding attribute-to-property', () => {
+ const changeStub = sinon.stub();
+ element.addEventListener('bind-value-changed', changeStub);
+
+ // The selected element should be the first one by default.
+ assert.equal(element.nativeSelect.value, '1');
+ assert.equal(element.bindValue, '1');
+ assert.isFalse(changeStub.called);
+
+ // Now change the value.
+ element.nativeSelect.value = '3';
+ element.fire('change');
+
+ // It should be updated.
+ assert.equal(element.nativeSelect.value, '3');
+ assert.equal(element.bindValue, '3');
+ assert.isTrue(changeStub.called);
+ });
+
+ suite('gr-select no options tests', () => {
let element;
setup(() => {
- element = fixture('basic');
+ element = fixture('noOptions');
});
- test('bindValue must be set to the first option value', () => {
- assert.equal(element.bindValue, '1');
- });
-
- test('value of 0 should still trigger value updates', () => {
- element.bindValue = 0;
- assert.equal(element.nativeSelect.value, 0);
- });
-
- test('bidirectional binding property-to-attribute', () => {
- const changeStub = sinon.stub();
- element.addEventListener('bind-value-changed', changeStub);
-
- // The selected element should be the first one by default.
- assert.equal(element.nativeSelect.value, '1');
- assert.equal(element.bindValue, '1');
- assert.isFalse(changeStub.called);
-
- // Now change the value.
- element.bindValue = '2';
-
- // It should be updated.
- assert.equal(element.nativeSelect.value, '2');
- assert.equal(element.bindValue, '2');
- assert.isTrue(changeStub.called);
- });
-
- test('bidirectional binding attribute-to-property', () => {
- const changeStub = sinon.stub();
- element.addEventListener('bind-value-changed', changeStub);
-
- // The selected element should be the first one by default.
- assert.equal(element.nativeSelect.value, '1');
- assert.equal(element.bindValue, '1');
- assert.isFalse(changeStub.called);
-
- // Now change the value.
- element.nativeSelect.value = '3';
- element.fire('change');
-
- // It should be updated.
- assert.equal(element.nativeSelect.value, '3');
- assert.equal(element.bindValue, '3');
- assert.isTrue(changeStub.called);
- });
-
- suite('gr-select no options tests', () => {
- let element;
-
- setup(() => {
- element = fixture('noOptions');
- });
-
- test('bindValue must not be changed', () => {
- assert.isUndefined(element.bindValue);
- });
+ test('bindValue must not be changed', () => {
+ assert.isUndefined(element.bindValue);
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
index 63dbcbd..151498c 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -14,26 +14,33 @@
* 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 GrShellCommand extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-shell-command'; }
+import '../../../styles/shared-styles.js';
+import '../gr-copy-clipboard/gr-copy-clipboard.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-shell-command_html.js';
- static get properties() {
- return {
- command: String,
- label: String,
- };
- }
+/** @extends Polymer.Element */
+class GrShellCommand extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- focusOnCopy() {
- this.shadowRoot.querySelector('gr-copy-clipboard').focusOnCopy();
- }
+ static get is() { return 'gr-shell-command'; }
+
+ static get properties() {
+ return {
+ command: String,
+ label: String,
+ };
}
- customElements.define(GrShellCommand.is, GrShellCommand);
-})();
+ focusOnCopy() {
+ this.shadowRoot.querySelector('gr-copy-clipboard').focusOnCopy();
+ }
+}
+
+customElements.define(GrShellCommand.is, GrShellCommand);
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js
index 15e282f..8fbf2b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_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="../../../styles/shared-styles.html">
-<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
-
-<dom-module id="gr-shell-command">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
.commandContainer {
margin-bottom: var(--spacing-m);
@@ -33,7 +29,7 @@
width: 100%;
}
.commandContainer:before {
- content: '$';
+ content: '\$';
position: absolute;
display: block;
box-sizing: border-box;
@@ -58,6 +54,4 @@
<div class="commandContainer">
<gr-copy-clipboard text="[[command]]"></gr-copy-clipboard>
</div>
- </template>
- <script src="gr-shell-command.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
index b596a4a..4e5be4d 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_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-shell-command</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-shell-command.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-shell-command.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-shell-command.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,30 +40,32 @@
</template>
</test-fixture>
-<script>
- suite('gr-shell-command 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-shell-command.js';
+suite('gr-shell-command tests', () => {
+ let element;
+ let sandbox;
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('basic');
- element.text = `git fetch http://gerrit@localhost:8080/a/test-project
- refs/changes/05/5/1 && git checkout FETCH_HEAD`;
- flushAsynchronousOperations();
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('focusOnCopy', () => {
- const focusStub = sandbox.stub(element.shadowRoot
- .querySelector('gr-copy-clipboard'),
- 'focusOnCopy');
- element.focusOnCopy();
- assert.isTrue(focusStub.called);
- });
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.text = `git fetch http://gerrit@localhost:8080/a/test-project
+ refs/changes/05/5/1 && git checkout FETCH_HEAD`;
+ flushAsynchronousOperations();
});
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('focusOnCopy', () => {
+ const focusStub = sandbox.stub(element.shadowRoot
+ .querySelector('gr-copy-clipboard'),
+ 'focusOnCopy');
+ element.focusOnCopy();
+ assert.isTrue(focusStub.called);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
deleted file mode 100644
index 7215b26..0000000
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html
+++ /dev/null
@@ -1,20 +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-storage">
- <script src="gr-storage.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 8cc9de9..1597439 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -14,148 +14,150 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const DURATION_DAY = 24 * 60 * 60 * 1000;
+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';
- // Clean up old entries no more frequently than one day.
- const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY;
+const DURATION_DAY = 24 * 60 * 60 * 1000;
- const CLEANUP_PREFIXES_MAX_AGE_MAP = {
- // respectfultip has a 3 day expiration
- 'respectfultip:': 3 * DURATION_DAY,
- 'draft:': DURATION_DAY,
- 'editablecontent:': DURATION_DAY,
- };
+// Clean up old entries no more frequently than one day.
+const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY;
- /** @extends Polymer.Element */
- class GrStorage extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-storage'; }
+const CLEANUP_PREFIXES_MAX_AGE_MAP = {
+ // respectfultip has a 3 day expiration
+ 'respectfultip:': 3 * DURATION_DAY,
+ 'draft:': DURATION_DAY,
+ 'editablecontent:': DURATION_DAY,
+};
- static get properties() {
- return {
- _lastCleanup: Number,
- /** @type {?Storage} */
- _storage: {
- type: Object,
- value() {
- return window.localStorage;
- },
+/** @extends Polymer.Element */
+class GrStorage extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get is() { return 'gr-storage'; }
+
+ static get properties() {
+ return {
+ _lastCleanup: Number,
+ /** @type {?Storage} */
+ _storage: {
+ type: Object,
+ value() {
+ return window.localStorage;
},
- _exceededQuota: {
- type: Boolean,
- value: false,
- },
- };
+ },
+ _exceededQuota: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
+
+ getDraftComment(location) {
+ this._cleanupItems();
+ return this._getObject(this._getDraftKey(location));
+ }
+
+ setDraftComment(location, message) {
+ const key = this._getDraftKey(location);
+ this._setObject(key, {message, updated: Date.now()});
+ }
+
+ eraseDraftComment(location) {
+ const key = this._getDraftKey(location);
+ this._storage.removeItem(key);
+ }
+
+ getEditableContentItem(key) {
+ this._cleanupItems();
+ return this._getObject(this._getEditableContentKey(key));
+ }
+
+ setEditableContentItem(key, message) {
+ this._setObject(this._getEditableContentKey(key),
+ {message, updated: Date.now()});
+ }
+
+ getRespectfulTipVisibility() {
+ this._cleanupItems();
+ return this._getObject('respectfultip:visibility');
+ }
+
+ setRespectfulTipVisibility(delayDays = 0) {
+ this._cleanupItems();
+ this._setObject(
+ 'respectfultip:visibility',
+ {updated: Date.now() + delayDays * DURATION_DAY}
+ );
+ }
+
+ eraseEditableContentItem(key) {
+ this._storage.removeItem(this._getEditableContentKey(key));
+ }
+
+ _getDraftKey(location) {
+ const range = location.range ?
+ `${location.range.start_line}-${location.range.start_character}` +
+ `-${location.range.end_character}-${location.range.end_line}` :
+ null;
+ let key = ['draft', location.changeNum, location.patchNum, location.path,
+ location.line || ''].join(':');
+ if (range) {
+ key = key + ':' + range;
}
+ return key;
+ }
- getDraftComment(location) {
- this._cleanupItems();
- return this._getObject(this._getDraftKey(location));
+ _getEditableContentKey(key) {
+ return `editablecontent:${key}`;
+ }
+
+ _cleanupItems() {
+ // Throttle cleanup to the throttle interval.
+ if (this._lastCleanup &&
+ Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
+ return;
}
+ this._lastCleanup = Date.now();
- setDraftComment(location, message) {
- const key = this._getDraftKey(location);
- this._setObject(key, {message, updated: Date.now()});
- }
-
- eraseDraftComment(location) {
- const key = this._getDraftKey(location);
- this._storage.removeItem(key);
- }
-
- getEditableContentItem(key) {
- this._cleanupItems();
- return this._getObject(this._getEditableContentKey(key));
- }
-
- setEditableContentItem(key, message) {
- this._setObject(this._getEditableContentKey(key),
- {message, updated: Date.now()});
- }
-
- getRespectfulTipVisibility() {
- this._cleanupItems();
- return this._getObject('respectfultip:visibility');
- }
-
- setRespectfulTipVisibility(delayDays = 0) {
- this._cleanupItems();
- this._setObject(
- 'respectfultip:visibility',
- {updated: Date.now() + delayDays * DURATION_DAY}
- );
- }
-
- eraseEditableContentItem(key) {
- this._storage.removeItem(this._getEditableContentKey(key));
- }
-
- _getDraftKey(location) {
- const range = location.range ?
- `${location.range.start_line}-${location.range.start_character}` +
- `-${location.range.end_character}-${location.range.end_line}` :
- null;
- let key = ['draft', location.changeNum, location.patchNum, location.path,
- location.line || ''].join(':');
- if (range) {
- key = key + ':' + range;
- }
- return key;
- }
-
- _getEditableContentKey(key) {
- return `editablecontent:${key}`;
- }
-
- _cleanupItems() {
- // Throttle cleanup to the throttle interval.
- if (this._lastCleanup &&
- Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
- return;
- }
- this._lastCleanup = Date.now();
-
- let item;
- Object.keys(this._storage).forEach(key => {
- Object.keys(CLEANUP_PREFIXES_MAX_AGE_MAP).forEach(prefix => {
- if (key.startsWith(prefix)) {
- item = this._getObject(key);
- const expiration = CLEANUP_PREFIXES_MAX_AGE_MAP[prefix];
- if (Date.now() - item.updated > expiration) {
- this._storage.removeItem(key);
- }
+ let item;
+ Object.keys(this._storage).forEach(key => {
+ Object.keys(CLEANUP_PREFIXES_MAX_AGE_MAP).forEach(prefix => {
+ if (key.startsWith(prefix)) {
+ item = this._getObject(key);
+ const expiration = CLEANUP_PREFIXES_MAX_AGE_MAP[prefix];
+ if (Date.now() - item.updated > expiration) {
+ this._storage.removeItem(key);
}
- });
- });
- }
-
- _getObject(key) {
- const serial = this._storage.getItem(key);
- if (!serial) { return null; }
- return JSON.parse(serial);
- }
-
- _setObject(key, obj) {
- if (this._exceededQuota) { return; }
- try {
- this._storage.setItem(key, JSON.stringify(obj));
- } catch (exc) {
- // Catch for QuotaExceededError and disable writes on local storage the
- // first time that it occurs.
- if (exc.code === 22) {
- this._exceededQuota = true;
- console.warn('Local storage quota exceeded: disabling');
- return;
- } else {
- throw exc;
}
+ });
+ });
+ }
+
+ _getObject(key) {
+ const serial = this._storage.getItem(key);
+ if (!serial) { return null; }
+ return JSON.parse(serial);
+ }
+
+ _setObject(key, obj) {
+ if (this._exceededQuota) { return; }
+ try {
+ this._storage.setItem(key, JSON.stringify(obj));
+ } catch (exc) {
+ // Catch for QuotaExceededError and disable writes on local storage the
+ // first time that it occurs.
+ if (exc.code === 22) {
+ this._exceededQuota = true;
+ console.warn('Local storage quota exceeded: disabling');
+ return;
+ } else {
+ throw exc;
}
}
}
+}
- customElements.define(GrStorage.is, GrStorage);
-})();
+customElements.define(GrStorage.is, GrStorage);
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index 66e7f98..06e5915 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_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-storage</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-storage.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-storage.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-storage.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -34,165 +39,167 @@
</template>
</test-fixture>
-<script>
- suite('gr-storage 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-storage.js';
+suite('gr-storage tests', () => {
+ let element;
+ let sandbox;
- function mockStorage(opt_quotaExceeded) {
- return {
- getItem(key) { return this[key]; },
- removeItem(key) { delete this[key]; },
- setItem(key, value) {
- // eslint-disable-next-line no-throw-literal
- if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ }
- this[key] = value;
- },
- };
- }
+ function mockStorage(opt_quotaExceeded) {
+ return {
+ getItem(key) { return this[key]; },
+ removeItem(key) { delete this[key]; },
+ setItem(key, value) {
+ // eslint-disable-next-line no-throw-literal
+ if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ }
+ this[key] = value;
+ },
+ };
+ }
- setup(() => {
- element = fixture('basic');
- sandbox = sinon.sandbox.create();
- element._storage = mockStorage();
- });
-
- teardown(() => sandbox.restore());
-
- test('storing, retrieving and erasing drafts', () => {
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
-
- // The key is in the expected format.
- const key = element._getDraftKey(location);
- assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
-
- // There should be no draft initially.
- const draft = element.getDraftComment(location);
- assert.isNotOk(draft);
-
- // Setting the draft stores it under the expected key.
- element.setDraftComment(location, 'my comment');
- assert.isOk(element._storage.getItem(key));
- assert.equal(JSON.parse(element._storage.getItem(key)).message,
- 'my comment');
- assert.isOk(JSON.parse(element._storage.getItem(key)).updated);
-
- // Erasing the draft removes the key.
- element.eraseDraftComment(location);
- assert.isNotOk(element._storage.getItem(key));
- });
-
- test('automatically removes old drafts', () => {
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
-
- const key = element._getDraftKey(location);
-
- // Make sure that the call to cleanup doesn't get throttled.
- element._lastCleanup = 0;
-
- const cleanupSpy = sandbox.spy(element, '_cleanupItems');
-
- // Create a message with a timestamp that is a second behind the max age.
- element._storage.setItem(key, JSON.stringify({
- message: 'old message',
- updated: Date.now() - 24 * 60 * 60 * 1000 - 1000,
- }));
-
- // Getting the draft should cause it to be removed.
- const draft = element.getDraftComment(location);
-
- assert.isTrue(cleanupSpy.called);
- assert.isNotOk(draft);
- assert.isNotOk(element._storage.getItem(key));
- });
-
- test('_getDraftKey', () => {
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
- let expectedResult = 'draft:1234:5:my_source_file.js:123';
- assert.equal(element._getDraftKey(location), expectedResult);
- location.range = {
- start_character: 1,
- start_line: 1,
- end_character: 1,
- end_line: 2,
- };
- expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2';
- assert.equal(element._getDraftKey(location), expectedResult);
- });
-
- test('exceeded quota disables storage', () => {
- element._storage = mockStorage(true);
- assert.isFalse(element._exceededQuota);
-
- const changeNum = 1234;
- const patchNum = 5;
- const path = 'my_source_file.js';
- const line = 123;
- const location = {
- changeNum,
- patchNum,
- path,
- line,
- };
- const key = element._getDraftKey(location);
- element.setDraftComment(location, 'my comment');
- assert.isTrue(element._exceededQuota);
- assert.isNotOk(element._storage.getItem(key));
- });
-
- test('editable content items', () => {
- const cleanupStub = sandbox.stub(element, '_cleanupItems');
- const key = 'testKey';
- const computedKey = element._getEditableContentKey(key);
- // Key correctly computed.
- assert.equal(computedKey, 'editablecontent:testKey');
-
- element.setEditableContentItem(key, 'my content');
-
- // Setting the draft stores it under the expected key.
- let item = element._storage.getItem(computedKey);
- assert.isOk(item);
- assert.equal(JSON.parse(item).message, 'my content');
- assert.isOk(JSON.parse(item).updated);
-
- // getEditableContentItem performs as expected.
- item = element.getEditableContentItem(key);
- assert.isOk(item);
- assert.equal(item.message, 'my content');
- assert.isOk(item.updated);
- assert.isTrue(cleanupStub.called);
-
- // eraseEditableContentItem performs as expected.
- element.eraseEditableContentItem(key);
- assert.isNotOk(element._storage.getItem(computedKey));
- });
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ element._storage = mockStorage();
});
+
+ teardown(() => sandbox.restore());
+
+ test('storing, retrieving and erasing drafts', () => {
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+
+ // The key is in the expected format.
+ const key = element._getDraftKey(location);
+ assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
+
+ // There should be no draft initially.
+ const draft = element.getDraftComment(location);
+ assert.isNotOk(draft);
+
+ // Setting the draft stores it under the expected key.
+ element.setDraftComment(location, 'my comment');
+ assert.isOk(element._storage.getItem(key));
+ assert.equal(JSON.parse(element._storage.getItem(key)).message,
+ 'my comment');
+ assert.isOk(JSON.parse(element._storage.getItem(key)).updated);
+
+ // Erasing the draft removes the key.
+ element.eraseDraftComment(location);
+ assert.isNotOk(element._storage.getItem(key));
+ });
+
+ test('automatically removes old drafts', () => {
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+
+ const key = element._getDraftKey(location);
+
+ // Make sure that the call to cleanup doesn't get throttled.
+ element._lastCleanup = 0;
+
+ const cleanupSpy = sandbox.spy(element, '_cleanupItems');
+
+ // Create a message with a timestamp that is a second behind the max age.
+ element._storage.setItem(key, JSON.stringify({
+ message: 'old message',
+ updated: Date.now() - 24 * 60 * 60 * 1000 - 1000,
+ }));
+
+ // Getting the draft should cause it to be removed.
+ const draft = element.getDraftComment(location);
+
+ assert.isTrue(cleanupSpy.called);
+ assert.isNotOk(draft);
+ assert.isNotOk(element._storage.getItem(key));
+ });
+
+ test('_getDraftKey', () => {
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+ let expectedResult = 'draft:1234:5:my_source_file.js:123';
+ assert.equal(element._getDraftKey(location), expectedResult);
+ location.range = {
+ start_character: 1,
+ start_line: 1,
+ end_character: 1,
+ end_line: 2,
+ };
+ expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2';
+ assert.equal(element._getDraftKey(location), expectedResult);
+ });
+
+ test('exceeded quota disables storage', () => {
+ element._storage = mockStorage(true);
+ assert.isFalse(element._exceededQuota);
+
+ const changeNum = 1234;
+ const patchNum = 5;
+ const path = 'my_source_file.js';
+ const line = 123;
+ const location = {
+ changeNum,
+ patchNum,
+ path,
+ line,
+ };
+ const key = element._getDraftKey(location);
+ element.setDraftComment(location, 'my comment');
+ assert.isTrue(element._exceededQuota);
+ assert.isNotOk(element._storage.getItem(key));
+ });
+
+ test('editable content items', () => {
+ const cleanupStub = sandbox.stub(element, '_cleanupItems');
+ const key = 'testKey';
+ const computedKey = element._getEditableContentKey(key);
+ // Key correctly computed.
+ assert.equal(computedKey, 'editablecontent:testKey');
+
+ element.setEditableContentItem(key, 'my content');
+
+ // Setting the draft stores it under the expected key.
+ let item = element._storage.getItem(computedKey);
+ assert.isOk(item);
+ assert.equal(JSON.parse(item).message, 'my content');
+ assert.isOk(JSON.parse(item).updated);
+
+ // getEditableContentItem performs as expected.
+ item = element.getEditableContentItem(key);
+ assert.isOk(item);
+ assert.equal(item.message, 'my content');
+ assert.isOk(item.updated);
+ assert.isTrue(cleanupStub.called);
+
+ // eraseEditableContentItem performs as expected.
+ element.eraseEditableContentItem(key);
+ assert.isNotOk(element._storage.getItem(computedKey));
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index 07b664b2..6f4c75d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -14,319 +14,335 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- const MAX_ITEMS_DROPDOWN = 10;
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
+import '../gr-cursor-manager/gr-cursor-manager.js';
+import '../gr-overlay/gr-overlay.js';
+import '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import {flush} 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-textarea_html.js';
- const ALL_SUGGESTIONS = [
- {value: '😊', match: 'smile :)'},
- {value: '👍', match: 'thumbs up'},
- {value: '😄', match: 'laugh :D'},
- {value: '🎉', match: 'party'},
- {value: '😞', match: 'sad :('},
- {value: '😂', match: 'tears :\')'},
- {value: '🙏', match: 'pray'},
- {value: '😐', match: 'neutral :|'},
- {value: '😮', match: 'shock :O'},
- {value: '👎', match: 'thumbs down'},
- {value: '😎', match: 'cool |;)'},
- {value: '😕', match: 'confused'},
- {value: '👌', match: 'ok'},
- {value: '🔥', match: 'fire'},
- {value: '👊', match: 'fistbump'},
- {value: '💯', match: '100'},
- {value: '💔', match: 'broken heart'},
- {value: '🍺', match: 'beer'},
- {value: '✔', match: 'check'},
- {value: '😋', match: 'tongue'},
- {value: '😭', match: 'crying :\'('},
- {value: '🐨', match: 'koala'},
- {value: '🤓', match: 'glasses'},
- {value: '😆', match: 'grin'},
- {value: '💩', match: 'poop'},
- {value: '😢', match: 'tear'},
- {value: '😒', match: 'unamused'},
- {value: '😉', match: 'wink ;)'},
- {value: '🍷', match: 'wine'},
- {value: '😜', match: 'winking tongue ;)'},
- ];
+const MAX_ITEMS_DROPDOWN = 10;
+const ALL_SUGGESTIONS = [
+ {value: '😊', match: 'smile :)'},
+ {value: '👍', match: 'thumbs up'},
+ {value: '😄', match: 'laugh :D'},
+ {value: '🎉', match: 'party'},
+ {value: '😞', match: 'sad :('},
+ {value: '😂', match: 'tears :\')'},
+ {value: '🙏', match: 'pray'},
+ {value: '😐', match: 'neutral :|'},
+ {value: '😮', match: 'shock :O'},
+ {value: '👎', match: 'thumbs down'},
+ {value: '😎', match: 'cool |;)'},
+ {value: '😕', match: 'confused'},
+ {value: '👌', match: 'ok'},
+ {value: '🔥', match: 'fire'},
+ {value: '👊', match: 'fistbump'},
+ {value: '💯', match: '100'},
+ {value: '💔', match: 'broken heart'},
+ {value: '🍺', match: 'beer'},
+ {value: '✔', match: 'check'},
+ {value: '😋', match: 'tongue'},
+ {value: '😭', match: 'crying :\'('},
+ {value: '🐨', match: 'koala'},
+ {value: '🤓', match: 'glasses'},
+ {value: '😆', match: 'grin'},
+ {value: '💩', match: 'poop'},
+ {value: '😢', match: 'tear'},
+ {value: '😒', match: 'unamused'},
+ {value: '😉', match: 'wink ;)'},
+ {value: '🍷', match: 'wine'},
+ {value: '😜', match: 'winking tongue ;)'},
+];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrTextarea extends mixinBehaviors( [
+ Gerrit.FireBehavior,
+ Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-textarea'; }
/**
- * @appliesMixin Gerrit.FireMixin
- * @appliesMixin Gerrit.KeyboardShortcutMixin
- * @extends Polymer.Element
+ * @event bind-value-changed
*/
- class GrTextarea extends Polymer.mixinBehaviors( [
- Gerrit.FireBehavior,
- Gerrit.KeyboardShortcutBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-textarea'; }
- /**
- * @event bind-value-changed
- */
- static get properties() {
- return {
- autocomplete: Boolean,
- disabled: Boolean,
- rows: Number,
- maxRows: Number,
- placeholder: String,
- text: {
- type: String,
- notify: true,
- observer: '_handleTextChanged',
- },
- hideBorder: {
- type: Boolean,
- value: false,
- },
- /** Text input should be rendered in monspace font. */
- monospace: {
- type: Boolean,
- value: false,
- },
- /** Text input should be rendered in code font, which is smaller than the
- standard monospace font. */
- code: {
- type: Boolean,
- value: false,
- },
- /** @type {?number} */
- _colonIndex: Number,
- _currentSearchString: {
- type: String,
- observer: '_determineSuggestions',
- },
- _hideAutocomplete: {
- type: Boolean,
- value: true,
- },
- _index: Number,
- _suggestions: Array,
- // Offset makes dropdown appear below text.
- _verticalOffset: {
- type: Number,
- value: 20,
- readOnly: true,
- },
- };
+ static get properties() {
+ return {
+ autocomplete: Boolean,
+ disabled: Boolean,
+ rows: Number,
+ maxRows: Number,
+ placeholder: String,
+ text: {
+ type: String,
+ notify: true,
+ observer: '_handleTextChanged',
+ },
+ hideBorder: {
+ type: Boolean,
+ value: false,
+ },
+ /** Text input should be rendered in monspace font. */
+ monospace: {
+ type: Boolean,
+ value: false,
+ },
+ /** Text input should be rendered in code font, which is smaller than the
+ standard monospace font. */
+ code: {
+ type: Boolean,
+ value: false,
+ },
+ /** @type {?number} */
+ _colonIndex: Number,
+ _currentSearchString: {
+ type: String,
+ observer: '_determineSuggestions',
+ },
+ _hideAutocomplete: {
+ type: Boolean,
+ value: true,
+ },
+ _index: Number,
+ _suggestions: Array,
+ // Offset makes dropdown appear below text.
+ _verticalOffset: {
+ type: Number,
+ value: 20,
+ readOnly: true,
+ },
+ };
+ }
+
+ get keyBindings() {
+ return {
+ esc: '_handleEscKey',
+ tab: '_handleEnterByKey',
+ enter: '_handleEnterByKey',
+ up: '_handleUpKey',
+ down: '_handleDownKey',
+ };
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ if (this.monospace) {
+ this.classList.add('monospace');
}
-
- get keyBindings() {
- return {
- esc: '_handleEscKey',
- tab: '_handleEnterByKey',
- enter: '_handleEnterByKey',
- up: '_handleUpKey',
- down: '_handleDownKey',
- };
+ if (this.code) {
+ this.classList.add('code');
}
-
- /** @override */
- ready() {
- super.ready();
- if (this.monospace) {
- this.classList.add('monospace');
- }
- if (this.code) {
- this.classList.add('code');
- }
- if (this.hideBorder) {
- this.$.textarea.classList.add('noBorder');
- }
- }
-
- closeDropdown() {
- return this.$.emojiSuggestions.close();
- }
-
- getNativeTextarea() {
- return this.$.textarea.textarea;
- }
-
- putCursorAtEnd() {
- const textarea = this.getNativeTextarea();
- // Put the cursor at the end always.
- textarea.selectionStart = textarea.value.length;
- textarea.selectionEnd = textarea.selectionStart;
- this.async(() => {
- textarea.focus();
- });
- }
-
- _handleEscKey(e) {
- if (this._hideAutocomplete) { return; }
- e.preventDefault();
- e.stopPropagation();
- this._resetEmojiDropdown();
- }
-
- _handleUpKey(e) {
- if (this._hideAutocomplete) { return; }
- e.preventDefault();
- e.stopPropagation();
- this.$.emojiSuggestions.cursorUp();
- this.$.textarea.textarea.focus();
- this.disableEnterKeyForSelectingEmoji = false;
- }
-
- _handleDownKey(e) {
- if (this._hideAutocomplete) { return; }
- e.preventDefault();
- e.stopPropagation();
- this.$.emojiSuggestions.cursorDown();
- this.$.textarea.textarea.focus();
- this.disableEnterKeyForSelectingEmoji = false;
- }
-
- _handleEnterByKey(e) {
- if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) {
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- this._setEmoji(this.$.emojiSuggestions.getCurrentText());
- }
-
- _handleEmojiSelect(e) {
- this._setEmoji(e.detail.selected.dataset.value);
- }
-
- _setEmoji(text) {
- const colonIndex = this._colonIndex;
- this.text = this._getText(text);
- this.$.textarea.selectionStart = colonIndex + 1;
- this.$.textarea.selectionEnd = colonIndex + 1;
- this.$.reporting.reportInteraction('select-emoji', {type: text});
- this._resetEmojiDropdown();
- }
-
- _getText(value) {
- return this.text.substr(0, this._colonIndex || 0) +
- value + this.text.substr(this.$.textarea.selectionStart);
- }
-
- /**
- * Uses a hidden element with the same width and styling of the textarea and
- * the text up until the point of interest. Then caratSpan element is added
- * to the end and is set to be the positionTarget for the dropdown. Together
- * this allows the dropdown to appear near where the user is typing.
- */
- _updateCaratPosition() {
- this._hideAutocomplete = false;
- this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
- this.$.textarea.selectionStart);
-
- const caratSpan = this.$.caratSpan;
- this.$.hiddenText.appendChild(caratSpan);
- this.$.emojiSuggestions.positionTarget = caratSpan;
- this._openEmojiDropdown();
- }
-
- _getFontSize() {
- const fontSizePx = getComputedStyle(this).fontSize || '12px';
- return parseInt(fontSizePx.substr(0, fontSizePx.length - 2),
- 10);
- }
-
- _getScrollTop() {
- return document.body.scrollTop;
- }
-
- /**
- * _handleKeydown used for key handling in the this.$.textarea AND all child
- * autocomplete options.
- */
- _onValueChanged(e) {
- // Relay the event.
- this.fire('bind-value-changed', e);
-
- // If cursor is not in textarea (just opened with colon as last char),
- // Don't do anything.
- if (!e.currentTarget.focused) { return; }
-
- const charAtCursor = e.detail && e.detail.value ?
- e.detail.value[this.$.textarea.selectionStart - 1] : '';
- if (charAtCursor !== ':' && this._colonIndex == null) { return; }
-
- // When a colon is detected, set a colon index. We are interested only on
- // colons after space or in beginning of textarea
- if (charAtCursor === ':') {
- if (this.$.textarea.selectionStart < 2 ||
- e.detail.value[this.$.textarea.selectionStart - 2] === ' ') {
- this._colonIndex = this.$.textarea.selectionStart - 1;
- }
- }
-
- this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
- this.$.textarea.selectionStart - this._colonIndex - 1);
- // Under the following conditions, close and reset the dropdown:
- // - The cursor is no longer at the end of the current search string
- // - The search string is an space or new line
- // - The colon has been removed
- // - There are no suggestions that match the search string
- if (this.$.textarea.selectionStart !==
- this._currentSearchString.length + this._colonIndex + 1 ||
- this._currentSearchString === ' ' ||
- this._currentSearchString === '\n' ||
- !(e.detail.value[this._colonIndex] === ':') ||
- !this._suggestions.length) {
- this._resetEmojiDropdown();
- // Otherwise open the dropdown and set the position to be just below the
- // cursor.
- } else if (this.$.emojiSuggestions.isHidden) {
- this._updateCaratPosition();
- }
- this.$.textarea.textarea.focus();
- }
-
- _openEmojiDropdown() {
- this.$.emojiSuggestions.open();
- this.$.reporting.reportInteraction('open-emoji-dropdown');
- }
-
- _formatSuggestions(matchedSuggestions) {
- const suggestions = [];
- for (const suggestion of matchedSuggestions) {
- suggestion.dataValue = suggestion.value;
- suggestion.text = suggestion.value + ' ' + suggestion.match;
- suggestions.push(suggestion);
- }
- this.set('_suggestions', suggestions);
- }
-
- _determineSuggestions(emojiText) {
- if (!emojiText.length) {
- this._formatSuggestions(ALL_SUGGESTIONS);
- this.disableEnterKeyForSelectingEmoji = true;
- } else {
- const matches = ALL_SUGGESTIONS
- .filter(suggestion => suggestion.match.includes(emojiText))
- .slice(0, MAX_ITEMS_DROPDOWN);
- this._formatSuggestions(matches);
- this.disableEnterKeyForSelectingEmoji = false;
- }
- }
-
- _resetEmojiDropdown() {
- // hide and reset the autocomplete dropdown.
- Polymer.dom.flush();
- this._currentSearchString = '';
- this._hideAutocomplete = true;
- this.closeDropdown();
- this._colonIndex = null;
- this.$.textarea.textarea.focus();
- }
-
- _handleTextChanged(text) {
- this.dispatchEvent(
- new CustomEvent('value-changed', {detail: {value: text}}));
+ if (this.hideBorder) {
+ this.$.textarea.classList.add('noBorder');
}
}
- customElements.define(GrTextarea.is, GrTextarea);
-})();
+ closeDropdown() {
+ return this.$.emojiSuggestions.close();
+ }
+
+ getNativeTextarea() {
+ return this.$.textarea.textarea;
+ }
+
+ putCursorAtEnd() {
+ const textarea = this.getNativeTextarea();
+ // Put the cursor at the end always.
+ textarea.selectionStart = textarea.value.length;
+ textarea.selectionEnd = textarea.selectionStart;
+ this.async(() => {
+ textarea.focus();
+ });
+ }
+
+ _handleEscKey(e) {
+ if (this._hideAutocomplete) { return; }
+ e.preventDefault();
+ e.stopPropagation();
+ this._resetEmojiDropdown();
+ }
+
+ _handleUpKey(e) {
+ if (this._hideAutocomplete) { return; }
+ e.preventDefault();
+ e.stopPropagation();
+ this.$.emojiSuggestions.cursorUp();
+ this.$.textarea.textarea.focus();
+ this.disableEnterKeyForSelectingEmoji = false;
+ }
+
+ _handleDownKey(e) {
+ if (this._hideAutocomplete) { return; }
+ e.preventDefault();
+ e.stopPropagation();
+ this.$.emojiSuggestions.cursorDown();
+ this.$.textarea.textarea.focus();
+ this.disableEnterKeyForSelectingEmoji = false;
+ }
+
+ _handleEnterByKey(e) {
+ if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ this._setEmoji(this.$.emojiSuggestions.getCurrentText());
+ }
+
+ _handleEmojiSelect(e) {
+ this._setEmoji(e.detail.selected.dataset.value);
+ }
+
+ _setEmoji(text) {
+ const colonIndex = this._colonIndex;
+ this.text = this._getText(text);
+ this.$.textarea.selectionStart = colonIndex + 1;
+ this.$.textarea.selectionEnd = colonIndex + 1;
+ this.$.reporting.reportInteraction('select-emoji', {type: text});
+ this._resetEmojiDropdown();
+ }
+
+ _getText(value) {
+ return this.text.substr(0, this._colonIndex || 0) +
+ value + this.text.substr(this.$.textarea.selectionStart);
+ }
+
+ /**
+ * Uses a hidden element with the same width and styling of the textarea and
+ * the text up until the point of interest. Then caratSpan element is added
+ * to the end and is set to be the positionTarget for the dropdown. Together
+ * this allows the dropdown to appear near where the user is typing.
+ */
+ _updateCaratPosition() {
+ this._hideAutocomplete = false;
+ this.$.hiddenText.textContent = this.$.textarea.value.substr(0,
+ this.$.textarea.selectionStart);
+
+ const caratSpan = this.$.caratSpan;
+ this.$.hiddenText.appendChild(caratSpan);
+ this.$.emojiSuggestions.positionTarget = caratSpan;
+ this._openEmojiDropdown();
+ }
+
+ _getFontSize() {
+ const fontSizePx = getComputedStyle(this).fontSize || '12px';
+ return parseInt(fontSizePx.substr(0, fontSizePx.length - 2),
+ 10);
+ }
+
+ _getScrollTop() {
+ return document.body.scrollTop;
+ }
+
+ /**
+ * _handleKeydown used for key handling in the this.$.textarea AND all child
+ * autocomplete options.
+ */
+ _onValueChanged(e) {
+ // Relay the event.
+ this.fire('bind-value-changed', e);
+
+ // If cursor is not in textarea (just opened with colon as last char),
+ // Don't do anything.
+ if (!e.currentTarget.focused) { return; }
+
+ const charAtCursor = e.detail && e.detail.value ?
+ e.detail.value[this.$.textarea.selectionStart - 1] : '';
+ if (charAtCursor !== ':' && this._colonIndex == null) { return; }
+
+ // When a colon is detected, set a colon index. We are interested only on
+ // colons after space or in beginning of textarea
+ if (charAtCursor === ':') {
+ if (this.$.textarea.selectionStart < 2 ||
+ e.detail.value[this.$.textarea.selectionStart - 2] === ' ') {
+ this._colonIndex = this.$.textarea.selectionStart - 1;
+ }
+ }
+
+ this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
+ this.$.textarea.selectionStart - this._colonIndex - 1);
+ // Under the following conditions, close and reset the dropdown:
+ // - The cursor is no longer at the end of the current search string
+ // - The search string is an space or new line
+ // - The colon has been removed
+ // - There are no suggestions that match the search string
+ if (this.$.textarea.selectionStart !==
+ this._currentSearchString.length + this._colonIndex + 1 ||
+ this._currentSearchString === ' ' ||
+ this._currentSearchString === '\n' ||
+ !(e.detail.value[this._colonIndex] === ':') ||
+ !this._suggestions.length) {
+ this._resetEmojiDropdown();
+ // Otherwise open the dropdown and set the position to be just below the
+ // cursor.
+ } else if (this.$.emojiSuggestions.isHidden) {
+ this._updateCaratPosition();
+ }
+ this.$.textarea.textarea.focus();
+ }
+
+ _openEmojiDropdown() {
+ this.$.emojiSuggestions.open();
+ this.$.reporting.reportInteraction('open-emoji-dropdown');
+ }
+
+ _formatSuggestions(matchedSuggestions) {
+ const suggestions = [];
+ for (const suggestion of matchedSuggestions) {
+ suggestion.dataValue = suggestion.value;
+ suggestion.text = suggestion.value + ' ' + suggestion.match;
+ suggestions.push(suggestion);
+ }
+ this.set('_suggestions', suggestions);
+ }
+
+ _determineSuggestions(emojiText) {
+ if (!emojiText.length) {
+ this._formatSuggestions(ALL_SUGGESTIONS);
+ this.disableEnterKeyForSelectingEmoji = true;
+ } else {
+ const matches = ALL_SUGGESTIONS
+ .filter(suggestion => suggestion.match.includes(emojiText))
+ .slice(0, MAX_ITEMS_DROPDOWN);
+ this._formatSuggestions(matches);
+ this.disableEnterKeyForSelectingEmoji = false;
+ }
+ }
+
+ _resetEmojiDropdown() {
+ // hide and reset the autocomplete dropdown.
+ flush();
+ this._currentSearchString = '';
+ this._hideAutocomplete = true;
+ this.closeDropdown();
+ this._colonIndex = null;
+ this.$.textarea.textarea.focus();
+ }
+
+ _handleTextChanged(text) {
+ this.dispatchEvent(
+ new CustomEvent('value-changed', {detail: {value: text}}));
+ }
+}
+
+customElements.define(GrTextarea.is, GrTextarea);
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
index 42a4f3b..99dd52d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
@@ -1,33 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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-autocomplete-dropdown/gr-autocomplete-dropdown.html">
-<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-
-<dom-module id="gr-textarea">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: flex;
@@ -82,27 +71,8 @@
hiddenText in order to correctly position the dropdown. After being moved,
it is set as the positionTarget for the emojiSuggestions dropdown. -->
<span id="caratSpan"></span>
- <gr-autocomplete-dropdown
- vertical-align="top"
- horizontal-align="left"
- dynamic-align
- id="emojiSuggestions"
- suggestions="[[_suggestions]]"
- index="[[_index]]"
- vertical-offset="[[_verticalOffset]]"
- on-dropdown-closed="_resetEmojiDropdown"
- on-item-selected="_handleEmojiSelect">
+ <gr-autocomplete-dropdown vertical-align="top" horizontal-align="left" dynamic-align="" id="emojiSuggestions" suggestions="[[_suggestions]]" index="[[_index]]" vertical-offset="[[_verticalOffset]]" on-dropdown-closed="_resetEmojiDropdown" on-item-selected="_handleEmojiSelect">
</gr-autocomplete-dropdown>
- <iron-autogrow-textarea
- id="textarea"
- autocomplete="[[autocomplete]]"
- placeholder=[[placeholder]]
- disabled="[[disabled]]"
- rows="[[rows]]"
- max-rows="[[maxRows]]"
- value="{{text}}"
- on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea>
+ <iron-autogrow-textarea id="textarea" autocomplete="[[autocomplete]]" placeholder="[[placeholder]]" disabled="[[disabled]]" rows="[[rows]]" max-rows="[[maxRows]]" value="{{text}}" on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea>
<gr-reporting id="reporting"></gr-reporting>
- </template>
- <script src="gr-textarea.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 674089d..9ede81c 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_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-textarea</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-textarea.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-textarea.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-textarea.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
<gr-textarea></gr-textarea>
@@ -46,16 +51,301 @@
</template>
</test-fixture>
-<script>
- suite('gr-textarea tests', async () => {
- await readyToTest();
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-textarea.js';
+suite('gr-textarea tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ sandbox.stub(element.$.reporting, 'reportInteraction');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('monospace is set properly', () => {
+ assert.isFalse(element.classList.contains('monospace'));
+ });
+
+ test('hideBorder is set properly', () => {
+ assert.isFalse(element.$.textarea.classList.contains('noBorder'));
+ });
+
+ test('emoji selector is not open with the textarea lacks focus', () => {
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
+ });
+
+ test('emoji selector is not open when a general text is entered', () => {
+ MockInteractions.focus(element.$.textarea);
+ element.$.textarea.selectionStart = 9;
+ element.$.textarea.selectionEnd = 9;
+ element.text = 'some text';
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
+ });
+
+ test('emoji selector opens when a colon is typed & the textarea has focus',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
+
+ test('emoji selector opens when a colon is typed after space',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 2;
+ element.$.textarea.selectionEnd = 2;
+ element.text = ' :';
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 1);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
+
+ test('emoji selector doesn\`t open when a colon is typed after character',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 5;
+ element.$.textarea.selectionEnd = 5;
+ element.text = 'test:';
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.emojiSuggestions.isHidden);
+ assert.isTrue(element._hideAutocomplete);
+ });
+
+ test('emoji selector opens when a colon is typed and some substring',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ element.$.textarea.selectionStart = 2;
+ element.$.textarea.selectionEnd = 2;
+ element.text = ':t';
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, 't');
+ });
+
+ test('emoji selector opens when a colon is typed in middle of text',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ // Since selectionStart is on Chrome set always on end of text, we
+ // stub it to 1
+ const text = ': hello';
+ sandbox.stub(element.$, 'textarea', {
+ selectionStart: 1,
+ value: text,
+ textarea: {
+ focus: () => {},
+ },
+ });
+ element.text = text;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
+ test('emoji selector closes when text changes before the colon', () => {
+ const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
+ MockInteractions.focus(element.$.textarea);
+ flushAsynchronousOperations();
+ element.$.textarea.selectionStart = 10;
+ element.$.textarea.selectionEnd = 10;
+ element.text = 'test test ';
+ element.$.textarea.selectionStart = 12;
+ element.$.textarea.selectionEnd = 12;
+ element.text = 'test test :';
+ element.$.textarea.selectionStart = 15;
+ element.$.textarea.selectionEnd = 15;
+ element.text = 'test test :smi';
+
+ assert.equal(element._currentSearchString, 'smi');
+ assert.isFalse(resetStub.called);
+ element.text = 'test test test :smi';
+ assert.isTrue(resetStub.called);
+ });
+
+ test('_resetEmojiDropdown', () => {
+ const closeSpy = sandbox.spy(element, 'closeDropdown');
+ element._resetEmojiDropdown();
+ assert.equal(element._currentSearchString, '');
+ assert.isTrue(element._hideAutocomplete);
+ assert.equal(element._colonIndex, null);
+
+ element.$.emojiSuggestions.open();
+ flushAsynchronousOperations();
+ element._resetEmojiDropdown();
+ assert.isTrue(closeSpy.called);
+ });
+
+ test('_determineSuggestions', () => {
+ const emojiText = 'tear';
+ const formatSpy = sandbox.spy(element, '_formatSuggestions');
+ element._determineSuggestions(emojiText);
+ assert.isTrue(formatSpy.called);
+ assert.isTrue(formatSpy.lastCall.calledWithExactly(
+ [{dataValue: '😂', value: '😂', match: 'tears :\')',
+ text: '😂 tears :\')'},
+ {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
+ ]));
+ });
+
+ test('_formatSuggestions', () => {
+ const matchedSuggestions = [{value: '😢', match: 'tear'},
+ {value: '😂', match: 'tears'}];
+ element._formatSuggestions(matchedSuggestions);
+ assert.deepEqual(
+ [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'},
+ {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}],
+ element._suggestions);
+ });
+
+ test('_handleEmojiSelect', () => {
+ element.$.textarea.selectionStart = 16;
+ element.$.textarea.selectionEnd = 16;
+ element.text = 'test test :tears';
+ element._colonIndex = 10;
+ const selectedItem = {dataset: {value: '😂'}};
+ const event = {detail: {selected: selectedItem}};
+ element._handleEmojiSelect(event);
+ assert.equal(element.text, 'test test 😂');
+ });
+
+ test('_updateCaratPosition', () => {
+ element.$.textarea.selectionStart = 4;
+ element.$.textarea.selectionEnd = 4;
+ element.text = 'test';
+ element._updateCaratPosition();
+ assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
+ element.$.caratSpan.outerHTML);
+ });
+
+ test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
+ const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
+ element.$.emojiSuggestions.fire('dropdown-closed');
+ assert.isTrue(resetSpy.called);
+ });
+
+ test('_onValueChanged fires bind-value-changed', () => {
+ const listenerStub = sinon.stub();
+ const eventObject = {currentTarget: {focused: false}};
+ element.addEventListener('bind-value-changed', listenerStub);
+ element._onValueChanged(eventObject);
+ assert.isTrue(listenerStub.called);
+ });
+
+ suite('keyboard shortcuts', () => {
+ function setupDropdown(callback) {
+ MockInteractions.focus(element.$.textarea);
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 2;
+ element.text = ':1';
+ flushAsynchronousOperations();
+ }
+
+ test('escape key', () => {
+ const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+ assert.isFalse(resetSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
+ assert.isTrue(resetSpy.called);
+ assert.isFalse(!element.$.emojiSuggestions.isHidden);
+ });
+
+ test('up key', () => {
+ const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+ assert.isFalse(upSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
+ assert.isTrue(upSpy.called);
+ });
+
+ test('down key', () => {
+ const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+ assert.isFalse(downSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
+ assert.isTrue(downSpy.called);
+ });
+
+ test('enter key', () => {
+ const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+ 'getCursorTarget');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
+ setupDropdown();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isTrue(enterSpy.called);
+ flushAsynchronousOperations();
+ assert.equal(element.text, '💯');
+ });
+
+ test('enter key - ignored on just colon without more information', () => {
+ const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+ 'getCursorTarget');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
+ MockInteractions.focus(element.$.textarea);
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
+ });
+ });
+
+ suite('gr-textarea monospace', () => {
+ // gr-textarea set monospace class in the ready() method.
+ // In Polymer2, ready() is called from the fixture(...) method,
+ // If ready() is called again later, some nested elements doesn't
+ // handle it correctly. A separate test-fixture is used to set
+ // properties before ready() is called.
+
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
- element = fixture('basic');
- sandbox.stub(element.$.reporting, 'reportInteraction');
+ element = fixture('monospace');
});
teardown(() => {
@@ -63,315 +353,32 @@
});
test('monospace is set properly', () => {
- assert.isFalse(element.classList.contains('monospace'));
+ assert.isTrue(element.classList.contains('monospace'));
+ });
+ });
+
+ suite('gr-textarea hideBorder', () => {
+ // gr-textarea set noBorder class in the ready() method.
+ // In Polymer2, ready() is called from the fixture(...) method,
+ // If ready() is called again later, some nested elements doesn't
+ // handle it correctly. A separate test-fixture is used to set
+ // properties before ready() is called.
+
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('hideBorder');
+ });
+
+ teardown(() => {
+ sandbox.restore();
});
test('hideBorder is set properly', () => {
- assert.isFalse(element.$.textarea.classList.contains('noBorder'));
- });
-
- test('emoji selector is not open with the textarea lacks focus', () => {
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- assert.isFalse(!element.$.emojiSuggestions.isHidden);
- });
-
- test('emoji selector is not open when a general text is entered', () => {
- MockInteractions.focus(element.$.textarea);
- element.$.textarea.selectionStart = 9;
- element.$.textarea.selectionEnd = 9;
- element.text = 'some text';
- assert.isFalse(!element.$.emojiSuggestions.isHidden);
- });
-
- test('emoji selector opens when a colon is typed & the textarea has focus',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, '');
- });
-
- test('emoji selector opens when a colon is typed after space',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 2;
- element.$.textarea.selectionEnd = 2;
- element.text = ' :';
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 1);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, '');
- });
-
- test('emoji selector doesn\`t open when a colon is typed after character',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 5;
- element.$.textarea.selectionEnd = 5;
- element.text = 'test:';
- flushAsynchronousOperations();
- assert.isTrue(element.$.emojiSuggestions.isHidden);
- assert.isTrue(element._hideAutocomplete);
- });
-
- test('emoji selector opens when a colon is typed and some substring',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- element.$.textarea.selectionStart = 2;
- element.$.textarea.selectionEnd = 2;
- element.text = ':t';
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, 't');
- });
-
- test('emoji selector opens when a colon is typed in middle of text',
- () => {
- MockInteractions.focus(element.$.textarea);
- // Needed for Safari tests. selectionStart is not updated when text is
- // updated.
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- // Since selectionStart is on Chrome set always on end of text, we
- // stub it to 1
- const text = ': hello';
- sandbox.stub(element.$, 'textarea', {
- selectionStart: 1,
- value: text,
- textarea: {
- focus: () => {},
- },
- });
- element.text = text;
- flushAsynchronousOperations();
- assert.isFalse(element.$.emojiSuggestions.isHidden);
- assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
- assert.equal(element._currentSearchString, '');
- });
- test('emoji selector closes when text changes before the colon', () => {
- const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
- MockInteractions.focus(element.$.textarea);
- flushAsynchronousOperations();
- element.$.textarea.selectionStart = 10;
- element.$.textarea.selectionEnd = 10;
- element.text = 'test test ';
- element.$.textarea.selectionStart = 12;
- element.$.textarea.selectionEnd = 12;
- element.text = 'test test :';
- element.$.textarea.selectionStart = 15;
- element.$.textarea.selectionEnd = 15;
- element.text = 'test test :smi';
-
- assert.equal(element._currentSearchString, 'smi');
- assert.isFalse(resetStub.called);
- element.text = 'test test test :smi';
- assert.isTrue(resetStub.called);
- });
-
- test('_resetEmojiDropdown', () => {
- const closeSpy = sandbox.spy(element, 'closeDropdown');
- element._resetEmojiDropdown();
- assert.equal(element._currentSearchString, '');
- assert.isTrue(element._hideAutocomplete);
- assert.equal(element._colonIndex, null);
-
- element.$.emojiSuggestions.open();
- flushAsynchronousOperations();
- element._resetEmojiDropdown();
- assert.isTrue(closeSpy.called);
- });
-
- test('_determineSuggestions', () => {
- const emojiText = 'tear';
- const formatSpy = sandbox.spy(element, '_formatSuggestions');
- element._determineSuggestions(emojiText);
- assert.isTrue(formatSpy.called);
- assert.isTrue(formatSpy.lastCall.calledWithExactly(
- [{dataValue: '😂', value: '😂', match: 'tears :\')',
- text: '😂 tears :\')'},
- {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
- ]));
- });
-
- test('_formatSuggestions', () => {
- const matchedSuggestions = [{value: '😢', match: 'tear'},
- {value: '😂', match: 'tears'}];
- element._formatSuggestions(matchedSuggestions);
- assert.deepEqual(
- [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'},
- {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}],
- element._suggestions);
- });
-
- test('_handleEmojiSelect', () => {
- element.$.textarea.selectionStart = 16;
- element.$.textarea.selectionEnd = 16;
- element.text = 'test test :tears';
- element._colonIndex = 10;
- const selectedItem = {dataset: {value: '😂'}};
- const event = {detail: {selected: selectedItem}};
- element._handleEmojiSelect(event);
- assert.equal(element.text, 'test test 😂');
- });
-
- test('_updateCaratPosition', () => {
- element.$.textarea.selectionStart = 4;
- element.$.textarea.selectionEnd = 4;
- element.text = 'test';
- element._updateCaratPosition();
- assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
- element.$.caratSpan.outerHTML);
- });
-
- test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
- const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
- element.$.emojiSuggestions.fire('dropdown-closed');
- assert.isTrue(resetSpy.called);
- });
-
- test('_onValueChanged fires bind-value-changed', () => {
- const listenerStub = sinon.stub();
- const eventObject = {currentTarget: {focused: false}};
- element.addEventListener('bind-value-changed', listenerStub);
- element._onValueChanged(eventObject);
- assert.isTrue(listenerStub.called);
- });
-
- suite('keyboard shortcuts', () => {
- function setupDropdown(callback) {
- MockInteractions.focus(element.$.textarea);
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 2;
- element.text = ':1';
- flushAsynchronousOperations();
- }
-
- test('escape key', () => {
- const resetSpy = sandbox.spy(element, '_resetEmojiDropdown');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
- assert.isFalse(resetSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27);
- assert.isTrue(resetSpy.called);
- assert.isFalse(!element.$.emojiSuggestions.isHidden);
- });
-
- test('up key', () => {
- const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
- assert.isFalse(upSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38);
- assert.isTrue(upSpy.called);
- });
-
- test('down key', () => {
- const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
- assert.isFalse(downSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40);
- assert.isTrue(downSpy.called);
- });
-
- test('enter key', () => {
- const enterSpy = sandbox.spy(element.$.emojiSuggestions,
- 'getCursorTarget');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isFalse(enterSpy.called);
- setupDropdown();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isTrue(enterSpy.called);
- flushAsynchronousOperations();
- assert.equal(element.text, '💯');
- });
-
- test('enter key - ignored on just colon without more information', () => {
- const enterSpy = sandbox.spy(element.$.emojiSuggestions,
- 'getCursorTarget');
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isFalse(enterSpy.called);
- MockInteractions.focus(element.$.textarea);
- element.$.textarea.selectionStart = 1;
- element.$.textarea.selectionEnd = 1;
- element.text = ':';
- flushAsynchronousOperations();
- MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
- assert.isFalse(enterSpy.called);
- });
- });
-
- suite('gr-textarea monospace', () => {
- // gr-textarea set monospace class in the ready() method.
- // In Polymer2, ready() is called from the fixture(...) method,
- // If ready() is called again later, some nested elements doesn't
- // handle it correctly. A separate test-fixture is used to set
- // properties before ready() is called.
-
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('monospace');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('monospace is set properly', () => {
- assert.isTrue(element.classList.contains('monospace'));
- });
- });
-
- suite('gr-textarea hideBorder', () => {
- // gr-textarea set noBorder class in the ready() method.
- // In Polymer2, ready() is called from the fixture(...) method,
- // If ready() is called again later, some nested elements doesn't
- // handle it correctly. A separate test-fixture is used to set
- // properties before ready() is called.
-
- let element;
- let sandbox;
-
- setup(() => {
- sandbox = sinon.sandbox.create();
- element = fixture('hideBorder');
- });
-
- teardown(() => {
- sandbox.restore();
- });
-
- test('hideBorder is set properly', () => {
- assert.isTrue(element.$.textarea.classList.contains('noBorder'));
- });
+ assert.isTrue(element.$.textarea.classList.contains('noBorder'));
});
});
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
index baa0fc9..3c9181f 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -14,33 +14,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-(function() {
- 'use strict';
+import '../../../scripts/bundled-polymer.js';
- /**
- * @appliesMixin Gerrit.TooltipMixin
- * @extends Polymer.Element
- */
- class GrTooltipContent extends Polymer.mixinBehaviors( [
- Gerrit.TooltipBehavior,
- ], Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element))) {
- static get is() { return 'gr-tooltip-content'; }
+import '../gr-icons/gr-icons.js';
+import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.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-tooltip-content_html.js';
- static get properties() {
- return {
- maxWidth: {
- type: String,
- reflectToAttribute: true,
- },
- showIcon: {
- type: Boolean,
- value: false,
- },
- };
- }
+/**
+ * @appliesMixin Gerrit.TooltipMixin
+ * @extends Polymer.Element
+ */
+class GrTooltipContent extends mixinBehaviors( [
+ Gerrit.TooltipBehavior,
+], GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement))) {
+ static get template() { return htmlTemplate; }
+
+ static get is() { return 'gr-tooltip-content'; }
+
+ static get properties() {
+ return {
+ maxWidth: {
+ type: String,
+ reflectToAttribute: true,
+ },
+ showIcon: {
+ type: Boolean,
+ value: false,
+ },
+ };
}
+}
- customElements.define(GrTooltipContent.is, GrTooltipContent);
-})();
+customElements.define(GrTooltipContent.is, GrTooltipContent);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
index ec56912..e4b5891 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
@@ -1,26 +1,22 @@
-<!--
-@license
-Copyright (C) 2017 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="../gr-icons/gr-icons.html">
-<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
-
-<dom-module id="gr-tooltip-content">
- <template>
+export const htmlTemplate = html`
<style>
iron-icon {
width: var(--line-height-normal);
@@ -29,7 +25,5 @@
}
</style>
<slot></slot><!--
- --><iron-icon icon="gr-icons:info" hidden$="[[!showIcon]]"></iron-icon>
- </template>
- <script src="gr-tooltip-content.js"></script>
-</dom-module>
+ --><iron-icon icon="gr-icons:info" hidden\$="[[!showIcon]]"></iron-icon>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
index 8237552..853f4c2 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_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-storage</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-tooltip-content.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-tooltip-content.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-tooltip-content.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,29 +40,32 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip-content tests', async () => {
- await readyToTest();
- let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('icon is not visible by default', () => {
- assert.equal(Polymer.dom(element.root)
- .querySelector('iron-icon').hidden, true);
- });
-
- test('position-below attribute is reflected', () => {
- assert.isFalse(element.hasAttribute('position-below'));
- element.positionBelow = true;
- assert.isTrue(element.hasAttribute('position-below'));
- });
-
- test('icon is visible with showIcon property', () => {
- element.showIcon = true;
- assert.equal(Polymer.dom(element.root)
- .querySelector('iron-icon').hidden, false);
- });
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-tooltip-content.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+suite('gr-tooltip-content tests', () => {
+ let element;
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('icon is not visible by default', () => {
+ assert.equal(dom(element.root)
+ .querySelector('iron-icon').hidden, true);
+ });
+
+ test('position-below attribute is reflected', () => {
+ assert.isFalse(element.hasAttribute('position-below'));
+ element.positionBelow = true;
+ assert.isTrue(element.hasAttribute('position-below'));
+ });
+
+ test('icon is visible with showIcon property', () => {
+ element.showIcon = true;
+ assert.equal(dom(element.root)
+ .querySelector('iron-icon').hidden, false);
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index 6f458d1..0cd2d7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -14,33 +14,39 @@
* 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 GrTooltip extends Polymer.GestureEventListeners(
- Polymer.LegacyElementMixin(
- Polymer.Element)) {
- static get is() { return 'gr-tooltip'; }
+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-tooltip_html.js';
- static get properties() {
- return {
- text: String,
- maxWidth: {
- type: String,
- observer: '_updateWidth',
- },
- positionBelow: {
- type: Boolean,
- reflectToAttribute: true,
- },
- };
- }
+/** @extends Polymer.Element */
+class GrTooltip extends GestureEventListeners(
+ LegacyElementMixin(
+ PolymerElement)) {
+ static get template() { return htmlTemplate; }
- _updateWidth(maxWidth) {
- this.updateStyles({'--tooltip-max-width': maxWidth});
- }
+ static get is() { return 'gr-tooltip'; }
+
+ static get properties() {
+ return {
+ text: String,
+ maxWidth: {
+ type: String,
+ observer: '_updateWidth',
+ },
+ positionBelow: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ };
}
- customElements.define(GrTooltip.is, GrTooltip);
-})();
+ _updateWidth(maxWidth) {
+ this.updateStyles({'--tooltip-max-width': maxWidth});
+ }
+}
+
+customElements.define(GrTooltip.is, GrTooltip);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
index d78d554..5f9ce51 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
@@ -1,25 +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="../../../styles/shared-styles.html">
-
-<dom-module id="gr-tooltip">
- <template>
+export const htmlTemplate = html`
<style include="shared-styles">
:host {
--gr-tooltip-arrow-size: .5em;
@@ -66,6 +63,4 @@
[[text]]
<i class="arrowPositionAbove arrow"></i>
</div>
- </template>
- <script src="gr-tooltip.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
index 4c9b954..be5e26e 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_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-storage</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-tooltip.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-tooltip.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-tooltip.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -35,35 +40,37 @@
</template>
</test-fixture>
-<script>
- suite('gr-tooltip tests', async () => {
- await readyToTest();
- let element;
- setup(() => {
- element = fixture('basic');
- });
-
- test('max-width is respected if set', () => {
- element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
- ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
- element.maxWidth = '50px';
- assert.equal(getComputedStyle(element).width, '50px');
- });
-
- test('the correct arrow is displayed', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionBelow')).display,
- 'none');
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionAbove'))
- .display, 'none');
- element.positionBelow = true;
- assert.notEqual(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionBelow'))
- .display, 'none');
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.arrowPositionAbove'))
- .display, 'none');
- });
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './gr-tooltip.js';
+suite('gr-tooltip tests', () => {
+ let element;
+ setup(() => {
+ element = fixture('basic');
});
+
+ test('max-width is respected if set', () => {
+ element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
+ ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
+ element.maxWidth = '50px';
+ assert.equal(getComputedStyle(element).width, '50px');
+ });
+
+ test('the correct arrow is displayed', () => {
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionBelow')).display,
+ 'none');
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionAbove'))
+ .display, 'none');
+ element.positionBelow = true;
+ assert.notEqual(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionBelow'))
+ .display, 'none');
+ assert.equal(getComputedStyle(element.shadowRoot
+ .querySelector('.arrowPositionAbove'))
+ .display, 'none');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.js b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
index 239e0fa..f89234f 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
@@ -1,88 +1,83 @@
-<!--
-@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.
+ */
+import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.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
+/**
+ * @constructor
+ * @param {Object} change A change object resulting from a change detail
+ * call that includes revision information.
+ */
+function RevisionInfo(change) {
+ this._change = change;
+}
-http://www.apache.org/licenses/LICENSE-2.0
+/**
+ * Get the largest number of parents of the commit in any revision. For
+ * example, with normal changes this will always return 1. For merge changes
+ * wherein the revisions are merge commits this will return 2 or potentially
+ * more.
+ *
+ * @return {number}
+ */
+RevisionInfo.prototype.getMaxParents = function() {
+ if (!this._change || !this._change.revisions) {
+ return 0;
+ }
+ return Object.values(this._change.revisions)
+ .reduce((acc, rev) => Math.max(rev.commit.parents.length, acc), 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-patch-set-behavior/gr-patch-set-behavior.html">
-<script>
- (function() {
- 'use strict';
+/**
+ * Get an object that maps revision numbers to the number of parents of the
+ * commit of that revision.
+ *
+ * @return {!Object}
+ */
+RevisionInfo.prototype.getParentCountMap = function() {
+ const result = {};
+ if (!this._change || !this._change.revisions) {
+ return {};
+ }
+ Object.values(this._change.revisions)
+ .forEach(rev => { result[rev._number] = rev.commit.parents.length; });
+ return result;
+};
- /**
- * @constructor
- * @param {Object} change A change object resulting from a change detail
- * call that includes revision information.
- */
- function RevisionInfo(change) {
- this._change = change;
- }
+/**
+ * @param {number|string} patchNum
+ * @return {number}
+ */
+RevisionInfo.prototype.getParentCount = function(patchNum) {
+ return this.getParentCountMap()[patchNum];
+};
- /**
- * Get the largest number of parents of the commit in any revision. For
- * example, with normal changes this will always return 1. For merge changes
- * wherein the revisions are merge commits this will return 2 or potentially
- * more.
- *
- * @return {number}
- */
- RevisionInfo.prototype.getMaxParents = function() {
- if (!this._change || !this._change.revisions) {
- return 0;
- }
- return Object.values(this._change.revisions)
- .reduce((acc, rev) => Math.max(rev.commit.parents.length, acc), 0);
- };
+/**
+ * Get the commit ID of the (0-offset) indexed parent in the given revision
+ * number.
+ *
+ * @param {number|string} patchNum
+ * @param {number} parentIndex (0-offset)
+ * @return {string}
+ */
+RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) {
+ const rev = Object.values(this._change.revisions).find(rev =>
+ Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum));
+ return rev.commit.parents[parentIndex].commit;
+};
- /**
- * Get an object that maps revision numbers to the number of parents of the
- * commit of that revision.
- *
- * @return {!Object}
- */
- RevisionInfo.prototype.getParentCountMap = function() {
- const result = {};
- if (!this._change || !this._change.revisions) {
- return {};
- }
- Object.values(this._change.revisions)
- .forEach(rev => { result[rev._number] = rev.commit.parents.length; });
- return result;
- };
-
- /**
- * @param {number|string} patchNum
- * @return {number}
- */
- RevisionInfo.prototype.getParentCount = function(patchNum) {
- return this.getParentCountMap()[patchNum];
- };
-
- /**
- * Get the commit ID of the (0-offset) indexed parent in the given revision
- * number.
- *
- * @param {number|string} patchNum
- * @param {number} parentIndex (0-offset)
- * @return {string}
- */
- RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) {
- const rev = Object.values(this._change.revisions).find(rev =>
- Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum));
- return rev.commit.parents[parentIndex].commit;
- };
-
- window.Gerrit = window.Gerrit || {};
- window.Gerrit.RevisionInfo = RevisionInfo;
- })();
-</script>
+window.Gerrit = window.Gerrit || {};
+window.Gerrit.RevisionInfo = RevisionInfo;
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
index fb7a011..4946e2e 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
@@ -19,72 +19,74 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>revision-info</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="revision-info.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="./revision-info.js"></script>
-<script>
- suite('revision-info tests', async () => {
- await readyToTest();
- let mockChange;
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import './revision-info.js';
+suite('revision-info tests', () => {
+ let mockChange;
- setup(() => {
- mockChange = {
- revisions: {
- r1: {_number: 1, commit: {parents: [
- {commit: 'p1'},
- {commit: 'p2'},
- {commit: 'p3'},
- ]}},
- r2: {_number: 2, commit: {parents: [
- {commit: 'p1'},
- {commit: 'p4'},
- ]}},
- r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}},
- r4: {_number: 4, commit: {parents: [
- {commit: 'p2'},
- {commit: 'p3'},
- ]}},
- r5: {_number: 5, commit: {parents: [
- {commit: 'p5'},
- {commit: 'p2'},
- {commit: 'p3'},
- ]}},
- },
- };
- });
-
- test('getMaxParents', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.equal(ri.getMaxParents(), 3);
- });
-
- test('getParentCountMap', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3});
- });
-
- test('getParentCount', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentCount(1), 3);
- assert.deepEqual(ri.getParentCount(3), 1);
- });
-
- test('getParentCount', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentCount(1), 3);
- assert.deepEqual(ri.getParentCount(3), 1);
- });
-
- test('getParentId', () => {
- const ri = new window.Gerrit.RevisionInfo(mockChange);
- assert.deepEqual(ri.getParentId(1, 2), 'p3');
- assert.deepEqual(ri.getParentId(2, 1), 'p4');
- assert.deepEqual(ri.getParentId(3, 0), 'p5');
- });
+ setup(() => {
+ mockChange = {
+ revisions: {
+ r1: {_number: 1, commit: {parents: [
+ {commit: 'p1'},
+ {commit: 'p2'},
+ {commit: 'p3'},
+ ]}},
+ r2: {_number: 2, commit: {parents: [
+ {commit: 'p1'},
+ {commit: 'p4'},
+ ]}},
+ r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}},
+ r4: {_number: 4, commit: {parents: [
+ {commit: 'p2'},
+ {commit: 'p3'},
+ ]}},
+ r5: {_number: 5, commit: {parents: [
+ {commit: 'p5'},
+ {commit: 'p2'},
+ {commit: 'p3'},
+ ]}},
+ },
+ };
});
+
+ test('getMaxParents', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.equal(ri.getMaxParents(), 3);
+ });
+
+ test('getParentCountMap', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3});
+ });
+
+ test('getParentCount', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentCount(1), 3);
+ assert.deepEqual(ri.getParentCount(3), 1);
+ });
+
+ test('getParentCount', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentCount(1), 3);
+ assert.deepEqual(ri.getParentCount(3), 1);
+ });
+
+ test('getParentId', () => {
+ const ri = new window.Gerrit.RevisionInfo(mockChange);
+ assert.deepEqual(ri.getParentId(1, 2), 'p3');
+ assert.deepEqual(ri.getParentId(2, 1), 'p4');
+ assert.deepEqual(ri.getParentId(3, 0), 'p5');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
index 0ab41de..b9ddac62 100644
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
+++ b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.html
@@ -19,123 +19,125 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-display-name-utils</title>
-<script src="/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="gr-display-name-utils.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 type="module" src="./gr-display-name-utils.js"></script>
-<script>
- suite('gr-display-name-utils tests', async () => {
- await readyToTest();
- // eslint-disable-next-line no-unused-vars
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import './gr-display-name-utils.js';
+suite('gr-display-name-utils tests', () => {
+ // eslint-disable-next-line no-unused-vars
+ const config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward',
+ },
+ };
+
+ test('getUserName name only', () => {
+ const account = {
+ name: 'test-name',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ 'test-name');
+ });
+
+ test('getUserName username only', () => {
+ const account = {
+ username: 'test-user',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ 'test-user');
+ });
+
+ test('getUserName email only', () => {
+ const account = {
+ email: 'test-user@test-url.com',
+ };
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
+ 'test-user@test-url.com');
+ });
+
+ test('getUserName returns not Anonymous Coward as the anon name', () => {
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+ 'Anonymous');
+ });
+
+ test('getUserName for the config returning the anon name', () => {
const config = {
user: {
- anonymous_coward_name: 'Anonymous Coward',
+ anonymous_coward_name: 'Test Anon',
},
};
-
- test('getUserName name only', () => {
- const account = {
- name: 'test-name',
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
- 'test-name');
- });
-
- test('getUserName username only', () => {
- const account = {
- username: 'test-user',
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
- 'test-user');
- });
-
- test('getUserName email only', () => {
- const account = {
- email: 'test-user@test-url.com',
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, account, true),
- 'test-user@test-url.com');
- });
-
- test('getUserName returns not Anonymous Coward as the anon name', () => {
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
- 'Anonymous');
- });
-
- test('getUserName for the config returning the anon name', () => {
- const config = {
- user: {
- anonymous_coward_name: 'Test Anon',
- },
- };
- assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
- 'Test Anon');
- });
-
- test('getAccountDisplayName - account with name only', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config,
- {name: 'Some user name'}),
- 'Some user name');
- });
-
- test('getAccountDisplayName - account with email only', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config,
- {email: 'my@example.com'}),
- 'Anonymous <my@example.com>');
- });
-
- test('getAccountDisplayName - account with email only - allowEmail', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config,
- {email: 'my@example.com'}, true),
- 'my@example.com <my@example.com>');
- });
-
- test('getAccountDisplayName - account with name and status', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config, {
- name: 'Some name',
- status: 'OOO',
- }),
- 'Some name (OOO)');
- });
-
- test('getAccountDisplayName - account with name and email', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config, {
- name: 'Some name',
- email: 'my@example.com',
- }),
- 'Some name <my@example.com>');
- });
-
- test('getAccountDisplayName - account with name, email and status', () => {
- assert.equal(
- GrDisplayNameUtils.getAccountDisplayName(config, {
- name: 'Some name',
- email: 'my@example.com',
- status: 'OOO',
- }),
- 'Some name <my@example.com> (OOO)');
- });
-
- test('getGroupDisplayName', () => {
- assert.equal(
- GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
- 'Some user name (group)');
- });
-
- test('_accountEmail', () => {
- assert.equal(
- GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
- '<email@gerritreview.com>');
- assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
- });
+ assert.deepEqual(GrDisplayNameUtils.getUserName(config, null, true),
+ 'Test Anon');
});
+
+ test('getAccountDisplayName - account with name only', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {name: 'Some user name'}),
+ 'Some user name');
+ });
+
+ test('getAccountDisplayName - account with email only', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {email: 'my@example.com'}),
+ 'Anonymous <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with email only - allowEmail', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config,
+ {email: 'my@example.com'}, true),
+ 'my@example.com <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with name and status', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ status: 'OOO',
+ }),
+ 'Some name (OOO)');
+ });
+
+ test('getAccountDisplayName - account with name and email', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com',
+ }),
+ 'Some name <my@example.com>');
+ });
+
+ test('getAccountDisplayName - account with name, email and status', () => {
+ assert.equal(
+ GrDisplayNameUtils.getAccountDisplayName(config, {
+ name: 'Some name',
+ email: 'my@example.com',
+ status: 'OOO',
+ }),
+ 'Some name <my@example.com> (OOO)');
+ });
+
+ test('getGroupDisplayName', () => {
+ assert.equal(
+ GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
+ 'Some user name (group)');
+ });
+
+ test('_accountEmail', () => {
+ assert.equal(
+ GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
+ '<email@gerritreview.com>');
+ assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
index f64d9ef..be47863 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.html
@@ -19,18 +19,25 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-email-suggestions-provider</title>
-<script src="/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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
-<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="gr-email-suggestions-provider.js"></script>
+<script 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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js"></script>
+<script type="module" src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script type="module" src="./gr-email-suggestions-provider.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-email-suggestions-provider.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -38,64 +45,68 @@
</template>
</test-fixture>
-<script>
- suite('GrEmailSuggestionsProvider tests', async () => {
- await readyToTest();
- let sandbox;
- let restAPI;
- let provider;
- const account1 = {
- name: 'Some name',
- email: 'some@example.com',
- };
- const account2 = {
- email: 'other@example.com',
- _account_id: 3,
- };
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-email-suggestions-provider.js';
+suite('GrEmailSuggestionsProvider tests', () => {
+ let sandbox;
+ let restAPI;
+ let provider;
+ const account1 = {
+ name: 'Some name',
+ email: 'some@example.com',
+ };
+ const account2 = {
+ email: 'other@example.com',
+ _account_id: 3,
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- restAPI = fixture('basic');
- provider = new GrEmailSuggestionsProvider(restAPI);
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
+ restAPI = fixture('basic');
+ provider = new GrEmailSuggestionsProvider(restAPI);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('getSuggestions', done => {
- const getSuggestedAccountsStub =
- sandbox.stub(restAPI, 'getSuggestedAccounts')
- .returns(Promise.resolve([account1, account2]));
+ test('getSuggestions', done => {
+ const getSuggestedAccountsStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([account1, account2]));
- provider.getSuggestions('Some input').then(res => {
- assert.deepEqual(res, [account1, account2]);
- assert.isTrue(getSuggestedAccountsStub.calledOnce);
- assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
- done();
- });
- });
-
- test('makeSuggestionItem', () => {
- assert.deepEqual(provider.makeSuggestionItem(account1), {
- name: 'Some name <some@example.com>',
- value: {
- account: account1,
- count: 1,
- },
- });
-
- assert.deepEqual(provider.makeSuggestionItem(account2), {
- name: 'other@example.com <other@example.com>',
- value: {
- account: account2,
- count: 1,
- },
- });
+ provider.getSuggestions('Some input').then(res => {
+ assert.deepEqual(res, [account1, account2]);
+ assert.isTrue(getSuggestedAccountsStub.calledOnce);
+ assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+ done();
});
});
+
+ test('makeSuggestionItem', () => {
+ assert.deepEqual(provider.makeSuggestionItem(account1), {
+ name: 'Some name <some@example.com>',
+ value: {
+ account: account1,
+ count: 1,
+ },
+ });
+
+ assert.deepEqual(provider.makeSuggestionItem(account2), {
+ name: 'other@example.com <other@example.com>',
+ value: {
+ account: account2,
+ count: 1,
+ },
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
index c46b443..21c5085 100644
--- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.html
@@ -19,17 +19,24 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group-suggestions-provider</title>
-<script src="/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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
-<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="gr-group-suggestions-provider.js"></script>
+<script 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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js"></script>
+<script type="module" src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script type="module" src="./gr-group-suggestions-provider.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-group-suggestions-provider.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,72 +44,76 @@
</template>
</test-fixture>
-<script>
- suite('GrGroupSuggestionsProvider tests', async () => {
- await readyToTest();
- let sandbox;
- let restAPI;
- let provider;
- const group1 = {
- name: 'Some name',
- id: 1,
- };
- const group2 = {
- name: 'Other name',
- id: 3,
- url: 'abcd',
- };
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-group-suggestions-provider.js';
+suite('GrGroupSuggestionsProvider tests', () => {
+ let sandbox;
+ let restAPI;
+ let provider;
+ const group1 = {
+ name: 'Some name',
+ id: 1,
+ };
+ const group2 = {
+ name: 'Other name',
+ id: 3,
+ url: 'abcd',
+ };
- setup(() => {
- sandbox = sinon.sandbox.create();
+ setup(() => {
+ sandbox = sinon.sandbox.create();
- stub('gr-rest-api-interface', {
- getConfig() { return Promise.resolve({}); },
- });
- restAPI = fixture('basic');
- provider = new GrGroupSuggestionsProvider(restAPI);
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
});
+ restAPI = fixture('basic');
+ provider = new GrGroupSuggestionsProvider(restAPI);
+ });
- teardown(() => {
- sandbox.restore();
- });
+ teardown(() => {
+ sandbox.restore();
+ });
- test('getSuggestions', done => {
- const getSuggestedAccountsStub =
- sandbox.stub(restAPI, 'getSuggestedGroups')
- .returns(Promise.resolve({
- 'Some name': {id: 1},
- 'Other name': {id: 3, url: 'abcd'},
- }));
+ test('getSuggestions', done => {
+ const getSuggestedAccountsStub =
+ sandbox.stub(restAPI, 'getSuggestedGroups')
+ .returns(Promise.resolve({
+ 'Some name': {id: 1},
+ 'Other name': {id: 3, url: 'abcd'},
+ }));
- provider.getSuggestions('Some input').then(res => {
- assert.deepEqual(res, [group1, group2]);
- assert.isTrue(getSuggestedAccountsStub.calledOnce);
- assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
- done();
- });
- });
-
- test('makeSuggestionItem', () => {
- assert.deepEqual(provider.makeSuggestionItem(group1), {
- name: 'Some name',
- value: {
- group: {
- name: 'Some name',
- id: 1,
- },
- },
- });
-
- assert.deepEqual(provider.makeSuggestionItem(group2), {
- name: 'Other name',
- value: {
- group: {
- name: 'Other name',
- id: 3,
- },
- },
- });
+ provider.getSuggestions('Some input').then(res => {
+ assert.deepEqual(res, [group1, group2]);
+ assert.isTrue(getSuggestedAccountsStub.calledOnce);
+ assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
+ done();
});
});
+
+ test('makeSuggestionItem', () => {
+ assert.deepEqual(provider.makeSuggestionItem(group1), {
+ name: 'Some name',
+ value: {
+ group: {
+ name: 'Some name',
+ id: 1,
+ },
+ },
+ });
+
+ assert.deepEqual(provider.makeSuggestionItem(group2), {
+ name: 'Other name',
+ value: {
+ group: {
+ name: 'Other name',
+ id: 3,
+ },
+ },
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
index 9696c2e..7821386 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.html
@@ -19,17 +19,24 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-suggestions-provider</title>
-<script src="/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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.html"/>
-<script src="../gr-display-name-utils/gr-display-name-utils.js"></script>
-<script src="gr-reviewer-suggestions-provider.js"></script>
+<script 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="../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js"></script>
+<script type="module" src="../gr-display-name-utils/gr-display-name-utils.js"></script>
+<script type="module" src="./gr-reviewer-suggestions-provider.js"></script>
-<script>void(0);</script>
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-reviewer-suggestions-provider.js';
+void(0);
+</script>
<test-fixture id="basic">
<template>
@@ -37,227 +44,231 @@
</template>
</test-fixture>
-<script>
- suite('GrReviewerSuggestionsProvider tests', async () => {
- await readyToTest();
- let sandbox;
- let _nextAccountId = 0;
- const makeAccount = function(opt_status) {
- const accountId = ++_nextAccountId;
- return {
- _account_id: accountId,
- name: 'name ' + accountId,
- email: 'email ' + accountId,
- status: opt_status,
- };
+<script type="module">
+import '../../test/test-pre-setup.js';
+import '../../test/common-test-setup.js';
+import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-display-name-utils/gr-display-name-utils.js';
+import './gr-reviewer-suggestions-provider.js';
+suite('GrReviewerSuggestionsProvider tests', () => {
+ let sandbox;
+ let _nextAccountId = 0;
+ const makeAccount = function(opt_status) {
+ const accountId = ++_nextAccountId;
+ return {
+ _account_id: accountId,
+ name: 'name ' + accountId,
+ email: 'email ' + accountId,
+ status: opt_status,
};
- let _nextAccountId2 = 0;
- const makeAccount2 = function(opt_status) {
- const accountId2 = ++_nextAccountId2;
- return {
- _account_id: accountId2,
- name: 'name ' + accountId2,
- status: opt_status,
- };
+ };
+ let _nextAccountId2 = 0;
+ const makeAccount2 = function(opt_status) {
+ const accountId2 = ++_nextAccountId2;
+ return {
+ _account_id: accountId2,
+ name: 'name ' + accountId2,
+ status: opt_status,
+ };
+ };
+
+ let owner;
+ let existingReviewer1;
+ let existingReviewer2;
+ let suggestion1;
+ let suggestion2;
+ let suggestion3;
+ let restAPI;
+ let provider;
+
+ let redundantSuggestion1;
+ let redundantSuggestion2;
+ let redundantSuggestion3;
+ let change;
+
+ setup(done => {
+ owner = makeAccount();
+ existingReviewer1 = makeAccount();
+ existingReviewer2 = makeAccount();
+ suggestion1 = {account: makeAccount()};
+ suggestion2 = {account: makeAccount()};
+ suggestion3 = {
+ group: {
+ id: 'suggested group id',
+ name: 'suggested group',
+ },
};
- let owner;
- let existingReviewer1;
- let existingReviewer2;
- let suggestion1;
- let suggestion2;
- let suggestion3;
- let restAPI;
- let provider;
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(true); },
+ getConfig() { return Promise.resolve({}); },
+ });
- let redundantSuggestion1;
- let redundantSuggestion2;
- let redundantSuggestion3;
- let change;
+ restAPI = fixture('basic');
+ change = {
+ _number: 42,
+ owner,
+ reviewers: {
+ CC: [existingReviewer1],
+ REVIEWER: [existingReviewer2],
+ },
+ };
+ sandbox = sinon.sandbox.create();
+ return flush(done);
+ });
+ teardown(() => {
+ sandbox.restore();
+ });
+ suite('allowAnyUser set to false', () => {
setup(done => {
- owner = makeAccount();
- existingReviewer1 = makeAccount();
- existingReviewer2 = makeAccount();
- suggestion1 = {account: makeAccount()};
- suggestion2 = {account: makeAccount()};
- suggestion3 = {
- group: {
- id: 'suggested group id',
- name: 'suggested group',
- },
- };
-
- stub('gr-rest-api-interface', {
- getLoggedIn() { return Promise.resolve(true); },
- getConfig() { return Promise.resolve({}); },
+ provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+ Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
+ provider.init().then(done);
+ });
+ suite('stubbed values for _getReviewerSuggestions', () => {
+ setup(() => {
+ stub('gr-rest-api-interface', {
+ getChangeSuggestedReviewers() {
+ redundantSuggestion1 = {account: existingReviewer1};
+ redundantSuggestion2 = {account: existingReviewer2};
+ redundantSuggestion3 = {account: owner};
+ return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
+ redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+ },
+ });
});
- restAPI = fixture('basic');
- change = {
- _number: 42,
- owner,
- reviewers: {
- CC: [existingReviewer1],
- REVIEWER: [existingReviewer2],
- },
- };
- sandbox = sinon.sandbox.create();
- return flush(done);
- });
+ test('makeSuggestionItem formats account or group accordingly', () => {
+ let account = makeAccount();
+ const account3 = makeAccount2();
+ let suggestion = provider.makeSuggestionItem({account});
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '>',
+ value: {account},
+ });
- teardown(() => {
- sandbox.restore();
- });
- suite('allowAnyUser set to false', () => {
- setup(done => {
- provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
- Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
- provider.init().then(done);
+ const group = {name: 'test'};
+ suggestion = provider.makeSuggestionItem({group});
+ assert.deepEqual(suggestion, {
+ name: group.name + ' (group)',
+ value: {group},
+ });
+
+ suggestion = provider.makeSuggestionItem(account);
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '>',
+ value: {account, count: 1},
+ });
+
+ suggestion = provider.makeSuggestionItem({account: {}});
+ assert.deepEqual(suggestion, {
+ name: 'Anonymous',
+ value: {account: {}},
+ });
+
+ provider._config = {
+ user: {
+ anonymous_coward_name: 'Anonymous Coward Name',
+ },
+ };
+
+ suggestion = provider.makeSuggestionItem({account: {}});
+ assert.deepEqual(suggestion, {
+ name: 'Anonymous Coward Name',
+ value: {account: {}},
+ });
+
+ account = makeAccount('OOO');
+
+ suggestion = provider.makeSuggestionItem({account});
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '> (OOO)',
+ value: {account},
+ });
+
+ suggestion = provider.makeSuggestionItem(account);
+ assert.deepEqual(suggestion, {
+ name: account.name + ' <' + account.email + '> (OOO)',
+ value: {account, count: 1},
+ });
+
+ sandbox.stub(GrDisplayNameUtils, '_accountEmail',
+ () => '');
+
+ suggestion = provider.makeSuggestionItem(account3);
+ assert.deepEqual(suggestion, {
+ name: account3.name,
+ value: {account: account3, count: 1},
+ });
});
- suite('stubbed values for _getReviewerSuggestions', () => {
- setup(() => {
- stub('gr-rest-api-interface', {
- getChangeSuggestedReviewers() {
- redundantSuggestion1 = {account: existingReviewer1};
- redundantSuggestion2 = {account: existingReviewer2};
- redundantSuggestion3 = {account: owner};
- return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
- redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
- },
- });
- });
- test('makeSuggestionItem formats account or group accordingly', () => {
- let account = makeAccount();
- const account3 = makeAccount2();
- let suggestion = provider.makeSuggestionItem({account});
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '>',
- value: {account},
- });
+ test('getSuggestions', done => {
+ provider.getSuggestions()
+ .then(reviewers => {
+ // Default is no filtering.
+ assert.equal(reviewers.length, 6);
+ assert.deepEqual(reviewers,
+ [redundantSuggestion1, redundantSuggestion2,
+ redundantSuggestion3, suggestion1,
+ suggestion2, suggestion3]);
+ })
+ .then(done);
+ });
- const group = {name: 'test'};
- suggestion = provider.makeSuggestionItem({group});
- assert.deepEqual(suggestion, {
- name: group.name + ' (group)',
- value: {group},
- });
-
- suggestion = provider.makeSuggestionItem(account);
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '>',
- value: {account, count: 1},
- });
-
- suggestion = provider.makeSuggestionItem({account: {}});
- assert.deepEqual(suggestion, {
- name: 'Anonymous',
- value: {account: {}},
- });
-
- provider._config = {
- user: {
- anonymous_coward_name: 'Anonymous Coward Name',
- },
- };
-
- suggestion = provider.makeSuggestionItem({account: {}});
- assert.deepEqual(suggestion, {
- name: 'Anonymous Coward Name',
- value: {account: {}},
- });
-
- account = makeAccount('OOO');
-
- suggestion = provider.makeSuggestionItem({account});
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '> (OOO)',
- value: {account},
- });
-
- suggestion = provider.makeSuggestionItem(account);
- assert.deepEqual(suggestion, {
- name: account.name + ' <' + account.email + '> (OOO)',
- value: {account, count: 1},
- });
-
- sandbox.stub(GrDisplayNameUtils, '_accountEmail',
- () => '');
-
- suggestion = provider.makeSuggestionItem(account3);
- assert.deepEqual(suggestion, {
- name: account3.name,
- value: {account: account3, count: 1},
- });
- });
-
- test('getSuggestions', done => {
- provider.getSuggestions()
- .then(reviewers => {
- // Default is no filtering.
- assert.equal(reviewers.length, 6);
- assert.deepEqual(reviewers,
- [redundantSuggestion1, redundantSuggestion2,
- redundantSuggestion3, suggestion1,
- suggestion2, suggestion3]);
- })
- .then(done);
- });
-
- test('getSuggestions short circuits when logged out', () => {
- // API call is already stubbed.
- const xhrSpy = restAPI.getChangeSuggestedReviewers;
- provider._loggedIn = false;
+ test('getSuggestions short circuits when logged out', () => {
+ // API call is already stubbed.
+ const xhrSpy = restAPI.getChangeSuggestedReviewers;
+ provider._loggedIn = false;
+ return provider.getSuggestions('').then(() => {
+ assert.isFalse(xhrSpy.called);
+ provider._loggedIn = true;
return provider.getSuggestions('').then(() => {
- assert.isFalse(xhrSpy.called);
- provider._loggedIn = true;
- return provider.getSuggestions('').then(() => {
- assert.isTrue(xhrSpy.called);
- });
+ assert.isTrue(xhrSpy.called);
});
});
});
-
- test('getChangeSuggestedReviewers is used', done => {
- const suggestReviewerStub =
- sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
- .returns(Promise.resolve([]));
- const suggestAccountStub =
- sandbox.stub(restAPI, 'getSuggestedAccounts')
- .returns(Promise.resolve([]));
-
- provider.getSuggestions('').then(() => {
- assert.isTrue(suggestReviewerStub.calledOnce);
- assert.isTrue(suggestReviewerStub.calledWith(42, ''));
- assert.isFalse(suggestAccountStub.called);
- done();
- });
- });
});
- suite('allowAnyUser set to true', () => {
- setup(done => {
- provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
- Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
- provider.init().then(done);
- });
+ test('getChangeSuggestedReviewers is used', done => {
+ const suggestReviewerStub =
+ sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+ .returns(Promise.resolve([]));
+ const suggestAccountStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([]));
- test('getSuggestedAccounts is used', done => {
- const suggestReviewerStub =
- sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
- .returns(Promise.resolve([]));
- const suggestAccountStub =
- sandbox.stub(restAPI, 'getSuggestedAccounts')
- .returns(Promise.resolve([]));
-
- provider.getSuggestions('').then(() => {
- assert.isFalse(suggestReviewerStub.called);
- assert.isTrue(suggestAccountStub.calledOnce);
- assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
- done();
- });
+ provider.getSuggestions('').then(() => {
+ assert.isTrue(suggestReviewerStub.calledOnce);
+ assert.isTrue(suggestReviewerStub.calledWith(42, ''));
+ assert.isFalse(suggestAccountStub.called);
+ done();
});
});
});
+
+ suite('allowAnyUser set to true', () => {
+ setup(done => {
+ provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+ Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
+ provider.init().then(done);
+ });
+
+ test('getSuggestedAccounts is used', done => {
+ const suggestReviewerStub =
+ sandbox.stub(restAPI, 'getChangeSuggestedReviewers')
+ .returns(Promise.resolve([]));
+ const suggestAccountStub =
+ sandbox.stub(restAPI, 'getSuggestedAccounts')
+ .returns(Promise.resolve([]));
+
+ provider.getSuggestions('').then(() => {
+ assert.isFalse(suggestReviewerStub.called);
+ assert.isTrue(suggestAccountStub.calledOnce);
+ assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
+ done();
+ });
+ });
+ });
+});
</script>
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.js b/polygerrit-ui/app/styles/dashboard-header-styles.js
index 88d50c0..683202e 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.js
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.js
@@ -1,21 +1,22 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-
-<dom-module id="dashboard-header-styles">
+$_documentContainer.innerHTML = `<dom-module id="dashboard-header-styles">
<template>
<style>
:host {
@@ -45,4 +46,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.js b/polygerrit-ui/app/styles/gr-change-list-styles.js
index a8f754d..148943b 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * 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.
+ * 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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-change-list-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-change-list-styles">
<template>
<style>
gr-change-list-item {
@@ -176,4 +178,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
index 84692ba..51cf6d3 100644
--- a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
@@ -1,20 +1,22 @@
-<!--
-@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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-change-metadata-shared-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-shared-styles">
<template>
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
@@ -47,4 +49,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
index 8b26c95..4bfb742 100644
--- a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
@@ -1,29 +1,22 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<!--
- This is shared styles for change-view-integration endpoints.
- All plugins that registered that endpoint should include this in
- the component to have a consistent UX:
-
- <style include="gr-change-view-integration-shared-styles"></style>
-
- And use those defined class to apply these styles.
--->
-<dom-module id="gr-change-view-integration-shared-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-change-view-integration-shared-styles">
<template>
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
@@ -59,4 +52,22 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ This is shared styles for change-view-integration endpoints.
+ All plugins that registered that endpoint should include this in
+ the component to have a consistent UX:
+
+ <style include="gr-change-view-integration-shared-styles"></style>
+
+ And use those defined class to apply these styles.
+*/
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-form-styles.js b/polygerrit-ui/app/styles/gr-form-styles.js
index 5133051..91763c5 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.js
+++ b/polygerrit-ui/app/styles/gr-form-styles.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-form-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-form-styles">
<template>
<style>
.gr-form-styles input {
@@ -113,4 +115,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.js b/polygerrit-ui/app/styles/gr-menu-page-styles.js
index 47c874b..e52a895 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.js
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-menu-page-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-menu-page-styles">
<template>
<style>
:host {
@@ -68,4 +70,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.js b/polygerrit-ui/app/styles/gr-page-nav-styles.js
index ced6ecb..97f1a03 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.js
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.js
@@ -1,20 +1,22 @@
-<!--
-@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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-page-nav-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-page-nav-styles">
<template>
<style>
.navStyles ul {
@@ -61,4 +63,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.js b/polygerrit-ui/app/styles/gr-subpage-styles.js
index 222c38b..f94cc9c 100644
--- a/polygerrit-ui/app/styles/gr-subpage-styles.js
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2018 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2018 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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="gr-subpage-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-subpage-styles">
<template>
<style>
main {
@@ -31,4 +33,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-table-styles.js b/polygerrit-ui/app/styles/gr-table-styles.js
index 26b6db0..ceac675 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.js
+++ b/polygerrit-ui/app/styles/gr-table-styles.js
@@ -1,21 +1,22 @@
-<!--
-@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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-
-<dom-module id="gr-table-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-table-styles">
<template>
<style>
.genericList {
@@ -106,4 +107,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.js b/polygerrit-ui/app/styles/gr-voting-styles.js
index eec79be..4860428 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.js
+++ b/polygerrit-ui/app/styles/gr-voting-styles.js
@@ -1,21 +1,22 @@
-<!--
-@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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-
-<dom-module id="gr-voting-styles">
+$_documentContainer.innerHTML = `<dom-module id="gr-voting-styles">
<template>
<style>
:host {
@@ -29,4 +30,13 @@
}
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/shared-styles.js b/polygerrit-ui/app/styles/shared-styles.js
index 3a0de59..dc4735e 100644
--- a/polygerrit-ui/app/styles/shared-styles.js
+++ b/polygerrit-ui/app/styles/shared-styles.js
@@ -1,20 +1,22 @@
-<!--
-@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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<dom-module id="shared-styles">
+$_documentContainer.innerHTML = `<dom-module id="shared-styles">
<template>
<style>
@@ -125,7 +127,7 @@
--iron-icon-width: 20px;
}
- /* Stopgap solution until we remove hidden$ attributes. */
+ /* Stopgap solution until we remove hidden\$ attributes. */
[hidden] {
display: none !important;
@@ -180,4 +182,13 @@
/** END: loading spiner */
</style>
</template>
-</dom-module>
+</dom-module>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/styles/themes/app-theme.js b/polygerrit-ui/app/styles/themes/app-theme.js
index 8aaaa01..295cb017 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.js
+++ b/polygerrit-ui/app/styles/themes/app-theme.js
@@ -1,20 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * 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.
+ * 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.
+ */
+const $_documentContainer = document.createElement('template');
-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.
--->
-<custom-style><style is="custom-style">
+$_documentContainer.innerHTML = `<custom-style><style is="custom-style">
html {
/**
* When adding a new color variable make sure to also add it to the other
@@ -205,4 +207,13 @@
--spacing-xxl: 16px;
}
}
-</style></custom-style>
+</style></custom-style>`;
+
+document.head.appendChild($_documentContainer.content);
+
+/*
+ FIXME(polymer-modulizer): the above comments were extracted
+ from HTML and may be out of place here. Review them and
+ then delete this comment!
+*/
+
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index 7a391b7..ab9cc39 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -1,63 +1,54 @@
-<!DOCTYPE html>
-<!--
-@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.
+ */
+import '../scripts/bundled-polymer.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-resin/standalone/polymer-resin.html" />
-<link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html">
-<script>
- security.polymer_resin.install({
- allowedIdentifierPrefixes: [''],
- reportHandler(isViolation, fmt, ...args) {
- const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
- log(isViolation, fmt, ...args);
- if (isViolation) {
- // This will cause the test to fail if there is a data binding
- // violation.
- throw new Error(
- 'polymer-resin violation: ' + fmt +
- JSON.stringify(args));
- }
- },
- safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
+import 'polymer-resin/standalone/polymer-resin.js';
+import '../behaviors/safe-types-behavior/safe-types-behavior.js';
+import '@polymer/iron-test-helpers/iron-test-helpers.js';
+import './test-router.js';
+import moment from 'moment/src/moment.js';
+self.moment = moment;
+security.polymer_resin.install({
+ allowedIdentifierPrefixes: [''],
+ reportHandler(isViolation, fmt, ...args) {
+ const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
+ log(isViolation, fmt, ...args);
+ if (isViolation) {
+ // This will cause the test to fail if there is a data binding
+ // violation.
+ throw new Error(
+ 'polymer-resin violation: ' + fmt +
+ JSON.stringify(args));
+ }
+ },
+ safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge,
+});
+self.mockPromise = () => {
+ let res;
+ const promise = new Promise(resolve => {
+ res = resolve;
});
-</script>
-<script>
- self.mockPromise = () => {
- let res;
- const promise = new Promise(resolve => {
- res = resolve;
- });
- promise.resolve = res;
- return promise;
- };
- self.isHidden = el => getComputedStyle(el).display === 'none';
-</script>
-<script>
- (function() {
- setup(() => {
- if (!window.Gerrit) { return; }
- if (Gerrit._testOnly_resetPlugins) {
- Gerrit._testOnly_resetPlugins();
- }
- });
- })();
-</script>
-<link rel="import"
- href="/bower_components/iron-test-helpers/iron-test-helpers.html" />
-<link rel="import" href="test-router.html" />
-<script src="/bower_components/moment/moment.js"></script>
+ promise.resolve = res;
+ return promise;
+};
+self.isHidden = el => getComputedStyle(el).display === 'none';
+setup(() => {
+ if (!window.Gerrit) { return; }
+ if (Gerrit._testOnly_resetPlugins) {
+ Gerrit._testOnly_resetPlugins();
+ }
+});
diff --git a/polygerrit-ui/app/test/test-router.js b/polygerrit-ui/app/test/test-router.js
index 34ff374..914537c 100644
--- a/polygerrit-ui/app/test/test-router.js
+++ b/polygerrit-ui/app/test/test-router.js
@@ -1,22 +1,19 @@
-<!DOCTYPE html>
-<!--
-@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.
+ */
+import '../elements/core/gr-navigation/gr-navigation.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="../elements/core/gr-navigation/gr-navigation.html">
-<script>
- Gerrit.Nav.setup(url => { /* noop */ }, params => '', () => []);
-</script>
+Gerrit.Nav.setup(url => { /* noop */ }, params => '', () => []);