Merge "Minor change to gr-status API."
diff --git a/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
index 8f51e13..2805f52 100644
--- a/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
+++ b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
@@ -84,11 +84,11 @@
}
/** Event to be fired when an attention set changes */
- private static class Event extends AbstractChangeEvent implements AttentionSetListener.Event {
+ public static class Event extends AbstractChangeEvent implements AttentionSetListener.Event {
private final Set<Integer> added;
private final Set<Integer> removed;
- Event(
+ public Event(
ChangeInfo change,
AccountInfo editor,
Set<Integer> added,
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 97e2f55..7a6ac0d 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -513,7 +513,8 @@
private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in)
throws BadRequestException, AuthException, UnprocessableEntityException,
- PermissionBackendException, IOException, ConfigInvalidException {
+ ResourceConflictException, PermissionBackendException, IOException,
+ ConfigInvalidException {
logger.atFine().log("request is executed on behalf of %s", in.onBehalfOf);
if (in.labels == null || in.labels.isEmpty()) {
@@ -571,7 +572,7 @@
try {
permissionBackend.user(reviewer).change(rev.getNotes()).check(ChangePermission.READ);
} catch (AuthException e) {
- throw new UnprocessableEntityException(
+ throw new ResourceConflictException(
String.format("on_behalf_of account %s cannot see change", reviewer.getAccountId()), e);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 859ccbd..180ec45 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4778,6 +4778,43 @@
assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isNull();
}
+ @Test
+ public void ccUserThatCannotSeeTheChange() throws Exception {
+ // Create a project that is only visible to admin users.
+ Project.NameKey project = projectOperations.newProject().create();
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
+ .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ // Create a change
+ TestRepository<?> adminTestRepo = cloneProject(project, admin);
+ PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+
+ // Check that the change is not visible to user.
+ requestScopeOperations.setApiUser(user.id());
+ assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).get());
+
+ // Add user as a CC.
+ requestScopeOperations.setApiUser(admin.id());
+ ReviewerInput reviewerInput = new ReviewerInput();
+ reviewerInput.state = CC;
+ reviewerInput.reviewer = user.id().toString();
+ gApi.changes().id(r.getChangeId()).addReviewer(reviewerInput);
+
+ // Check that user was not added as a CC since they cannot see the change. Note,
+ // ChangeInfo#reviewers is a map that also contains CCs (if any are present).
+ assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
+
+ // Check that the change is still not visible to user.
+ requestScopeOperations.setApiUser(user.id());
+ assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).get());
+ }
+
private PushOneCommit.Result createWorkInProgressChange() throws Exception {
return pushTo("refs/for/master%wip");
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 4da4da4..eb827c0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -62,6 +62,7 @@
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
@@ -313,8 +314,8 @@
in.onBehalfOf = user.id().toString();
in.label("Code-Review", 1);
- UnprocessableEntityException thrown =
- assertThrows(UnprocessableEntityException.class, () -> revision.review(in));
+ ResourceConflictException thrown =
+ assertThrows(ResourceConflictException.class, () -> revision.review(in));
assertThat(thrown)
.hasMessageThat()
.contains("on_behalf_of account " + user.id() + " cannot see change");
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index a5da590..4b62710 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -5,6 +5,7 @@
*/
import '@polymer/iron-input/iron-input';
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
import '../gr-permission/gr-permission';
import {
AccessPermissions,
@@ -32,7 +33,6 @@
import {customElement, property, query, state} from 'lit/decorators';
import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
-import {iconStyles} from '../../../styles/gr-icon-styles';
/**
* Fired when the section has been modified or removed.
@@ -103,7 +103,6 @@
return [
formStyles,
fontStyles,
- iconStyles,
sharedStyles,
css`
:host {
@@ -180,7 +179,7 @@
class=${this.section?.id === GLOBAL_NAME ? 'global' : ''}
@click=${this.editReference}
>
- <span class="material-icon" id="icon">edit</span>
+ <gr-icon id="icon" icon="edit"></gr-icon>
</gr-button>
</div>
<iron-input
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
index c59ef0a..1048dde 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
@@ -84,7 +84,7 @@
role="button"
tabindex="0"
>
- <span class="material-icon" id="icon"> edit </span>
+ <gr-icon icon="edit" id="icon"></gr-icon>
</gr-button>
</div>
<iron-input class="editRefInput">
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index bd6773e..270d9be 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-dropdown-list/gr-dropdown-list';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-page-nav/gr-page-nav';
import '../gr-admin-group-list/gr-admin-group-list';
import '../gr-group/gr-group';
@@ -50,7 +51,6 @@
import {customElement, property, state} from 'lit/decorators';
import {ifDefined} from 'lit/directives/if-defined';
import {ValueChangedEvent} from '../../../types/events';
-import {iconStyles} from '../../../styles/gr-icon-styles';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
@@ -125,7 +125,6 @@
static override get styles() {
return [
- iconStyles,
sharedStyles,
menuPageStyles,
pageNavStyles,
@@ -134,7 +133,7 @@
/* Same as dropdown trigger so chevron spacing is consistent. */
padding: 5px 4px;
}
- .material-icon {
+ gr-icon {
margin: 0 var(--spacing-xs);
}
.breadcrumb {
@@ -240,7 +239,7 @@
<section class="mainHeader">
<span class="breadcrumb">
<span class="breadcrumbText">${this.breadcrumbParentName}</span>
- <span class="material-icon">chevron_right</span>
+ <gr-icon icon="chevron_right"></gr-icon>
</span>
<gr-dropdown-list
id="pageSelect"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
index 35ab0da..9aa0598 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
@@ -32,6 +32,7 @@
import {ProgressStatus} from '../../../constants/constants';
import {fireAlert, fireReload} from '../../../utils/event-util';
import '../../shared/gr-dialog/gr-dialog';
+import '../../shared/gr-icon/gr-icon';
import '../../change/gr-label-score-row/gr-label-score-row';
import {getOverallStatus} from '../../../utils/bulk-flow-util';
import {allSettled} from '../../../utils/async-util';
@@ -39,7 +40,6 @@
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {Interaction} from '../../../constants/reporting';
-import {iconStyles} from '../../../styles/gr-icon-styles';
@customElement('gr-change-list-bulk-vote-flow')
export class GrChangeListBulkVoteFlow extends LitElement {
@@ -62,7 +62,6 @@
static override get styles() {
return [
fontStyles,
- iconStyles,
css`
gr-dialog {
width: 840px;
@@ -97,20 +96,20 @@
background-color: var(--error-background);
margin-top: var(--spacing-l);
}
- .code-review-message-container .material-icon,
- .error-container .material-icon {
+ .code-review-message-container gr-icon,
+ .error-container gr-icon {
padding: 10px var(--spacing-xl);
}
- .error-container .material-icon {
+ .error-container gr-icon {
color: var(--error-foreground);
}
- .code-review-message-container .material-icon {
+ .code-review-message-container gr-icon {
color: var(--selected-foreground);
}
- .error-container span,
- .code-review-message-container span {
+ .error-container .error-text,
+ .code-review-message-container .warning-text {
position: relative;
- top: 1px;
+ top: 10px;
}
.code-review-message-container {
display: table-caption;
@@ -196,10 +195,8 @@
<div class="code-review-message-container">
<div class="code-review-message-layout-container">
<div>
- <span class="material-icon" aria-label="Information" role="img"
- >info</span
- >
- <span>
+ <gr-icon icon="info" aria-label="Information" role="img"></gr-icon>
+ <span class="warning-text">
Code Review vote is only available on the individual change page
</span>
</div>
@@ -241,10 +238,8 @@
}
return html`
<div class="error-container">
- <span class="material-icon filled" role="img" aria-label="Error"
- >error</span
- >
- <span>
+ <gr-icon icon="error" filled role="img" aria-label="Error"></gr-icon>
+ <span class="error-text">
<!-- prettier-ignore -->
Failed to vote on ${pluralize(
Array.from(this.progressByChange.values()).filter(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index 18284a7..6f9e4a0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -160,8 +160,8 @@
<div class="code-review-message-container">
<div class="code-review-message-layout-container">
<div>
- <span class="material-icon" aria-label="Information" role="img">info</span>
- <span>
+ <gr-icon icon="info" aria-label="Information" role="img"></gr-icon>
+ <span class="warning-text">
Code Review vote is only available on the individual change page
</span>
</div>
@@ -248,8 +248,8 @@
<div class="code-review-message-container">
<div class="code-review-message-layout-container">
<div>
- <span class="material-icon" aria-label="Information" role="img">info</span>
- <span>
+ <gr-icon icon="info" aria-label="Information" role="img"></gr-icon>
+ <span class="warning-text">
Code Review vote is only available on the individual change page
</span>
</div>
@@ -280,8 +280,8 @@
</gr-label-score-row>
</div>
<div class="error-container">
- <span class="material-icon filled" role="img" aria-label="Error">error</span>
- <span> Failed to vote on 1 change </span>
+ <gr-icon icon="error" filled role="img" aria-label="Error"></gr-icon>
+ <span class="error-text"> Failed to vote on 1 change </span>
</div>
</div>
</gr-dialog>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
index 20899c2..b19094f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement.ts
@@ -5,6 +5,7 @@
*/
import '../../change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard';
import '../../shared/gr-change-status/gr-change-status';
+import '../../shared/gr-icon/gr-icon';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {
@@ -29,7 +30,6 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {ifDefined} from 'lit/directives/if-defined';
import {capitalizeFirstLetter} from '../../../utils/string-util';
-import {iconStyles} from '../../../styles/gr-icon-styles';
@customElement('gr-change-list-column-requirement')
export class GrChangeListColumnRequirement extends LitElement {
@@ -41,7 +41,6 @@
static override get styles() {
return [
- iconStyles,
submitRequirementsStyles,
sharedStyles,
css`
@@ -139,11 +138,13 @@
}
private renderStatusIcon(status: SubmitRequirementStatus) {
- const icon = iconForStatus(status ?? SubmitRequirementStatus.ERROR);
+ const icon = iconForStatus(status);
return html`
- <span class="material-icon ${icon.icon} ${icon.filled ? 'filled' : ''}"
- >${icon.icon}</span
- >
+ <gr-icon
+ class=${icon.icon}
+ icon=${icon.icon}
+ ?filled=${icon.filled}
+ ></gr-icon>
`;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
index 067cce8..1ca21a4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
@@ -57,7 +57,7 @@
expect(element).shadowDom.to.equal(
/* HTML */
` <div class="container" title="Satisfied">
- <span class="material-icon filled check_circle">check_circle</span>
+ <gr-icon class="check_circle" filled icon="check_circle"></gr-icon>
</div>`
);
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
index 4ac7581..eec67b9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary.ts
@@ -5,6 +5,7 @@
*/
import '../../change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard';
import '../../shared/gr-change-status/gr-change-status';
+import '../../shared/gr-icon/gr-icon';
import {LitElement, css, html, TemplateResult} from 'lit';
import {customElement, property} from 'lit/decorators';
import {ChangeInfo, SubmitRequirementStatus} from '../../../api/rest-api';
@@ -16,7 +17,6 @@
} from '../../../utils/label-util';
import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
import {pluralize} from '../../../utils/string-util';
-import {iconStyles} from '../../../styles/gr-icon-styles';
@customElement('gr-change-list-column-requirements-summary')
export class GrChangeListColumnRequirementsSummary extends LitElement {
@@ -25,17 +25,16 @@
static override get styles() {
return [
- iconStyles,
submitRequirementsStyles,
css`
- .material-icon {
+ gr-icon {
font-size: var(--line-height-normal, 20px);
}
- .material-icon.block,
- .material-icon.check_circle {
+ gr-icon.block,
+ gr-icon.check_circle {
margin-right: var(--spacing-xs);
}
- .material-icon.commentIcon {
+ gr-icon.commentIcon {
color: var(--deemphasized-text-color);
margin-left: var(--spacing-s);
}
@@ -117,10 +116,12 @@
return html`<span class=${icon.icon} role="button" tabindex="0">
<gr-submit-requirement-dashboard-hovercard .change=${this.change}>
</gr-submit-requirement-dashboard-hovercard>
- <span
- class="material-icon ${icon.icon} ${icon.filled ? 'filled' : ''}"
+ <gr-icon
+ class=${icon.icon}
+ icon=${icon.icon}
+ ?filled=${icon.filled}
role="img"
- >${icon.icon}</span
+ ></gr-icon
>${aggregation}</span
>`;
}
@@ -131,14 +132,15 @@
renderCommentIcon() {
if (!this.change?.unresolved_comment_count) return;
- return html`<span
- class="commentIcon material-icon filled"
+ return html`<gr-icon
+ class="commentIcon"
+ icon="mode_comment"
+ filled
.title=${pluralize(
this.change?.unresolved_comment_count,
'unresolved comment'
)}
- >mode_comment</span
- >`;
+ ></gr-icon>`;
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts
index bcdf143..eecb009 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary_test.ts
@@ -64,7 +64,7 @@
>
<gr-submit-requirement-dashboard-hovercard>
</gr-submit-requirement-dashboard-hovercard>
- <span class="material-icon block" role="img">block</span>
+ <gr-icon class="block" role="img" icon="block"></gr-icon>
<span class="unsatisfied">1 missing</span>
</span>`);
});
@@ -85,13 +85,14 @@
>
<gr-submit-requirement-dashboard-hovercard>
</gr-submit-requirement-dashboard-hovercard>
- <span class="material-icon block" role="img">block</span>
+ <gr-icon class="block" role="img" icon="block"></gr-icon>
<span class="unsatisfied">1 missing</span>
</span>
- <span
- class="commentIcon material-icon filled"
+ <gr-icon
+ class="commentIcon"
+ filled
+ icon="mode_comment"
title="5 unresolved comments"
- >mode_comment</span
- >`);
+ ></gr-icon>`);
});
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
index 6164355..688e78d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
@@ -10,6 +10,7 @@
import {ChangeInfo, Hashtag} from '../../../types/common';
import {subscribe} from '../../lit/subscription-controller';
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '@polymer/iron-dropdown/iron-dropdown';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
@@ -26,7 +27,6 @@
import {fireAlert} from '../../../utils/event-util';
import {pluralize} from '../../../utils/string-util';
import {Interaction} from '../../../constants/reporting';
-import {iconStyles} from '../../../styles/gr-icon-styles';
@customElement('gr-change-list-hashtag-flow')
export class GrChangeListHashtagFlow extends LitElement {
@@ -57,7 +57,6 @@
static override get styles() {
return [
- iconStyles,
spinnerStyles,
css`
iron-dropdown {
@@ -121,7 +120,7 @@
.error {
color: var(--deemphasized-text-color);
}
- .material-icon {
+ gr-icon {
color: var(--error-color);
/* Center with text by aligning it to the top and then pushing it down
to match the text */
@@ -251,7 +250,7 @@
`;
case ProgressStatus.FAILED:
return html`
- <span class="material-icon filled">error</span>
+ <gr-icon icon="error" filled></gr-icon>
<div class="error">${this.errorText}</div>
`;
default:
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 013dd9e..8910e27 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -7,6 +7,7 @@
import '../../shared/gr-change-star/gr-change-star';
import '../../shared/gr-change-status/gr-change-status';
import '../../shared/gr-date-formatter/gr-date-formatter';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-limited-text/gr-limited-text';
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
@@ -41,7 +42,6 @@
import {bulkActionsModelToken} from '../../../models/bulk-actions/bulk-actions-model';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
-import {iconStyles} from '../../../styles/gr-icon-styles';
enum ChangeSize {
XS = 10,
@@ -153,7 +153,6 @@
static override get styles() {
return [
changeListStyles,
- iconStyles,
sharedStyles,
submitRequirementsStyles,
css`
@@ -266,7 +265,7 @@
.cell.label {
font-weight: var(--font-weight-normal);
}
- .cell.label .material-icon {
+ .cell.label gr-icon {
vertical-align: top;
}
/* Requirement child needs whole area */
@@ -463,7 +462,7 @@
return html`
<td class="cell comments">
${this.change?.unresolved_comment_count
- ? html`<span class="material-icon filled">mode_comment</span>`
+ ? html`<gr-icon icon="mode_comment" filled></gr-icon>`
: ''}
<span
>${this.computeComments(this.change?.unresolved_comment_count)}</span
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index f48f512..a2a3d45 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -21,6 +21,7 @@
import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {getAppContext} from '../../../services/app-context';
import {
@@ -140,7 +141,7 @@
.warning + .warning {
margin-top: var(--spacing-s);
}
- .material-icon {
+ gr-icon {
color: var(--orange-800);
font-size: 18px;
}
@@ -309,9 +310,7 @@
}
return html`
<div class="error">
- <span class="material-icon filled" role="img" aria-label="Error"
- >error</span
- >
+ <gr-icon icon="error" filled role="img" aria-label="Error"></gr-icon>
Failed to add ${listForSentence(failedAccounts)} to changes.
</div>
`;
@@ -335,9 +334,12 @@
updatedReviewerState === ReviewerState.CC ? 'CC' : 'reviewer';
return html`
<div class="warning">
- <span class="material-icon filled" role="img" aria-label="Warning"
- >warning</span
- >
+ <gr-icon
+ icon="warning"
+ filled
+ role="img"
+ aria-label="Warning"
+ ></gr-icon>
${listForSentence(overwrittenNames)} ${pluralizedVerb} ${currentLabel}
on some selected changes and will be moved to ${updatedLabel} on all
changes.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index 029dd6f..5a4a90c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -683,15 +683,15 @@
</gr-overlay>
</div>
<div class="warning">
- <span class="material-icon filled" role="img"
- aria-label="Warning">warning</span>
+ <gr-icon icon="warning" filled role="img" aria-label="Warning"
+ ></gr-icon>
User-1 is a reviewer
on some selected changes and will be moved to CC on all
changes.
</div>
<div class="warning">
- <span class="material-icon filled"role="img"
- aria-label="Warning">warning</span>
+ <gr-icon icon="warning" filled role="img" aria-label="Warning"
+ ></gr-icon>
User-4 is a CC
on some selected changes and will be moved to reviewer on all
changes.
@@ -804,7 +804,7 @@
</gr-overlay>
</div>
<div class="error">
- <span class="material-icon filled" role="img" aria-label="Error">error</span>
+ <gr-icon icon="error" filled role="img" aria-label="Error"></gr-icon>
Failed to add User-0, User-2, Group 0, and User-3 to changes.
</div>
</div>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
index e1d6ae9..53d59f5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
@@ -10,6 +10,7 @@
import {ChangeInfo, TopicName} from '../../../types/common';
import {subscribe} from '../../lit/subscription-controller';
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '@polymer/iron-dropdown/iron-dropdown';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
@@ -27,7 +28,6 @@
import {fireAlert} from '../../../utils/event-util';
import {pluralize} from '../../../utils/string-util';
import {Interaction} from '../../../constants/reporting';
-import {iconStyles} from '../../../styles/gr-icon-styles';
@customElement('gr-change-list-topic-flow')
export class GrChangeListTopicFlow extends LitElement {
@@ -58,7 +58,6 @@
static override get styles() {
return [
- iconStyles,
spinnerStyles,
css`
iron-dropdown {
@@ -121,7 +120,7 @@
.error {
color: var(--deemphasized-text-color);
}
- .material-icon {
+ gr-icon {
color: var(--error-color);
/* Center with text by aligning it to the top and then pushing it down
to match the text */
@@ -264,7 +263,7 @@
`;
case ProgressStatus.FAILED:
return html`
- <span class="material-icon filled">error</span>
+ <gr-icon icon="error" filled></gr-icon>
<div class="error">${this.errorText}</div>
`;
default:
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 170525d..de9c211 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -131,7 +131,7 @@
margin-right: 20px;
color: var(--deemphasized-text-color);
}
- .material-icon {
+ gr-icon {
font-size: 1.85rem;
margin-left: 16px;
}
@@ -210,7 +210,7 @@
return html`
<a id="prevArrow" href=${this.computeNavLink(-1)}>
- <span class="material-icon" aria-label="Older">chevron_left</span>
+ <gr-icon icon="chevron_left" aria-label="Older"></gr-icon>
</a>
`;
}
@@ -226,7 +226,7 @@
return html`
<a id="nextArrow" href=${this.computeNavLink(1)}>
- <span class="material-icon" aria-label="Newer">chevron_right</span>
+ <gr-icon icon="chevron_right" aria-label="Newer"></gr-icon>
</a>
`;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
index 1ebe328..14fd2e8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
@@ -61,7 +61,7 @@
<nav>
Page
<a href="" id="prevArrow">
- <span class="material-icon" aria-label="Older">chevron_left</span>
+ <gr-icon icon="chevron_left" aria-label="Older"></gr-icon>
</a>
</nav>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 49526d2..50c51c2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -985,7 +985,7 @@
showPrependedDynamicColumns
),
initialCount: this.fileListIncrement,
- targetFrameRate: 30,
+ targetFrameRate: 1,
});
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index a3aa8e6..8262b09 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -81,7 +81,7 @@
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
- stub('gr-date-formatter', '_loadTimeFormat').callsFake(() =>
+ stub('gr-date-formatter', 'loadTimeFormat').callsFake(() =>
Promise.resolve()
);
stub('gr-diff-host', 'reload').callsFake(() => Promise.resolve());
@@ -1997,7 +1997,7 @@
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
- stub('gr-date-formatter', '_loadTimeFormat').callsFake(() =>
+ stub('gr-date-formatter', 'loadTimeFormat').callsFake(() =>
Promise.resolve()
);
stubRestApi('getDiff').callsFake(() => Promise.resolve(createDiff()));
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 2daf768..38b6eea 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -10,6 +10,7 @@
import '../../shared/gr-account-chip/gr-account-chip';
import '../../shared/gr-textarea/gr-textarea';
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-formatted-text/gr-formatted-text';
import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-account-list/gr-account-list';
@@ -106,7 +107,6 @@
import {customElement, property, state, query} from 'lit/decorators';
import {subscribe} from '../../lit/subscription-controller';
import {configModelToken} from '../../../models/config/config-model';
-import {iconStyles} from '../../../styles/gr-icon-styles';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -341,7 +341,6 @@
static override styles = [
sharedStyles,
- iconStyles,
css`
:host {
background-color: var(--dialog-background-color);
@@ -491,7 +490,7 @@
vertical-align: top;
--gr-button-padding: 0px 4px;
}
- .attention .edit-attention-button .material-icon {
+ .attention .edit-attention-button gr-icon {
color: inherit;
}
.attention a,
@@ -553,7 +552,7 @@
margin-top: var(--spacing-m);
background-color: var(--assignee-highlight-color);
}
- .attentionTip div iron-icon {
+ .attentionTip div gr-icon {
margin-right: var(--spacing-s);
}
.patchsetLevelContainer {
@@ -969,7 +968,7 @@
role="button"
tabindex="0"
>
- <span class="material-icon filled">edit</span>
+ <gr-icon icon="edit" filled></gr-icon>
Modify
</gr-button>
</gr-tooltip-content>
@@ -979,7 +978,7 @@
href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
target="_blank"
>
- <span class="material-icon" title="read documentation">help</span>
+ <gr-icon icon="help" title="read documentation"></gr-icon>
</a>
</div>
</div>
@@ -1001,7 +1000,7 @@
href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
target="_blank"
>
- <span class="material-icon" title="read documentation">help</span>
+ <gr-icon icon="help" title="read documentation"></gr-icon>
</a>
</div>
</div>
@@ -1090,7 +1089,7 @@
this.computeShowAttentionTip(),
() => html`
<div class="attentionTip">
- <span class="material-icon pointer">lightbulb</span>
+ <gr-icon icon="lightbulb"></gr-icon>
Please be mindful of requiring attention from too many users.
</div>
`
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 70f708d..b62a430 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -281,7 +281,7 @@
role="button"
tabindex="0"
>
- <span class="material-icon filled">edit</span>
+ <gr-icon icon="edit" filled></gr-icon>
Modify
</gr-button>
</gr-tooltip-content>
@@ -291,9 +291,7 @@
href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
target="_blank"
>
- <span class="material-icon" title="read documentation"
- >help</span
- >
+ <gr-icon icon="help" title="read documentation"></gr-icon>
</a>
</div>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index 354b7d3..ef58f94 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-label-info/gr-label-info';
import {customElement, property} from 'lit/decorators';
import {
@@ -32,7 +33,6 @@
import {CURRENT} from '../../../utils/patch-set-util';
import {fireReload} from '../../../utils/event-util';
import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
-import {iconStyles} from '../../../styles/gr-icon-styles';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
const base = HovercardMixin(LitElement);
@@ -59,7 +59,6 @@
static override get styles() {
return [
fontStyles,
- iconStyles,
submitRequirementsStyles,
base.styles || [],
css`
@@ -98,7 +97,7 @@
div.sectionIcon {
flex: 0 0 30px;
}
- div.sectionIcon .material-icon {
+ div.sectionIcon gr-icon {
position: relative;
}
.section.condition > .sectionContent {
@@ -112,7 +111,7 @@
.expression {
color: var(--gray-foreground);
}
- .button .material-icon {
+ .button gr-icon {
color: inherit;
}
div.button {
@@ -137,7 +136,7 @@
</div>
<div class="section">
<div class="sectionIcon">
- <span class="small material-icon">info</span>
+ <gr-icon class="small" icon="info"></gr-icon>
</div>
<div class="sectionContent">
<div class="row">
@@ -154,12 +153,13 @@
private renderStatus(requirement: SubmitRequirementResultInfo) {
const icon = iconForRequirement(requirement);
- return html`<span
- class="material-icon ${icon.icon} ${icon.filled ? 'filled' : ''}"
+ return html`<gr-icon
+ class=${icon.icon}
+ icon=${icon.icon}
+ ?filled=${icon.filled}
role="img"
aria-label=${requirement.status.toLowerCase()}
- >${icon.icon}</span
- >`;
+ ></gr-icon>`;
}
private renderDescription() {
@@ -175,7 +175,7 @@
if (!description) return;
return html`<div class="section description">
<div class="sectionIcon">
- <span class="material-icon">description</span>
+ <gr-icon icon="description"></gr-icon>
</div>
<div class="sectionContent">
<gr-formatted-text
@@ -238,7 +238,7 @@
@click=${(_: MouseEvent) => this.toggleConditionsVisibility()}
>
${buttonText}
- <span class="material-icon">${icon}</span>
+ <gr-icon .icon=${icon}></gr-icon>
</gr-button>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
index 8b7fec3..1a44caa 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -40,12 +40,14 @@
<div id="container" role="tooltip" tabindex="-1">
<div class="section">
<div class="sectionIcon">
- <span
+ <gr-icon
aria-label="satisfied"
role="img"
- class="material-icon filled check_circle"
- >check_circle
- </span>
+ class="check_circle"
+ filled
+ icon="check_circle"
+ >
+ </gr-icon>
</div>
<div class="sectionContent">
<h3 class="heading-3 name">
@@ -55,7 +57,7 @@
</div>
<div class="section">
<div class="sectionIcon">
- <span class="small material-icon">info</span>
+ <gr-icon class="small" icon="info"></gr-icon>
</div>
<div class="sectionContent">
<div class="row">
@@ -73,7 +75,7 @@
tabindex="0"
>
View conditions
- <span class="material-icon">expand_more</span>
+ <gr-icon icon="expand_more"></gr-icon>
</gr-button>
</div>
</div>
@@ -88,12 +90,14 @@
<div id="container" role="tooltip" tabindex="-1">
<div class="section">
<div class="sectionIcon">
- <span
+ <gr-icon
aria-label="satisfied"
role="img"
- class="material-icon filled check_circle"
- >check_circle
- </span>
+ class="check_circle"
+ filled
+ icon="check_circle"
+ >
+ </gr-icon>
</div>
<div class="sectionContent">
<h3 class="heading-3 name">
@@ -103,7 +107,7 @@
</div>
<div class="section">
<div class="sectionIcon">
- <span class="small material-icon">info</span>
+ <gr-icon class="small" icon="info"></gr-icon>
</div>
<div class="sectionContent">
<div class="row">
@@ -121,7 +125,7 @@
tabindex="0"
>
Hide conditions
- <span class="material-icon">expand_less</span>
+ <gr-icon icon="expand_less"></gr-icon>
</gr-button>
</div>
<div class="section condition">
@@ -168,12 +172,13 @@
<div id="container" role="tooltip" tabindex="-1">
<div class="section">
<div class="sectionIcon">
- <span
+ <gr-icon
aria-label="satisfied"
role="img"
- class="material-icon filled check_circle"
- >check_circle
- </span>
+ class="check_circle"
+ filled
+ icon="check_circle"
+ ></gr-icon>
</div>
<div class="sectionContent">
<h3 class="heading-3 name">
@@ -183,7 +188,7 @@
</div>
<div class="section">
<div class="sectionIcon">
- <span class="small material-icon">info</span>
+ <gr-icon class="small" icon="info"></gr-icon>
</div>
<div class="sectionContent">
<div class="row">
@@ -202,7 +207,7 @@
</div>
<div class="section description">
<div class="sectionIcon">
- <span class="material-icon">description</span>
+ <gr-icon icon="description"></gr-icon>
</div>
<div class="sectionContent">
<gr-formatted-text notrailingmargin=""></gr-formatted-text>
@@ -217,7 +222,7 @@
tabindex="0"
>
View conditions
- <span class="material-icon">expand_more</span>
+ <gr-icon icon="expand_more"></gr-icon>
</gr-button>
</div>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index eac9e6f..63566c1 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-label-info/gr-label-info';
+import '../../shared/gr-icon/gr-icon';
import '../gr-submit-requirement-hovercard/gr-submit-requirement-hovercard';
import '../gr-trigger-vote/gr-trigger-vote';
import '../gr-change-summary/gr-change-summary';
@@ -43,7 +44,6 @@
import {checksModelToken} from '../../../models/checks/checks-model';
import {join} from 'lit/directives/join';
import {map} from 'lit/directives/map';
-import {iconStyles} from '../../../styles/gr-icon-styles';
/**
* @attr {Boolean} suppress-title - hide titles, currently for hovercard view
@@ -71,7 +71,6 @@
static override get styles() {
return [
fontStyles,
- iconStyles,
submitRequirementsStyles,
css`
:host([suppress-title]) .metadata-title {
@@ -83,7 +82,7 @@
margin: 0 0 var(--spacing-s);
padding-top: var(--spacing-s);
}
- .material-icon {
+ gr-icon {
font-size: var(--line-height-normal, 20px);
}
.requirements,
@@ -244,12 +243,13 @@
private renderStatus(requirement: SubmitRequirementResultInfo) {
const icon = iconForRequirement(requirement);
- return html`<span
- class="material-icon ${icon.icon} ${icon.filled ? 'filled' : ''}"
+ return html`<gr-icon
+ class=${icon.icon}
+ ?filled=${icon.filled}
+ .icon=${icon.icon}
role="img"
aria-label=${requirement.status.toLowerCase()}
- >${icon.icon}</span
- >`;
+ ></gr-icon>`;
}
renderVoteCell(requirement: SubmitRequirementResultInfo) {
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index 4d1cd54..f380159 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -78,12 +78,14 @@
<tbody>
<tr id="requirement-0-Verified" role="button" tabindex="0">
<td>
- <span
+ <gr-icon
aria-label="satisfied"
role="img"
- class="material-icon filled check_circle"
- >check_circle
- </span>
+ class="check_circle"
+ filled
+ icon="check_circle"
+ >
+ </gr-icon>
</td>
<td class="name">
<gr-limited-text class="name"></gr-limited-text>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 255f60d..1608528 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -599,6 +599,7 @@
static override get styles() {
return [
+ iconStyles,
sharedStyles,
css`
.links {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
index 5af3c4c..d438a73 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results_test.ts
@@ -160,7 +160,7 @@
<span
aria-label="Fake Bug Report 1"
class="material-icon filled link"
- >bug
+ >bug_report
</span>
<paper-tooltip offset="5"> </paper-tooltip>
</a>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts
index 921518d27..b9d621d 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.ts
@@ -5,17 +5,28 @@
*/
import '../../../test/common-test-setup-karma';
import './gr-account-dropdown';
+import {fixture, html} from '@open-wc/testing-helpers';
import {GrAccountDropdown} from './gr-account-dropdown';
import {AccountInfo} from '../../../types/common';
import {createServerInfo} from '../../../test/test-data-generators';
-const basicFixture = fixtureFromElement('gr-account-dropdown');
-
suite('gr-account-dropdown tests', () => {
let element: GrAccountDropdown;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ element = await fixture(html`<gr-account-dropdown></gr-account-dropdown>`);
+ });
+
+ test('renders', async () => {
+ element.account = {name: 'John Doe', email: 'john@doe.com'} as AccountInfo;
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-dropdown link="">
+ <span>John Doe</span>
+ <gr-avatar aria-label="Account avatar" hidden=""> </gr-avatar>
+ </gr-dropdown>
+ `);
});
test('account information', () => {
@@ -29,7 +40,7 @@
test('test for account without a name', () => {
element.account = {id: '0001'} as AccountInfo;
assert.deepEqual(element.topContent, [
- {text: 'Anonymous', bold: true},
+ {text: 'Name of user not set', bold: true},
{text: ''},
]);
});
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
index fd474e5..19db752 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.ts
@@ -3,20 +3,34 @@
* Copyright 2018 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {fixture, html} from '@open-wc/testing-helpers';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import '../../../test/common-test-setup-karma';
import {mockPromise, queryAndAssert} from '../../../test/test-utils';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrErrorDialog} from './gr-error-dialog';
-
-const basicFixture = fixtureFromElement('gr-error-dialog');
+import './gr-error-dialog';
suite('gr-error-dialog tests', () => {
let element: GrErrorDialog;
setup(async () => {
- element = basicFixture.instantiate();
- await element.updateComplete;
+ element = await fixture(html`<gr-error-dialog></gr-error-dialog>`);
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-dialog
+ cancel-label=""
+ confirm-label="Dismiss"
+ confirm-on-enter=""
+ id="dialog"
+ role="dialog"
+ >
+ <div class="header" slot="header">An error occurred</div>
+ <div class="main" slot="main"></div>
+ </gr-dialog>
+ `);
});
test('dismiss tap fires event', async () => {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index d7b1130..6487bb8 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -319,7 +319,7 @@
this.reporting.error(new Error(`network error: ${e.detail.error.message}`));
};
- // TODO(dhruvsr): allow less priority alerts to override high priority alerts
+ // TODO(dhruvsri): allow less priority alerts to override high priority alerts
// In some use cases we may want generic alerts to show along/over errors
// private but used in tests
canOverride(incoming = ErrorType.GENERIC, existing = ErrorType.GENERIC) {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
index de23365..a4f1893 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
@@ -56,6 +56,31 @@
});
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-overlay
+ aria-hidden="true"
+ id="errorOverlay"
+ style="outline: none; display: none;"
+ tabindex="-1"
+ with-backdrop=""
+ >
+ <gr-error-dialog id="errorDialog"> </gr-error-dialog>
+ </gr-overlay>
+ <gr-overlay
+ always-on-top=""
+ aria-hidden="true"
+ id="noInteractionOverlay"
+ no-cancel-on-esc-key=""
+ no-cancel-on-outside-click=""
+ style="outline: none; display: none;"
+ tabindex="-1"
+ with-backdrop=""
+ >
+ </gr-overlay>
+ `);
+ });
+
test('does not show auth error on 403 by default', async () => {
const showAuthErrorStub = sinon.stub(element, 'showAuthErrorAlert');
const responseText = Promise.resolve('server says no.');
@@ -305,7 +330,7 @@
assert.equal(fetchStub.callCount, 1);
await flush();
- // here needs two flush as there are two chanined
+ // here needs two flush as there are two chained
// promises on server-error handler and flush only flushes one
assert.equal(fetchStub.callCount, 2);
await flush();
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts
index 480391a..5d12c72 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.ts
@@ -3,38 +3,58 @@
* Copyright 2018 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {fixture, html} from '@open-wc/testing-helpers';
import '../../../test/common-test-setup-karma';
import './gr-key-binding-display';
import {GrKeyBindingDisplay} from './gr-key-binding-display';
-const basicFixture = fixtureFromElement('gr-key-binding-display');
+const x = ['x'];
+const ctrlX = ['Ctrl', 'x'];
+const shiftMetaX = ['Shift', 'Meta', 'x'];
suite('gr-key-binding-display tests', () => {
let element: GrKeyBindingDisplay;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ element = await fixture(
+ html`<gr-key-binding-display
+ .binding=${[x, ctrlX, shiftMetaX]}
+ ></gr-key-binding-display>`
+ );
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <span class="key"> x </span>
+ or
+ <span class="key modifier"> Ctrl </span>
+ <span class="key"> x </span>
+ or
+ <span class="key modifier"> Shift </span>
+ <span class="key modifier"> Meta </span>
+ <span class="key"> x </span>
+ `);
});
suite('_computeKey', () => {
test('unmodified key', () => {
- assert.strictEqual(element._computeKey(['x']), 'x');
+ 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');
+ assert.strictEqual(element._computeKey(ctrlX), 'x');
+ assert.strictEqual(element._computeKey(shiftMetaX), 'x');
});
});
suite('_computeModifiers', () => {
test('single unmodified key', () => {
- assert.deepEqual(element._computeModifiers(['x']), []);
+ assert.deepEqual(element._computeModifiers(x), []);
});
test('key with modifiers', () => {
- assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']);
- assert.deepEqual(element._computeModifiers(['Shift', 'Meta', 'x']), [
+ assert.deepEqual(element._computeModifiers(ctrlX), ['Ctrl']);
+ assert.deepEqual(element._computeModifiers(shiftMetaX), [
'Shift',
'Meta',
]);
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
index e04c48c..bed46ef 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
@@ -13,6 +13,10 @@
const basicFixture = fixtureFromElement('gr-keyboard-shortcuts-dialog');
+const x = ['x'];
+const ctrlX = ['Ctrl', 'x'];
+const shiftMetaX = ['Shift', 'Meta', 'x'];
+
suite('gr-keyboard-shortcuts-dialog tests', () => {
let element: GrKeyboardShortcutsDialog;
@@ -26,6 +30,83 @@
flush();
}
+ test('renders left and right contents', async () => {
+ const directory = new Map([
+ [
+ ShortcutSection.NAVIGATION,
+ [{binding: [x, ctrlX], text: 'navigation shortcuts'}],
+ ],
+ [
+ ShortcutSection.ACTIONS,
+ [{binding: [shiftMetaX], text: 'navigation shortcuts'}],
+ ],
+ ]);
+ update(directory);
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <header>
+ <h3 class="heading-2">Keyboard shortcuts</h3>
+ <gr-button aria-disabled="false" link="" role="button" tabindex="0">
+ Close
+ </gr-button>
+ </header>
+ <main>
+ <div class="column">
+ <table>
+ <caption class="heading-3">
+ Navigation
+ </caption>
+ <thead>
+ <tr>
+ <th>
+ <strong> Action </strong>
+ </th>
+ <th>
+ <strong> Key </strong>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>navigation shortcuts</td>
+ <td>
+ <gr-key-binding-display> </gr-key-binding-display>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="column">
+ <table>
+ <caption class="heading-3">
+ Actions
+ </caption>
+ <thead>
+ <tr>
+ <th>
+ <strong> Action </strong>
+ </th>
+ <th>
+ <strong> Key </strong>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>navigation shortcuts</td>
+ <td>
+ <gr-key-binding-display> </gr-key-binding-display>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </main>
+ <footer></footer>
+ `);
+ });
+
suite('left and right contents', () => {
test('empty dialog', () => {
assert.isEmpty(element.left);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index b618a76..8134101 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -7,7 +7,6 @@
import {map, distinctUntilChanged} from 'rxjs/operators';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../shared/gr-dropdown/gr-dropdown';
-import '../../shared/gr-icons/gr-icons';
import '../gr-account-dropdown/gr-account-dropdown';
import '../gr-smart-search/gr-smart-search';
import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util';
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index 70034aa..6da1c2c 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -28,6 +28,66 @@
await element.updateComplete;
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <nav>
+ <a class="bigTitle" href="//localhost:9876/">
+ <gr-endpoint-decorator name="header-title">
+ <span class="titleText"> </span>
+ </gr-endpoint-decorator>
+ </a>
+ <ul class="links">
+ <li>
+ <gr-dropdown down-arrow="" horizontal-align="left" link="">
+ <span class="linksTitle" id="Changes"> Changes </span>
+ </gr-dropdown>
+ </li>
+ <li>
+ <gr-dropdown down-arrow="" horizontal-align="left" link="">
+ <span class="linksTitle" id="Browse"> Browse </span>
+ </gr-dropdown>
+ </li>
+ </ul>
+ <div class="rightItems">
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-small-banner"
+ >
+ </gr-endpoint-decorator>
+ <gr-smart-search id="search" label="Search for changes">
+ </gr-smart-search>
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-browse-source"
+ >
+ </gr-endpoint-decorator>
+ <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
+ </gr-endpoint-decorator>
+ </div>
+ <div class="accountContainer" id="accountContainer">
+ <span
+ aria-label="Hide Searchbar"
+ class="material-icon"
+ id="mobileSearch"
+ role="button"
+ >
+ search
+ </span>
+ <a class="loginButton" href="/login"> Sign in </a>
+ <a
+ aria-label="Settings"
+ class="settingsButton"
+ href="/settings/"
+ role="button"
+ title="Settings"
+ >
+ <span class="filled material-icon"> settings </span>
+ </a>
+ </div>
+ </nav>
+ `);
+ });
+
test('link visibility', async () => {
element.loading = true;
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index db46c22..bde1ec7 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -28,6 +28,30 @@
await element.updateComplete;
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <form>
+ <gr-autocomplete
+ allow-non-suggested-values=""
+ id="searchInput"
+ multi=""
+ show-search-icon=""
+ tab-complete=""
+ >
+ <a
+ class="help"
+ href="https://gerrit-review.googlesource.com/documentation/user-search.html"
+ slot="suffix"
+ tabindex="-1"
+ target="_blank"
+ >
+ <span class="material-icon" title="read documentation"> help </span>
+ </a>
+ </gr-autocomplete>
+ </form>
+ `);
+ });
+
test('value is propagated to inputVal', async () => {
element.value = 'foo';
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
index 4eca296..f269200 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.ts
@@ -19,6 +19,12 @@
await element.updateComplete;
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-search-bar id="search"> </gr-search-bar>
+ `);
+ });
+
test('Autocompletes accounts', () => {
stubRestApi('getSuggestedAccounts').callsFake(() =>
Promise.resolve([
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 3f70197..5296f13 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -5,6 +5,7 @@
*/
import '../../../styles/shared-styles';
import '../../shared/gr-dialog/gr-dialog';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-overlay/gr-overlay';
import '../../../embed/diff/gr-diff/gr-diff';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
@@ -30,7 +31,6 @@
import {customElement, property, query, state} from 'lit/decorators';
import {sharedStyles} from '../../../styles/shared-styles';
import {subscribe} from '../../lit/subscription-controller';
-import {iconStyles} from '../../../styles/gr-icon-styles';
interface FilePreview {
filepath: string;
@@ -107,7 +107,6 @@
}
static override styles = [
- iconStyles,
sharedStyles,
css`
gr-diff {
@@ -190,14 +189,14 @@
@click=${this.onPrevFixClick}
?disabled=${id === 0}
>
- <span class="material-icon">chevron_left</span>
+ <gr-icon icon="chevron_left"></gr-icon>
</gr-button>
<gr-button
id="nextFix"
@click=${this.onNextFixClick}
?disabled=${id === fixCount - 1}
>
- <span class="material-icon">chevron_right</span>
+ <gr-icon icon="chevron_right"></gr-icon>
</gr-button>
</div>
`;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index af5df9b..e29731b 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -213,7 +213,7 @@
role="button"
tabindex="-1"
>
- <span class="material-icon">chevron_left</span>
+ <gr-icon icon="chevron_left"></gr-icon>
</gr-button>
<gr-button
aria-disabled="false"
@@ -221,7 +221,7 @@
role="button"
tabindex="0"
>
- <span class="material-icon">chevron_right</span>
+ <gr-icon icon="chevron_right"></gr-icon>
</gr-button>
</div>
</gr-dialog>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index bdb7db7..7570ac5 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -34,6 +34,7 @@
[urlEncodedCommentId: string]: CommentThread;
};
+// TODO: Move file out of elements/ directory
export class ChangeComments {
private readonly _comments: PathToCommentsInfoMap;
@@ -45,10 +46,6 @@
private readonly _portedDrafts: PathToCommentsInfoMap;
- /**
- * Construct a change comments object, which can be data-bound to child
- * elements of that which uses the gr-comment-api.
- */
constructor(
comments?: PathToCommentsInfoMap,
robotComments?: {[path: string]: RobotCommentInfo[]},
@@ -315,7 +312,7 @@
return createCommentThreads(allComments).filter(thread => {
// Robot comments and drafts are not ported over. A human reply to
- // the robot comment will be ported over, thefore it's possible to
+ // the robot comment will be ported over, therefore it's possible to
// have the root comment of the thread not be ported, hence loop over
// entire thread
const portedComment = portedComments.find(portedComment =>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.ts
deleted file mode 100644
index 6f83fe9..0000000
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html``;
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
deleted file mode 100644
index 66949d7..0000000
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
+++ /dev/null
@@ -1,823 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup-karma.js';
-import './gr-comment-api.js';
-import {ChangeComments} from './gr-comment-api.js';
-import {isInRevisionOfPatchRange, isInBaseOfPatchRange, isDraftThread, isUnresolved, createCommentThreads} from '../../../utils/comment-util.js';
-import {createDraft, createComment, createChangeComments, createCommentThread} from '../../../test/test-data-generators.js';
-import {CommentSide} from '../../../constants/constants.js';
-import {PARENT} from '../../../types/common.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-comment-api');
-
-suite('gr-comment-api tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- suite('_changeComment methods', () => {
- setup(() => {
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
- });
-
- suite('ported comments', () => {
- let portedComments;
- let changeComments;
- const comment1 = {
- ...createComment(),
- unresolved: true,
- id: '1',
- line: 136,
- patch_set: 2,
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 1,
- },
- };
-
- const comment2 = {
- ...createComment(),
- patch_set: 2,
- id: '2',
- line: 5,
- };
-
- const comment3 = {
- ...createComment(),
- side: CommentSide.PARENT,
- line: 10,
- unresolved: true,
- };
-
- const comment4 = {
- ...comment3,
- parent: -2,
- };
-
- const draft1 = {
- ...createDraft(),
- id: 'db977012_e1f13828',
- line: 4,
- patch_set: 2,
- };
- const draft2 = {
- ...createDraft(),
- id: '503008e2_0ab203ee',
- line: 11,
- unresolved: true,
- // slightly larger timestamp so it's sorted higher
- updated: '2018-02-13 22:49:48.018000001',
- patch_set: 2,
- };
-
- setup(() => {
- portedComments = {
- 'karma.conf.js': [{
- ...comment1,
- patch_set: 4,
- range: {
- start_line: 136,
- start_character: 16,
- end_line: 136,
- end_character: 29,
- },
- }],
- };
-
- changeComments = new ChangeComments(
- {/* comments */
- 'karma.conf.js': [
- // resolved comment that will not be ported over
- comment2,
- // original comment that will be ported over to patchset 4
- comment1,
- ],
- },
- {}/* robot comments */,
- {}/* drafts */,
- portedComments,
- {}/* ported drafts */
- );
- });
-
- test('threads containing ported comment are returned', () => {
- assert.equal(changeComments.getAllThreadsForChange().length,
- 2);
-
- const portedThreads = changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: PARENT});
-
- assert.equal(portedThreads.length, 1);
- // check that the location of the thread matches the ported comment
- assert.equal(portedThreads[0].patchNum, 4);
- assert.deepEqual(portedThreads[0].range, {
- start_line: 136,
- start_character: 16,
- end_line: 136,
- end_character: 29,
- });
-
- // thread ported over if comparing patchset 1 vs patchset 4
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 1}
- ).length, 1);
-
- // verify ported thread is not returned if original thread will be
- // shown
- // original thread attached to right side
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 2, basePatchNum: PARENT}
- ).length, 0);
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 2, basePatchNum: 1}
- ).length, 0);
-
- // original thread attached to left side
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 3, basePatchNum: 2}
- ).length, 0);
- });
-
- test('threads without any ported comment are filtered out', () => {
- changeComments = new ChangeComments(
- {/* comments */
- // comment that is not ported over
- 'karma.conf.js': [comment2],
- },
- {}/* robot comments */,
- {/* drafts */
- 'karma.conf.js': [draft2],
- },
- // comment1 that is ported over but does not have any thread
- // that has a comment that matches it
- portedComments,
- {}/* ported drafts */
- );
-
- assert.equal(createCommentThreads(changeComments
- .getAllCommentsForPath('karma.conf.js')).length, 1);
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: PARENT}
- ).length, 0);
- });
-
- test('comments with side=PARENT are ported over', () => {
- changeComments = new ChangeComments(
- {/* comments */
- // comment left on Base
- 'karma.conf.js': [comment3],
- },
- {}/* robot comments */,
- {/* drafts */
- 'karma.conf.js': [draft2],
- },
- {/* ported comments */
- 'karma.conf.js': [{
- ...comment3,
- line: 31,
- patch_set: 4,
- }],
- },
- {}/* ported drafts */
- );
-
- const portedThreads = changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: PARENT});
- assert.equal(portedThreads.length, 1);
- assert.equal(portedThreads[0].line, 31);
-
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: -2}
- ).length, 0);
-
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 2}
- ).length, 0);
- });
-
- test('comments left on merge parent is not ported over', () => {
- changeComments = new ChangeComments(
- {/* comments */
- // comment left on Base
- 'karma.conf.js': [comment4],
- },
- {}/* robot comments */,
- {/* drafts */
- 'karma.conf.js': [draft2],
- },
- {/* ported comments */
- 'karma.conf.js': [{
- ...comment4,
- line: 31,
- patch_set: 4,
- }],
- },
- {}/* ported drafts */
- );
-
- const portedThreads = changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: PARENT});
- assert.equal(portedThreads.length, 0);
-
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: -2}
- ).length, 0);
-
- assert.equal(changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 2}
- ).length, 0);
- });
-
- test('ported comments contribute to comment count', () => {
- assert.equal(changeComments.computeCommentsString(
- {basePatchNum: PARENT, patchNum: 2}, 'karma.conf.js',
- {__path: 'karma.conf.js'}), '2 comments (1 unresolved)');
-
- // comment1 is ported over to patchset 4
- assert.equal(changeComments.computeCommentsString(
- {basePatchNum: PARENT, patchNum: 4}, 'karma.conf.js',
- {__path: 'karma.conf.js'}), '1 comment (1 unresolved)');
- });
-
- test('drafts are ported over', () => {
- changeComments = new ChangeComments(
- {}/* comments */,
- {}/* robotComments */,
- {/* drafts */
- // draft1: resolved draft that will be ported over to ps 4
- // draft2: unresolved draft that will be ported over to ps 4
- 'karma.conf.js': [draft1, draft2],
- },
- {}/* ported comments */,
- {/* ported drafts */
- 'karma.conf.js': [
- {
- ...draft1,
- line: 5,
- patch_set: 4,
- },
- {
- ...draft2,
- line: 31,
- patch_set: 4,
- },
- ],
- }
- );
-
- const portedThreads = changeComments._getPortedCommentThreads(
- {path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: PARENT});
-
- // resolved draft is ported over
- assert.equal(portedThreads.length, 2);
- assert.equal(portedThreads[0].line, 5);
- assert.isTrue(isDraftThread(portedThreads[0]));
- assert.isFalse(isUnresolved(portedThreads[0]));
-
- // unresolved draft is ported over
- assert.equal(portedThreads[1].line, 31);
- assert.isTrue(isDraftThread(portedThreads[1]));
- assert.isTrue(isUnresolved(portedThreads[1]));
-
- assert.equal(createCommentThreads(
- changeComments.getAllCommentsForPath('karma.conf.js'),
- {patchNum: 4, basePatchNum: PARENT}).length, 0);
- });
- });
-
- test('_isInBaseOfPatchRange', () => {
- const comment = {patch_set: 1};
- const patchRange = {basePatchNum: 1, patchNum: 2};
- assert.isTrue(isInBaseOfPatchRange(comment,
- patchRange));
-
- patchRange.basePatchNum = PARENT;
- assert.isFalse(isInBaseOfPatchRange(comment,
- patchRange));
-
- comment.side = PARENT;
- assert.isFalse(isInBaseOfPatchRange(comment,
- patchRange));
-
- comment.patch_set = 2;
- assert.isTrue(isInBaseOfPatchRange(comment,
- patchRange));
-
- patchRange.basePatchNum = -2;
- comment.side = PARENT;
- comment.parent = 1;
- assert.isFalse(isInBaseOfPatchRange(comment,
- patchRange));
-
- comment.parent = 2;
- assert.isTrue(isInBaseOfPatchRange(comment,
- patchRange));
- });
-
- test('isInRevisionOfPatchRange', () => {
- const comment = {patch_set: 123};
- const patchRange = {basePatchNum: 122, patchNum: 124};
- assert.isFalse(isInRevisionOfPatchRange(
- comment, patchRange));
-
- patchRange.patchNum = 123;
- assert.isTrue(isInRevisionOfPatchRange(
- comment, patchRange));
-
- comment.side = PARENT;
- assert.isFalse(isInRevisionOfPatchRange(
- comment, patchRange));
- });
-
- suite('comment ranges and paths', () => {
- const commentObjs = {};
- function makeTime(mins) {
- return `2013-02-26 15:0${mins}:43.986000000`;
- }
-
- setup(() => {
- commentObjs['01'] = {
- ...createComment(),
- id: '01',
- patch_set: 2,
- path: 'file/one',
- side: PARENT,
- line: 1,
- updated: makeTime(1),
- range: {
- start_line: 1,
- start_character: 2,
- end_line: 2,
- end_character: 2,
- },
- };
-
- commentObjs['02'] = {
- ...createComment(),
- id: '02',
- in_reply_to: '04',
- patch_set: 2,
- path: 'file/one',
- unresolved: true,
- line: 1,
- updated: makeTime(3),
- };
-
- commentObjs['03'] = {
- ...createComment(),
- id: '03',
- patch_set: 2,
- path: 'file/one',
- side: PARENT,
- line: 2,
- updated: makeTime(1),
- };
-
- commentObjs['04'] = {
- ...createComment(),
- id: '04',
- patch_set: 2,
- path: 'file/one',
- line: 1,
- updated: makeTime(1),
- };
-
- commentObjs['05'] = {
- ...createComment(),
- id: '05',
- patch_set: 2,
- line: 2,
- updated: makeTime(1),
- };
-
- commentObjs['06'] = {
- ...createComment(),
- id: '06',
- patch_set: 3,
- line: 2,
- updated: makeTime(1),
- };
-
- commentObjs['07'] = {
- ...createComment(),
- id: '07',
- patch_set: 2,
- side: PARENT,
- unresolved: false,
- line: 1,
- updated: makeTime(1),
- };
-
- commentObjs['08'] = {
- ...createComment(),
- id: '08',
- patch_set: 2,
- side: PARENT,
- unresolved: true,
- in_reply_to: '07',
- line: 1,
- updated: makeTime(1),
- };
-
- commentObjs['09'] = {
- ...createComment(),
- id: '09',
- patch_set: 3,
- line: 1,
- updated: makeTime(1),
- };
-
- commentObjs['10'] = {
- ...createComment(),
- id: '10',
- patch_set: 5,
- side: PARENT,
- line: 1,
- updated: makeTime(1),
- };
-
- commentObjs['11'] = {
- ...createComment(),
- id: '11',
- patch_set: 5,
- line: 1,
- updated: makeTime(1),
- };
-
- commentObjs['12'] = {
- ...createDraft(),
- id: '12',
- patch_set: 2,
- side: PARENT,
- line: 1,
- updated: makeTime(3),
- path: 'file/one',
- };
-
- commentObjs['13'] = {
- ...createDraft(),
- id: '13',
- in_reply_to: '04',
- patch_set: 2,
- line: 1,
- // Draft gets lower timestamp than published comment, because we
- // want to test that the draft still gets sorted to the end.
- updated: makeTime(2),
- path: 'file/one',
- };
-
- commentObjs['14'] = {
- ...createDraft(),
- id: '14',
- patch_set: 3,
- line: 1,
- path: 'file/two',
- updated: makeTime(3),
- };
-
- const drafts = {
- 'file/one': [
- commentObjs['12'],
- commentObjs['13'],
- ],
- 'file/two': [
- commentObjs['14'],
- ],
- };
- const robotComments = {
- 'file/one': [
- commentObjs['01'], commentObjs['02'],
- ],
- };
- const comments = {
- 'file/one': [commentObjs['03'], commentObjs['04']],
- 'file/two': [commentObjs['05'], commentObjs['06']],
- 'file/three': [commentObjs['07'], commentObjs['08'],
- commentObjs['09']],
- 'file/four': [commentObjs['10'], commentObjs['11']],
- };
- element._changeComments =
- new ChangeComments(comments, robotComments, drafts, {}, {});
- });
-
- 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('getCommentsForPath', () => {
- const patchRange = {basePatchNum: 1, patchNum: 3};
- let path = 'file/one';
- let comments = element._changeComments.getCommentsForPath(path,
- patchRange);
- assert.equal(comments.filter(c => isInBaseOfPatchRange(c, patchRange))
- .length, 0);
- assert.equal(comments.filter(c => isInRevisionOfPatchRange(c,
- patchRange)).length, 0);
-
- path = 'file/two';
- comments = element._changeComments.getCommentsForPath(path,
- patchRange);
- assert.equal(comments.filter(c => isInBaseOfPatchRange(c, patchRange))
- .length, 0);
- assert.equal(comments.filter(c => isInRevisionOfPatchRange(c,
- patchRange)).length, 2);
-
- patchRange.basePatchNum = 2;
- comments = element._changeComments.getCommentsForPath(path,
- patchRange);
- assert.equal(comments.filter(c => isInBaseOfPatchRange(c,
- patchRange)).length, 1);
- assert.equal(comments.filter(c => isInRevisionOfPatchRange(c,
- patchRange)).length, 2);
-
- patchRange.basePatchNum = PARENT;
- path = 'file/three';
- comments = element._changeComments.getCommentsForPath(path,
- patchRange);
- assert.equal(comments.filter(c => isInBaseOfPatchRange(c, patchRange))
- .length, 0);
- assert.equal(comments.filter(c => isInRevisionOfPatchRange(c,
- patchRange)).length, 1);
- });
-
- test('getAllCommentsForPath', () => {
- let path = 'file/one';
- let comments = element._changeComments.getAllCommentsForPath(path);
- assert.equal(comments.length, 4);
- path = 'file/two';
- comments = element._changeComments.getAllCommentsForPath(path, 2);
- assert.equal(comments.length, 1);
- const aCopyOfComments = element._changeComments
- .getAllCommentsForPath(path, 2);
- assert.deepEqual(comments, aCopyOfComments);
- assert.notEqual(comments[0], aCopyOfComments[0]);
- });
-
- test('getAllDraftsForPath', () => {
- const path = 'file/one';
- const drafts = element._changeComments.getAllDraftsForPath(path);
- assert.equal(drafts.length, 2);
- });
-
- test('computeUnresolvedNum', () => {
- assert.equal(element._changeComments
- .computeUnresolvedNum({
- patchNum: 2,
- path: 'file/one',
- }), 0);
- assert.equal(element._changeComments
- .computeUnresolvedNum({
- patchNum: 1,
- path: 'file/one',
- }), 0);
- assert.equal(element._changeComments
- .computeUnresolvedNum({
- patchNum: 2,
- path: 'file/three',
- }), 1);
- });
-
- test('computeUnresolvedNum w/ non-linear thread', () => {
- const 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,
- }],
- };
- element._changeComments = new ChangeComments(comments, {}, {}, 1234);
- assert.equal(
- element._changeComments.computeUnresolvedNum(1, 'path'), 0);
- });
-
- test('computeCommentsString', () => {
- const changeComments = createChangeComments();
- const parentTo1 = {
- basePatchNum: PARENT,
- patchNum: 1,
- };
- const parentTo2 = {
- basePatchNum: PARENT,
- patchNum: 2,
- };
- const _1To2 = {
- basePatchNum: 1,
- patchNum: 2,
- };
-
- assert.equal(
- changeComments.computeCommentsString(parentTo1, '/COMMIT_MSG',
- {__path: '/COMMIT_MSG'}), '2 comments (1 unresolved)');
- assert.equal(
- changeComments.computeCommentsString(parentTo1, '/COMMIT_MSG',
- {__path: '/COMMIT_MSG', status: 'U'}, true),
- '2 comments (1 unresolved)(no changes)');
- assert.equal(
- changeComments.computeCommentsString(_1To2, '/COMMIT_MSG',
- {__path: '/COMMIT_MSG'}), '3 comments (1 unresolved)');
-
- assert.equal(
- changeComments.computeCommentsString(parentTo1, 'myfile.txt',
- {__path: 'myfile.txt'}), '1 comment');
- assert.equal(
- changeComments.computeCommentsString(_1To2, 'myfile.txt',
- {__path: 'myfile.txt'}), '3 comments');
-
- assert.equal(
- changeComments.computeCommentsString(parentTo1,
- 'file_added_in_rev2.txt',
- {__path: 'file_added_in_rev2.txt'}), '');
- assert.equal(
- changeComments.computeCommentsString(_1To2,
- 'file_added_in_rev2.txt',
- {__path: 'file_added_in_rev2.txt'}), '');
-
- assert.equal(
- changeComments.computeCommentsString(parentTo2, '/COMMIT_MSG',
- {__path: '/COMMIT_MSG'}), '1 comment');
- assert.equal(
- changeComments.computeCommentsString(_1To2, '/COMMIT_MSG',
- {__path: '/COMMIT_MSG'}), '3 comments (1 unresolved)');
-
- assert.equal(
- changeComments.computeCommentsString(parentTo2, 'myfile.txt',
- {__path: 'myfile.txt'}), '2 comments');
- assert.equal(
- changeComments.computeCommentsString(_1To2, 'myfile.txt',
- {__path: 'myfile.txt'}), '3 comments');
-
- assert.equal(
- changeComments.computeCommentsString(parentTo2,
- 'file_added_in_rev2.txt',
- {__path: 'file_added_in_rev2.txt'}), '');
- assert.equal(
- changeComments.computeCommentsString(_1To2,
- 'file_added_in_rev2.txt',
- {__path: 'file_added_in_rev2.txt'}), '');
- assert.equal(
- changeComments.computeCommentsString(parentTo2, 'unresolved.file',
- {__path: 'unresolved.file'}), '2 comments (1 unresolved)');
- assert.equal(
- changeComments.computeCommentsString(_1To2, 'unresolved.file',
- {__path: 'unresolved.file'}), '2 comments (1 unresolved)');
- });
-
- test('computeCommentThreadCount', () => {
- assert.equal(element._changeComments
- .computeCommentThreadCount({
- patchNum: 2,
- path: 'file/one',
- }), 3);
- assert.equal(element._changeComments
- .computeCommentThreadCount({
- patchNum: 1,
- path: 'file/one',
- }), 0);
- assert.equal(element._changeComments
- .computeCommentThreadCount({
- patchNum: 2,
- path: 'file/three',
- }), 1);
- });
-
- test('computeDraftCount', () => {
- assert.equal(element._changeComments
- .computeDraftCount({
- patchNum: 2,
- path: 'file/one',
- }), 2);
- assert.equal(element._changeComments
- .computeDraftCount({
- patchNum: 1,
- path: 'file/one',
- }), 0);
- assert.equal(element._changeComments
- .computeDraftCount({
- patchNum: 2,
- path: '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 = [
- {
- ...createCommentThread([{...commentObjs['01'], path: 'file/one'}]),
- }, {
- ...createCommentThread([{...commentObjs['03'], path: 'file/one'}]),
- }, {
- ...createCommentThread([{...commentObjs['04'], path: 'file/one'},
- {...commentObjs['02'], path: 'file/one'},
- {...commentObjs['13'], path: 'file/one'}]),
- }, {
- ...createCommentThread([{...commentObjs['05'], path: 'file/two'}]),
- }, {
- ...createCommentThread([{...commentObjs['06'], path: 'file/two'}]),
- }, {
- ...createCommentThread([{...commentObjs['07'], path: 'file/three'},
- {...commentObjs['08'], path: 'file/three'}]),
- }, {
- ...createCommentThread([{...commentObjs['09'], path: 'file/three'}]
- ),
- }, {
- ...createCommentThread([{...commentObjs['10'], path: 'file/four'}]),
- }, {
- ...createCommentThread([{...commentObjs['11'], path: 'file/four'}]),
- }, {
- ...createCommentThread([{...commentObjs['12'], path: 'file/one'}]),
- }, {
- ...createCommentThread([{...commentObjs['14'], path: 'file/two'}]),
- },
- ];
- const threads = element._changeComments.getAllThreadsForChange();
- assert.deepEqual(threads, expectedThreads);
- });
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.ts
new file mode 100644
index 0000000..b195762
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.ts
@@ -0,0 +1,1042 @@
+/**
+ * @license
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {ChangeComments} from './gr-comment-api';
+import {
+ isInRevisionOfPatchRange,
+ isInBaseOfPatchRange,
+ isDraftThread,
+ isUnresolved,
+ createCommentThreads,
+ DraftInfo,
+ CommentThread,
+} from '../../../utils/comment-util';
+import {
+ createDraft,
+ createComment,
+ createChangeComments,
+ createCommentThread,
+ createFileInfo,
+ createRobotComment,
+} from '../../../test/test-data-generators';
+import {CommentSide, FileInfoStatus} from '../../../constants/constants';
+import {
+ BasePatchSetNum,
+ CommentInfo,
+ PARENT,
+ PatchRange,
+ PatchSetNum,
+ PathToCommentsInfoMap,
+ RevisionPatchSetNum,
+ RobotCommentInfo,
+ Timestamp,
+ UrlEncodedCommentId,
+} from '../../../types/common';
+import {stubRestApi} from '../../../test/test-utils';
+
+suite('ChangeComments tests', () => {
+ let changeComments: ChangeComments;
+
+ suite('_changeComment methods', () => {
+ setup(() => {
+ stubRestApi('getDiffComments').resolves({});
+ stubRestApi('getDiffRobotComments').resolves({});
+ stubRestApi('getDiffDrafts').resolves({});
+ });
+
+ suite('ported comments', () => {
+ let portedComments: PathToCommentsInfoMap;
+ const comment1: CommentInfo = {
+ ...createComment(),
+ unresolved: true,
+ id: '1' as UrlEncodedCommentId,
+ line: 136,
+ patch_set: 2 as RevisionPatchSetNum,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 1,
+ },
+ };
+
+ const comment2: CommentInfo = {
+ ...createComment(),
+ patch_set: 2 as RevisionPatchSetNum,
+ id: '2' as UrlEncodedCommentId,
+ line: 5,
+ };
+
+ const comment3: CommentInfo = {
+ ...createComment(),
+ side: CommentSide.PARENT,
+ line: 10,
+ unresolved: true,
+ };
+
+ const comment4: CommentInfo = {
+ ...comment3,
+ parent: -2,
+ };
+
+ const draft1: DraftInfo = {
+ ...createDraft(),
+ id: 'db977012_e1f13828' as UrlEncodedCommentId,
+ line: 4,
+ patch_set: 2 as RevisionPatchSetNum,
+ };
+ const draft2: DraftInfo = {
+ ...createDraft(),
+ id: '503008e2_0ab203ee' as UrlEncodedCommentId,
+ line: 11,
+ unresolved: true,
+ // slightly larger timestamp so it's sorted higher
+ updated: '2018-02-13 22:49:48.018000001' as Timestamp,
+ patch_set: 2 as RevisionPatchSetNum,
+ };
+
+ setup(() => {
+ portedComments = {
+ 'karma.conf.js': [
+ {
+ ...comment1,
+ patch_set: 4 as RevisionPatchSetNum,
+ range: {
+ start_line: 136,
+ start_character: 16,
+ end_line: 136,
+ end_character: 29,
+ },
+ },
+ ],
+ };
+
+ changeComments = new ChangeComments(
+ {
+ /* comments */
+ 'karma.conf.js': [
+ // resolved comment that will not be ported over
+ comment2,
+ // original comment that will be ported over to patchset 4
+ comment1,
+ ],
+ },
+ {} /* robot comments */,
+ {} /* drafts */,
+ portedComments,
+ {} /* ported drafts */
+ );
+ });
+
+ test('threads containing ported comment are returned', () => {
+ assert.equal(changeComments.getAllThreadsForChange().length, 2);
+
+ const portedThreads = changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {patchNum: 4 as RevisionPatchSetNum, basePatchNum: PARENT}
+ );
+
+ assert.equal(portedThreads.length, 1);
+ // check that the location of the thread matches the ported comment
+ assert.equal(portedThreads[0].patchNum, 4 as RevisionPatchSetNum);
+ assert.deepEqual(portedThreads[0].range, {
+ start_line: 136,
+ start_character: 16,
+ end_line: 136,
+ end_character: 29,
+ });
+
+ // thread ported over if comparing patchset 1 vs patchset 4
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: 1 as BasePatchSetNum,
+ }
+ ).length,
+ 1
+ );
+
+ // verify ported thread is not returned if original thread will be
+ // shown
+ // original thread attached to right side
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {patchNum: 2 as RevisionPatchSetNum, basePatchNum: PARENT}
+ ).length,
+ 0
+ );
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {
+ patchNum: 2 as RevisionPatchSetNum,
+ basePatchNum: 1 as BasePatchSetNum,
+ }
+ ).length,
+ 0
+ );
+
+ // original thread attached to left side
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {
+ patchNum: 3 as RevisionPatchSetNum,
+ basePatchNum: 2 as BasePatchSetNum,
+ }
+ ).length,
+ 0
+ );
+ });
+
+ test('threads without any ported comment are filtered out', () => {
+ changeComments = new ChangeComments(
+ {
+ /* comments */
+ // comment that is not ported over
+ 'karma.conf.js': [comment2],
+ },
+ {} /* robot comments */,
+ {
+ /* drafts */ 'karma.conf.js': [draft2],
+ },
+ // comment1 that is ported over but does not have any thread
+ // that has a comment that matches it
+ portedComments,
+ {} /* ported drafts */
+ );
+
+ assert.equal(
+ createCommentThreads(
+ changeComments.getAllCommentsForPath('karma.conf.js')
+ ).length,
+ 1
+ );
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {patchNum: 4 as RevisionPatchSetNum, basePatchNum: PARENT}
+ ).length,
+ 0
+ );
+ });
+
+ test('comments with side=PARENT are ported over', () => {
+ changeComments = new ChangeComments(
+ {
+ /* comments */
+ // comment left on Base
+ 'karma.conf.js': [comment3],
+ },
+ {} /* robot comments */,
+ {
+ /* drafts */ 'karma.conf.js': [draft2],
+ },
+ {
+ /* ported comments */
+ 'karma.conf.js': [
+ {
+ ...comment3,
+ line: 31,
+ patch_set: 4 as RevisionPatchSetNum,
+ },
+ ],
+ },
+ {} /* ported drafts */
+ );
+
+ const portedThreads = changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {patchNum: 4 as RevisionPatchSetNum, basePatchNum: PARENT}
+ );
+ assert.equal(portedThreads.length, 1);
+ assert.equal(portedThreads[0].line, 31);
+
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: -2 as BasePatchSetNum,
+ }
+ ).length,
+ 0
+ );
+
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: 2 as BasePatchSetNum,
+ }
+ ).length,
+ 0
+ );
+ });
+
+ test('comments left on merge parent is not ported over', () => {
+ changeComments = new ChangeComments(
+ {
+ /* comments */
+ // comment left on Base
+ 'karma.conf.js': [comment4],
+ },
+ {} /* robot comments */,
+ {
+ /* drafts */ 'karma.conf.js': [draft2],
+ },
+ {
+ /* ported comments */
+ 'karma.conf.js': [
+ {
+ ...comment4,
+ line: 31,
+ patch_set: 4 as RevisionPatchSetNum,
+ },
+ ],
+ },
+ {} /* ported drafts */
+ );
+
+ const portedThreads = changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {patchNum: 4 as RevisionPatchSetNum, basePatchNum: PARENT}
+ );
+ assert.equal(portedThreads.length, 0);
+
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: -2 as BasePatchSetNum,
+ }
+ ).length,
+ 0
+ );
+
+ assert.equal(
+ changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: 2 as BasePatchSetNum,
+ }
+ ).length,
+ 0
+ );
+ });
+
+ test('ported comments contribute to comment count', () => {
+ const fileInfo = createFileInfo();
+ assert.equal(
+ changeComments.computeCommentsString(
+ {basePatchNum: PARENT, patchNum: 2 as RevisionPatchSetNum},
+ 'karma.conf.js',
+ fileInfo
+ ),
+ '2 comments (1 unresolved)'
+ );
+
+ // comment1 is ported over to patchset 4
+ assert.equal(
+ changeComments.computeCommentsString(
+ {basePatchNum: PARENT, patchNum: 4 as RevisionPatchSetNum},
+ 'karma.conf.js',
+ fileInfo
+ ),
+ '1 comment (1 unresolved)'
+ );
+ });
+
+ test('drafts are ported over', () => {
+ changeComments = new ChangeComments(
+ {} /* comments */,
+ {} /* robotComments */,
+ {
+ /* drafts */
+ // draft1: resolved draft that will be ported over to ps 4
+ // draft2: unresolved draft that will be ported over to ps 4
+ 'karma.conf.js': [draft1, draft2],
+ },
+ {} /* ported comments */,
+ {
+ /* ported drafts */
+ 'karma.conf.js': [
+ {
+ ...draft1,
+ line: 5,
+ patch_set: 4 as RevisionPatchSetNum,
+ },
+ {
+ ...draft2,
+ line: 31,
+ patch_set: 4 as RevisionPatchSetNum,
+ },
+ ],
+ }
+ );
+
+ const portedThreads = changeComments._getPortedCommentThreads(
+ {path: 'karma.conf.js'},
+ {patchNum: 4 as RevisionPatchSetNum, basePatchNum: PARENT}
+ );
+
+ // resolved draft is ported over
+ assert.equal(portedThreads.length, 2);
+ assert.equal(portedThreads[0].line, 5);
+ assert.isTrue(isDraftThread(portedThreads[0]));
+ assert.isFalse(isUnresolved(portedThreads[0]));
+
+ // unresolved draft is ported over
+ assert.equal(portedThreads[1].line, 31);
+ assert.isTrue(isDraftThread(portedThreads[1]));
+ assert.isTrue(isUnresolved(portedThreads[1]));
+
+ assert.equal(
+ createCommentThreads(
+ changeComments.getAllCommentsForPath('karma.conf.js')
+ ).length,
+ 0
+ );
+ });
+ });
+
+ test('_isInBaseOfPatchRange', () => {
+ const comment: {
+ patch_set?: PatchSetNum;
+ side?: CommentSide;
+ parent?: number;
+ } = {patch_set: 1 as PatchSetNum};
+ const patchRange = {
+ basePatchNum: 1 as BasePatchSetNum,
+ patchNum: 2 as RevisionPatchSetNum,
+ };
+ assert.isTrue(isInBaseOfPatchRange(comment, patchRange));
+
+ patchRange.basePatchNum = PARENT;
+ assert.isFalse(isInBaseOfPatchRange(comment, patchRange));
+
+ comment.side = CommentSide.PARENT;
+ assert.isFalse(isInBaseOfPatchRange(comment, patchRange));
+
+ comment.patch_set = 2 as PatchSetNum;
+ assert.isTrue(isInBaseOfPatchRange(comment, patchRange));
+
+ patchRange.basePatchNum = -2 as BasePatchSetNum;
+ comment.side = CommentSide.PARENT;
+ comment.parent = 1;
+ assert.isFalse(isInBaseOfPatchRange(comment, patchRange));
+
+ comment.parent = 2;
+ assert.isTrue(isInBaseOfPatchRange(comment, patchRange));
+ });
+
+ test('isInRevisionOfPatchRange', () => {
+ const comment: {
+ patch_set?: PatchSetNum;
+ side?: CommentSide;
+ } = {patch_set: 123 as PatchSetNum};
+ const patchRange: PatchRange = {
+ basePatchNum: 122 as BasePatchSetNum,
+ patchNum: 124 as RevisionPatchSetNum,
+ };
+ assert.isFalse(isInRevisionOfPatchRange(comment, patchRange));
+
+ patchRange.patchNum = 123 as RevisionPatchSetNum;
+ assert.isTrue(isInRevisionOfPatchRange(comment, patchRange));
+
+ comment.side = CommentSide.PARENT;
+ assert.isFalse(isInRevisionOfPatchRange(comment, patchRange));
+ });
+
+ suite('comment ranges and paths', () => {
+ const comments = [
+ {
+ ...createRobotComment(),
+ id: '01' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ path: 'file/one',
+ side: CommentSide.PARENT,
+ line: 1,
+ updated: makeTime(1),
+ range: {
+ start_line: 1,
+ start_character: 2,
+ end_line: 2,
+ end_character: 2,
+ },
+ },
+ {
+ ...createRobotComment(),
+ id: '02' as UrlEncodedCommentId,
+ in_reply_to: '04' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ path: 'file/one',
+ unresolved: true,
+ line: 1,
+ updated: makeTime(3),
+ },
+ {
+ ...createComment(),
+ id: '03' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ path: 'file/one',
+ side: CommentSide.PARENT,
+ line: 2,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '04' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ path: 'file/one',
+ line: 1,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '05' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ line: 2,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '06' as UrlEncodedCommentId,
+ patch_set: 3 as RevisionPatchSetNum,
+ line: 2,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '07' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ side: CommentSide.PARENT,
+ unresolved: false,
+ line: 1,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '08' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ side: CommentSide.PARENT,
+ unresolved: true,
+ in_reply_to: '07' as UrlEncodedCommentId,
+ line: 1,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '09' as UrlEncodedCommentId,
+ patch_set: 3 as RevisionPatchSetNum,
+ line: 1,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '10' as UrlEncodedCommentId,
+ patch_set: 5 as RevisionPatchSetNum,
+ side: CommentSide.PARENT,
+ line: 1,
+ updated: makeTime(1),
+ },
+ {
+ ...createComment(),
+ id: '11' as UrlEncodedCommentId,
+ patch_set: 5 as RevisionPatchSetNum,
+ line: 1,
+ updated: makeTime(1),
+ },
+ {
+ ...createDraft(),
+ id: '12' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ side: CommentSide.PARENT,
+ line: 1,
+ updated: makeTime(3),
+ path: 'file/one',
+ },
+ {
+ ...createDraft(),
+ id: '13' as UrlEncodedCommentId,
+ in_reply_to: '04' as UrlEncodedCommentId,
+ patch_set: 2 as RevisionPatchSetNum,
+ line: 1,
+ // Draft gets lower timestamp than published comment, because we
+ // want to test that the draft still gets sorted to the end.
+ updated: makeTime(2),
+ path: 'file/one',
+ },
+ {
+ ...createDraft(),
+ id: '14' as UrlEncodedCommentId,
+ patch_set: 3 as RevisionPatchSetNum,
+ line: 1,
+ path: 'file/two',
+ updated: makeTime(3),
+ },
+ ] as const;
+ const drafts: {[path: string]: DraftInfo[]} = {
+ 'file/one': [comments[11], comments[12]],
+ 'file/two': [comments[13]],
+ };
+ const robotComments: {[path: string]: RobotCommentInfo[]} = {
+ 'file/one': [comments[0], comments[1]],
+ };
+ const commentsByFile: PathToCommentsInfoMap = {
+ 'file/one': [comments[2], comments[3]],
+ 'file/two': [comments[4], comments[5]],
+ 'file/three': [comments[6], comments[7], comments[8]],
+ 'file/four': [comments[9], comments[10]],
+ };
+
+ function makeTime(mins: number) {
+ return `2013-02-26 15:0${mins}:43.986000000` as Timestamp;
+ }
+
+ setup(() => {
+ changeComments = new ChangeComments(
+ commentsByFile,
+ robotComments,
+ drafts,
+ {} /* portedComments */,
+ {} /* portedDrafts */
+ );
+ });
+
+ test('getPaths', () => {
+ const patchRange: PatchRange = {
+ basePatchNum: 1 as BasePatchSetNum,
+ patchNum: 4 as RevisionPatchSetNum,
+ };
+ let paths = changeComments.getPaths(patchRange);
+ assert.equal(Object.keys(paths).length, 0);
+
+ patchRange.basePatchNum = PARENT;
+ patchRange.patchNum = 3 as RevisionPatchSetNum;
+ paths = 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 as RevisionPatchSetNum;
+ paths = changeComments.getPaths(patchRange);
+ assert.property(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.notProperty(paths, 'file/four');
+
+ paths = changeComments.getPaths();
+ assert.property(paths, 'file/one');
+ assert.property(paths, 'file/two');
+ assert.property(paths, 'file/three');
+ assert.property(paths, 'file/four');
+ });
+
+ test('getCommentsForPath', () => {
+ const patchRange: PatchRange = {
+ basePatchNum: 1 as BasePatchSetNum,
+ patchNum: 3 as RevisionPatchSetNum,
+ };
+ let path = 'file/one';
+ let comments = changeComments.getCommentsForPath(path, patchRange);
+ assert.equal(
+ comments.filter(c => isInBaseOfPatchRange(c, patchRange)).length,
+ 0
+ );
+ assert.equal(
+ comments.filter(c => isInRevisionOfPatchRange(c, patchRange)).length,
+ 0
+ );
+
+ path = 'file/two';
+ comments = changeComments.getCommentsForPath(path, patchRange);
+ assert.equal(
+ comments.filter(c => isInBaseOfPatchRange(c, patchRange)).length,
+ 0
+ );
+ assert.equal(
+ comments.filter(c => isInRevisionOfPatchRange(c, patchRange)).length,
+ 2
+ );
+
+ patchRange.basePatchNum = 2 as BasePatchSetNum;
+ comments = changeComments.getCommentsForPath(path, patchRange);
+ assert.equal(
+ comments.filter(c => isInBaseOfPatchRange(c, patchRange)).length,
+ 1
+ );
+ assert.equal(
+ comments.filter(c => isInRevisionOfPatchRange(c, patchRange)).length,
+ 2
+ );
+
+ patchRange.basePatchNum = PARENT;
+ path = 'file/three';
+ comments = changeComments.getCommentsForPath(path, patchRange);
+ assert.equal(
+ comments.filter(c => isInBaseOfPatchRange(c, patchRange)).length,
+ 0
+ );
+ assert.equal(
+ comments.filter(c => isInRevisionOfPatchRange(c, patchRange)).length,
+ 1
+ );
+ });
+
+ test('getAllCommentsForPath', () => {
+ let path = 'file/one';
+ let comments = changeComments.getAllCommentsForPath(path);
+ assert.equal(comments.length, 4);
+ path = 'file/two';
+ comments = changeComments.getAllCommentsForPath(path, 2 as PatchSetNum);
+ assert.equal(comments.length, 1);
+ const aCopyOfComments = changeComments.getAllCommentsForPath(
+ path,
+ 2 as PatchSetNum
+ );
+ assert.deepEqual(comments, aCopyOfComments);
+ assert.notEqual(comments[0], aCopyOfComments[0]);
+ });
+
+ test('getAllDraftsForPath', () => {
+ const path = 'file/one';
+ const drafts = changeComments.getAllDraftsForPath(path);
+ assert.equal(drafts.length, 2);
+ });
+
+ test('computeUnresolvedNum', () => {
+ assert.equal(
+ changeComments.computeUnresolvedNum({
+ patchNum: 2 as PatchSetNum,
+ path: 'file/one',
+ }),
+ 0
+ );
+ assert.equal(
+ changeComments.computeUnresolvedNum({
+ patchNum: 1 as PatchSetNum,
+ path: 'file/one',
+ }),
+ 0
+ );
+ assert.equal(
+ changeComments.computeUnresolvedNum({
+ patchNum: 2 as PatchSetNum,
+ path: 'file/three',
+ }),
+ 1
+ );
+ });
+
+ test('computeUnresolvedNum w/ non-linear thread', () => {
+ const comments: PathToCommentsInfoMap = {
+ path: [
+ {
+ id: '9c6ba3c6_28b7d467' as UrlEncodedCommentId,
+ patch_set: 1 as RevisionPatchSetNum,
+ updated: '2018-02-28 14:41:13.000000000' as Timestamp,
+ unresolved: true,
+ },
+ {
+ id: '3df7b331_0bead405' as UrlEncodedCommentId,
+ patch_set: 1 as RevisionPatchSetNum,
+ in_reply_to: '1c346623_ab85d14a' as UrlEncodedCommentId,
+ updated: '2018-02-28 23:07:55.000000000' as Timestamp,
+ unresolved: false,
+ },
+ {
+ id: '6153dce6_69958d1e' as UrlEncodedCommentId,
+ patch_set: 1 as RevisionPatchSetNum,
+ in_reply_to: '9c6ba3c6_28b7d467' as UrlEncodedCommentId,
+ updated: '2018-02-28 17:11:31.000000000' as Timestamp,
+ unresolved: true,
+ },
+ {
+ id: '1c346623_ab85d14a' as UrlEncodedCommentId,
+ patch_set: 1 as RevisionPatchSetNum,
+ in_reply_to: '9c6ba3c6_28b7d467' as UrlEncodedCommentId,
+ updated: '2018-02-28 23:01:39.000000000' as Timestamp,
+ unresolved: false,
+ },
+ ],
+ };
+ changeComments = new ChangeComments(comments, {}, {}, {});
+ assert.equal(
+ changeComments.computeUnresolvedNum(
+ {patchNum: 1 as PatchSetNum},
+ true
+ ),
+ 0
+ );
+ });
+
+ test('computeCommentsString', () => {
+ const changeComments = createChangeComments();
+ const parentTo1: PatchRange = {
+ basePatchNum: PARENT,
+ patchNum: 1 as RevisionPatchSetNum,
+ };
+ const parentTo2: PatchRange = {
+ basePatchNum: PARENT,
+ patchNum: 2 as RevisionPatchSetNum,
+ };
+ const _1To2: PatchRange = {
+ basePatchNum: 1 as BasePatchSetNum,
+ patchNum: 2 as RevisionPatchSetNum,
+ };
+ const fileInfo = createFileInfo();
+
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo1,
+ '/COMMIT_MSG',
+ fileInfo
+ ),
+ '2 comments (1 unresolved)'
+ );
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo1,
+ '/COMMIT_MSG',
+ {...fileInfo, status: FileInfoStatus.UNMODIFIED},
+ true
+ ),
+ '2 comments (1 unresolved)(no changes)'
+ );
+ assert.equal(
+ changeComments.computeCommentsString(_1To2, '/COMMIT_MSG', fileInfo),
+ '3 comments (1 unresolved)'
+ );
+
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo1,
+ 'myfile.txt',
+ fileInfo
+ ),
+ '1 comment'
+ );
+ assert.equal(
+ changeComments.computeCommentsString(_1To2, 'myfile.txt', fileInfo),
+ '3 comments'
+ );
+
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo1,
+ 'file_added_in_rev2.txt',
+ fileInfo
+ ),
+ ''
+ );
+ assert.equal(
+ changeComments.computeCommentsString(
+ _1To2,
+ 'file_added_in_rev2.txt',
+ fileInfo
+ ),
+ ''
+ );
+
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo2,
+ '/COMMIT_MSG',
+ fileInfo
+ ),
+
+ '1 comment'
+ );
+ assert.equal(
+ changeComments.computeCommentsString(_1To2, '/COMMIT_MSG', fileInfo),
+ '3 comments (1 unresolved)'
+ );
+
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo2,
+ 'myfile.txt',
+ fileInfo
+ ),
+ '2 comments'
+ );
+ assert.equal(
+ changeComments.computeCommentsString(_1To2, 'myfile.txt', fileInfo),
+ '3 comments'
+ );
+
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo2,
+ 'file_added_in_rev2.txt',
+ fileInfo
+ ),
+ ''
+ );
+ assert.equal(
+ changeComments.computeCommentsString(
+ _1To2,
+ 'file_added_in_rev2.txt',
+ fileInfo
+ ),
+ ''
+ );
+ assert.equal(
+ changeComments.computeCommentsString(
+ parentTo2,
+ 'unresolved.file',
+ fileInfo
+ ),
+ '2 comments (1 unresolved)'
+ );
+ assert.equal(
+ changeComments.computeCommentsString(
+ _1To2,
+ 'unresolved.file',
+ fileInfo
+ ),
+ '2 comments (1 unresolved)'
+ );
+ });
+
+ test('computeCommentThreadCount', () => {
+ assert.equal(
+ changeComments.computeCommentThreadCount({
+ patchNum: 2 as PatchSetNum,
+ path: 'file/one',
+ }),
+ 3
+ );
+ assert.equal(
+ changeComments.computeCommentThreadCount({
+ patchNum: 1 as PatchSetNum,
+ path: 'file/one',
+ }),
+ 0
+ );
+ assert.equal(
+ changeComments.computeCommentThreadCount({
+ patchNum: 2 as PatchSetNum,
+ path: 'file/three',
+ }),
+ 1
+ );
+ });
+
+ test('computeDraftCount', () => {
+ assert.equal(
+ changeComments.computeDraftCount({
+ patchNum: 2 as PatchSetNum,
+ path: 'file/one',
+ }),
+ 2
+ );
+ assert.equal(
+ changeComments.computeDraftCount({
+ patchNum: 1 as PatchSetNum,
+ path: 'file/one',
+ }),
+ 0
+ );
+ assert.equal(
+ changeComments.computeDraftCount({
+ patchNum: 2 as PatchSetNum,
+ path: 'file/three',
+ }),
+ 0
+ );
+ assert.equal(changeComments.computeDraftCount(), 3);
+ });
+
+ test('getAllPublishedComments', () => {
+ let publishedComments = 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 = changeComments.getAllPublishedComments(
+ 2 as PatchSetNum
+ );
+ assert.equal(Object.keys(publishedComments['file/one']).length, 4);
+ assert.equal(Object.keys(publishedComments['file/two']).length, 1);
+ });
+
+ test('getAllComments', () => {
+ let comments = 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 = changeComments.getAllComments(false, 2 as PatchSetNum);
+ 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 = 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 = changeComments.getAllComments(true, 2 as PatchSetNum);
+ 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: CommentThread[] = [
+ {
+ ...createCommentThread([{...comments[0], path: 'file/one'}]),
+ },
+ {
+ ...createCommentThread([{...comments[2], path: 'file/one'}]),
+ },
+ {
+ ...createCommentThread([
+ {...comments[3], path: 'file/one'},
+ {...comments[1], path: 'file/one'},
+ {...comments[12], path: 'file/one'},
+ ]),
+ },
+ {
+ ...createCommentThread([{...comments[4], path: 'file/two'}]),
+ },
+ {
+ ...createCommentThread([{...comments[5], path: 'file/two'}]),
+ },
+ {
+ ...createCommentThread([
+ {...comments[6], path: 'file/three'},
+ {...comments[7], path: 'file/three'},
+ ]),
+ },
+ {
+ ...createCommentThread([{...comments[8], path: 'file/three'}]),
+ },
+ {
+ ...createCommentThread([{...comments[9], path: 'file/four'}]),
+ },
+ {
+ ...createCommentThread([{...comments[10], path: 'file/four'}]),
+ },
+ {
+ ...createCommentThread([{...comments[11], path: 'file/one'}]),
+ },
+ {
+ ...createCommentThread([{...comments[13], path: 'file/two'}]),
+ },
+ ];
+ const threads = changeComments.getAllThreadsForChange();
+ assert.deepEqual(threads, expectedThreads);
+ });
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index ba87e3e..aff8442 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -10,9 +10,9 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-dropdown/gr-dropdown';
import '../../shared/gr-dropdown-list/gr-dropdown-list';
+import '../../shared/gr-icon/gr-icon';
import '../../shared/gr-select/gr-select';
import '../../shared/revision-info/revision-info';
-import '../gr-comment-api/gr-comment-api';
import '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
import '../gr-apply-fix-dialog/gr-apply-fix-dialog';
import '../gr-diff-host/gr-diff-host';
@@ -118,7 +118,6 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {ifDefined} from 'lit/directives/if-defined';
import {when} from 'lit/directives/when';
-import {iconStyles} from '../../../styles/gr-icon-styles';
const LOADING_BLAME = 'Loading blame...';
const LOADED_BLAME = 'Blame loaded';
@@ -499,7 +498,6 @@
static override get styles() {
return [
a11yStyles,
- iconStyles,
sharedStyles,
css`
:host {
@@ -977,8 +975,8 @@
link=""
class="prefsButton"
@click=${(e: Event) => this.handlePrefsTap(e)}
- ><span class="material-icon filled">settings</span></gr-button
- >
+ ><gr-icon icon="settings" filled></gr-icon
+ ></gr-button>
</gr-tooltip-content>
</span>
</span>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index aa92a1d..6bf2470 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -572,7 +572,7 @@
role="button"
tabindex="0"
>
- <span class="filled material-icon">settings</span>
+ <gr-icon icon="settings" filled></gr-icon>
</gr-button>
</gr-tooltip-content>
</span>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index 28aac53..2fe3de0 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup-karma';
-import '../gr-comment-api/gr-comment-api';
import '../../shared/revision-info/revision-info';
import './gr-patch-range-select';
import {GrPatchRangeSelect} from './gr-patch-range-select';
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
index 071e1ce..285380e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.ts
@@ -21,6 +21,18 @@
await element.updateComplete;
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-autocomplete
+ allow-non-suggested-values="false"
+ clear-on-commit=""
+ id="input"
+ warn-uncommitted=""
+ >
+ </gr-autocomplete>
+ `);
+ });
+
test('account-text-changed fired when input text changed and allowAnyInput', async () => {
// Spy on query, as that is called when _updateSuggestions proceeds.
const changeStub = sinon.stub();
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
index 7a00ba0..643d14c 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
@@ -12,6 +12,8 @@
let element: GrAlert;
setup(() => {
+ // The gr-alert element attaches itself to the root element on .show(),
+ // rather than existing under a fixture parent.
element = document.createElement('gr-alert');
});
@@ -21,6 +23,26 @@
}
});
+ test('render', async () => {
+ element.show('Alert text');
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div class="content-wrapper">
+ <span class="text"> Alert text </span>
+ <gr-button
+ aria-disabled="false"
+ class="action"
+ hidden=""
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ </gr-button>
+ </div>
+ `);
+ });
+
test('show/hide', async () => {
assert.isNull(element.parentNode);
element.show('Alert text');
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
index 42d088a..c9c3922 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
@@ -31,6 +31,45 @@
element.close();
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div
+ class="dropdown-content"
+ id="suggestions"
+ role="listbox"
+ slot="dropdown-content"
+ >
+ <ul>
+ <li
+ aria-label="test name 1"
+ class="autocompleteOption selected"
+ data-index="0"
+ data-value="test value 1"
+ role="option"
+ tabindex="-1"
+ >
+ <span> 1 </span>
+ <span class="label"> hi </span>
+ </li>
+ <li
+ aria-label="test name 2"
+ class="autocompleteOption"
+ data-index="1"
+ data-value="test value 2"
+ role="option"
+ tabindex="-1"
+ >
+ <span> 2 </span>
+ <span class="hide label"> </span>
+ </li>
+ <dom-repeat style="display: none;">
+ <template is="dom-repeat"> </template>
+ </dom-repeat>
+ </ul>
+ </div>
+ `);
+ });
+
test('shows labels', () => {
const els = queryAll<HTMLElement>(suggestionsEl(), 'li');
assert.equal(els[0].innerText.trim(), '1\nhi');
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index f1705c3..deafa90 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -6,7 +6,6 @@
import '@polymer/paper-input/paper-input';
import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import '../gr-cursor-manager/gr-cursor-manager';
-import '../gr-icons/gr-icons';
import '../../../styles/shared-styles';
import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {fire, fireEvent} from '../../../utils/event-util';
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index b851624..73e472c 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -7,8 +7,7 @@
import './gr-autocomplete';
import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {assertIsDefined} from '../../../utils/common-util';
-import {queryAll, queryAndAssert, waitUntil} from '../../../test/test-utils';
+import {queryAndAssert, waitUntil} from '../../../test/test-utils';
import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {PaperInputElement} from '@polymer/paper-input/paper-input';
import {fixture, html} from '@open-wc/testing-helpers';
@@ -30,45 +29,104 @@
element = await fixture(
html`<gr-autocomplete no-debounce></gr-autocomplete>`
);
- await element.updateComplete;
});
- test('renders', async () => {
- let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
- const queryStub = sinon.spy(
- (input: string) =>
- (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'},
- ] as AutocompleteSuggestion[]))
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <paper-input
+ aria-disabled="false"
+ autocomplete="off"
+ id="input"
+ tabindex="0"
+ >
+ <div slot="prefix">
+ <span class="material-icon searchIcon"> search </span>
+ </div>
+ <div slot="suffix">
+ <slot name="suffix"> </slot>
+ </div>
+ </paper-input>
+ <gr-autocomplete-dropdown
+ horizontal-align="left"
+ id="suggestions"
+ is-hidden=""
+ role="listbox"
+ style="position: fixed; top: 300px; left: 392.5px; box-sizing: border-box; max-height: 600px; max-width: 785px;"
+ vertical-align="top"
+ >
+ </gr-autocomplete-dropdown>
+ `);
+ });
+
+ test('renders with suggestions', async () => {
+ const queryStub = sinon.spy((input: string) =>
+ 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'},
+ ] as AutocompleteSuggestion[])
);
element.query = queryStub;
- assert.isTrue(suggestionsEl().isHidden);
+
+ focusOnInput();
+ element.text = 'blah';
+ await waitUntil(() => queryStub.called);
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(
+ /* HTML */ `
+ <paper-input
+ aria-disabled="false"
+ autocomplete="off"
+ id="input"
+ tabindex="0"
+ >
+ <div slot="prefix">
+ <span class="material-icon searchIcon"> search </span>
+ </div>
+ <div slot="suffix">
+ <slot name="suffix"> </slot>
+ </div>
+ </paper-input>
+ <gr-autocomplete-dropdown
+ horizontal-align="left"
+ id="suggestions"
+ role="listbox"
+ vertical-align="top"
+ >
+ </gr-autocomplete-dropdown>
+ `,
+ {
+ // gr-autocomplete-dropdown sizing seems to vary between local & CI
+ ignoreAttributes: [
+ {tags: ['gr-autocomplete-dropdown'], attributes: ['style']},
+ ],
+ }
+ );
+ });
+
+ test('cursor starts on suggestions', async () => {
+ const queryStub = sinon.spy((input: string) =>
+ 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'},
+ ] as AutocompleteSuggestion[])
+ );
+ element.query = queryStub;
+
assert.equal(suggestionsEl().cursor.index, -1);
focusOnInput();
element.text = 'blah';
await waitUntil(() => queryStub.called);
+ await element.updateComplete;
- assert.isTrue(queryStub.called);
- element.setFocus(true);
-
- assertIsDefined(promise);
- return promise.then(async () => {
- await element.updateComplete;
- assert.isFalse(suggestionsEl().isHidden);
- const suggestions = queryAll<HTMLElement>(suggestionsEl(), 'li');
- assert.equal(suggestions.length, 5);
-
- for (let i = 0; i < 5; i++) {
- assert.equal(suggestions[i].innerText.trim(), `blah ${i}`);
- }
-
- assert.notEqual(suggestionsEl().cursor.index, -1);
- });
+ assert.notEqual(suggestionsEl().cursor.index, -1);
});
test('selectAll', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
index 56de0b6..460f167 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -66,10 +66,10 @@
this.hidden = true;
return;
}
+ this.hidden = false;
const url = this.buildAvatarURL(this.account);
if (url) {
- this.hidden = false;
this.style.backgroundImage = `url("${url}")`;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
index 4179eb5..73daa7e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
@@ -6,6 +6,7 @@
import '../../../test/common-test-setup-karma';
import {queryAndAssert} from '../../../test/test-utils';
import {GrChangeStar} from './gr-change-star';
+import './gr-change-star';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {createChange} from '../../../test/test-data-generators';
@@ -23,21 +24,32 @@
await element.updateComplete;
});
- test('star visibility states', async () => {
- element.change!.starred = true;
- await element.updateComplete;
- let icon = queryAndAssert<HTMLSpanElement>(element, '.material-icon');
- assert.isTrue(icon.classList.contains('filled'));
- assert.isTrue(icon.classList.contains('active'));
- assert.equal(icon.innerText, 'grade');
+ test('renders starred', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <button
+ aria-label="Unstar this change"
+ role="checkbox"
+ title="Star/unstar change (shortcut: s)"
+ >
+ <span class="active filled material-icon"> grade </span>
+ </button>
+ `);
+ });
+ test('renders unstarred', async () => {
element.change!.starred = false;
element.requestUpdate('change');
await element.updateComplete;
- icon = queryAndAssert<HTMLSpanElement>(element, '.material-icon');
- assert.isFalse(icon.classList.contains('filled'));
- assert.isFalse(icon.classList.contains('active'));
- assert.equal(icon.innerText, 'grade');
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <button
+ aria-label="Star this change"
+ role="checkbox"
+ title="Star/unstar change (shortcut: s)"
+ >
+ <span class="material-icon"> grade </span>
+ </button>
+ `);
});
test('starring', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
index 9139614..f38444a 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
@@ -25,6 +25,22 @@
`);
});
+ test('render', async () => {
+ element.status = ChangeStates.WIP;
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-tooltip-content
+ has-tooltip=""
+ max-width="40em"
+ position-below=""
+ title="This change isn't ready to be reviewed or submitted. It will not appear on dashboards unless you are CC'ed, and email notifications will be silenced until the review is started."
+ >
+ <div aria-label="Label: WIP" class="chip">Work in Progress</div>
+ </gr-tooltip-content>
+ `);
+ });
+
test('WIP', async () => {
element.status = ChangeStates.WIP;
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 73755ae..67f7b1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -85,11 +85,6 @@
test('renders with draft', async () => {
element.thread = createThread(c1, c2, c3);
await element.updateComplete;
- });
-
- test('renders with draft', async () => {
- element.thread = createThread(c1, c2, c3);
- await element.updateComplete;
expect(element).shadowDom.to.equal(/* HTML */ `
<div class="fileName">
<span>test-path-comment-thread</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
index 6c66a1c..4a3dcb3 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
@@ -6,6 +6,7 @@
import '../gr-dialog/gr-dialog';
import {css, html, LitElement} from 'lit';
import {property, query, customElement} from 'lit/decorators';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
import {sharedStyles} from '../../../styles/shared-styles';
import {assertIsDefined} from '../../../utils/common-util';
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
new file mode 100644
index 0000000..496c513
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {GrConfirmDeleteCommentDialog} from './gr-confirm-delete-comment-dialog';
+import './gr-confirm-delete-comment-dialog';
+
+suite('gr-confirm-delete-comment-dialog tests', () => {
+ let element: GrConfirmDeleteCommentDialog;
+
+ setup(async () => {
+ element = await fixture(
+ html`<gr-confirm-delete-comment-dialog></gr-confirm-delete-comment-dialog>`
+ );
+ });
+
+ test('render', () => {
+ // prettier and shadowDom string disagree about wrapping in <p> tag.
+ expect(element).shadowDom.to
+ .equal(/* prettier-ignore */ /* HTML */ `
+ <gr-dialog confirm-label="Delete" role="dialog">
+ <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
+ aria-disabled="false"
+ autocomplete="on"
+ class="message"
+ id="messageInput"
+ placeholder="<Insert reasoning here>"
+ >
+ </iron-autogrow-textarea>
+ </div>
+ </gr-dialog>
+ `);
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
index 589ea0a..5d5912e 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
@@ -21,6 +21,35 @@
await flush();
});
+ test('render', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div class="text">
+ <iron-input class="copyText">
+ <input
+ id="input"
+ is="iron-input"
+ part="text-container-style"
+ readonly=""
+ type="text"
+ />
+ </iron-input>
+ <gr-tooltip-content>
+ <gr-button
+ aria-disabled="false"
+ aria-label="Click to copy to clipboard"
+ class="copyToClipboard"
+ id="copy-clipboard-button"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ <span class="material-icon" id="icon"> content_copy </span>
+ </gr-button>
+ </gr-tooltip-content>
+ </div>
+ `);
+ });
+
test('copy to clipboard', () => {
const clipboardSpy = sinon.spy(navigator.clipboard, 'writeText');
const copyBtn = queryAndAssert(element, '.copyToClipboard');
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index 6119a82..be25922 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -5,7 +5,7 @@
*/
import '../gr-tooltip-content/gr-tooltip-content';
import {css, html, LitElement} from 'lit';
-import {customElement, property} from 'lit/decorators';
+import {customElement, property, state} from 'lit/decorators';
import {
parseDate,
fromNow,
@@ -80,27 +80,22 @@
@property({type: Boolean})
showYesterday = false;
- /** @type {?{short: string, full: string}} */
- @property({type: Object})
- private dateFormat?: DateFormatPair;
-
- @property({type: String})
- private timeFormat?: string;
-
- @property({type: Boolean})
- private relative = false;
-
@property({type: Boolean})
forceRelative = false;
@property({type: Boolean})
relativeOptionNoAgo = false;
- private readonly restApiService = getAppContext().restApiService;
+ @state()
+ dateFormat?: DateFormatPair;
- constructor() {
- super();
- }
+ @state()
+ timeFormat?: string;
+
+ @state()
+ relative = false;
+
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
@@ -130,12 +125,12 @@
}
private renderDateString() {
- return html` <span>${this._computeDateStr()}</span>`;
+ return html` <span>${this.computeDateStr()}</span>`;
}
override connectedCallback() {
super.connectedCallback();
- this._loadPreferences();
+ this.loadPreferences();
}
// private but used by tests
@@ -144,27 +139,25 @@
}
// private but used by tests
- _loadPreferences() {
- return this._getLoggedIn().then(loggedIn => {
- if (!loggedIn) {
- this.timeFormat = TimeFormats.TIME_24;
- this.dateFormat = DateFormats.STD;
- this.relative = this.forceRelative;
- return;
- }
- return Promise.all([this._loadTimeFormat(), this.loadRelative()]);
- });
+ async loadPreferences() {
+ const loggedIn = await this.restApiService.getLoggedIn();
+ if (!loggedIn) {
+ this.timeFormat = TimeFormats.TIME_24;
+ this.dateFormat = DateFormats.STD;
+ this.relative = this.forceRelative;
+ return;
+ }
+ await Promise.all([this.loadTimeFormat(), this.loadRelative()]);
}
- // private but used in gr/file-list_test.js
- _loadTimeFormat() {
- return this.getPreferences().then(preferences => {
- if (!preferences) {
- throw Error('Preferences is not set');
- }
- this.decideTimeFormat(preferences.time_format);
- this.decideDateFormat(preferences.date_format);
- });
+ // private but used in gr/file-list_test.ts
+ async loadTimeFormat() {
+ const preferences = await this.restApiService.getPreferences();
+ if (!preferences) {
+ throw Error('Preferences is not set');
+ }
+ this.decideTimeFormat(preferences.time_format);
+ this.decideDateFormat(preferences.date_format);
}
private decideTimeFormat(timeFormat: TimeFormat) {
@@ -202,24 +195,13 @@
}
}
- private loadRelative() {
- return this.getPreferences().then(prefs => {
- // prefs.relative_date_in_change_table is not set when false.
- this.relative =
- this.forceRelative || !!(prefs && prefs.relative_date_in_change_table);
- });
+ private async loadRelative() {
+ const prefs = await this.restApiService.getPreferences();
+ this.relative =
+ this.forceRelative || Boolean(prefs?.relative_date_in_change_table);
}
- _getLoggedIn() {
- return this.restApiService.getLoggedIn();
- }
-
- private getPreferences() {
- return this.restApiService.getPreferences();
- }
-
- // private but used by tests
- _computeDateStr() {
+ private computeDateStr() {
if (!this.dateStr || !this.timeFormat || !this.dateFormat) {
return '';
}
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
deleted file mode 100644
index 96e42e5..0000000
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
+++ /dev/null
@@ -1,449 +0,0 @@
-/**
- * @license
- * Copyright 2015 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup-karma.js';
-import './gr-date-formatter.js';
-import {parseDate} from '../../../utils/date-util.js';
-import {fixture, html} from '@open-wc/testing-helpers';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicTemplate = html`
- <gr-date-formatter withTooltip dateStr="2015-09-24 23:30:17.033000000">
- </gr-date-formatter>
-`;
-
-const lightTemplate = html`
- <gr-date-formatter dateStr="2015-09-24 23:30:17.033000000">
- </gr-date-formatter>
-`;
-
-suite('gr-date-formatter tests', () => {
- let element;
-
- setup(() => {
- });
-
- /**
- * Parse server-formatter date and normalize into current timezone.
- */
- function normalizedDate(dateStr) {
- const d = parseDate(dateStr);
- d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
- return d;
- }
-
- async function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
- expectedTooltip) {
- // Normalize and convert the date to mimic server response.
- dateStr = normalizedDate(dateStr)
- .toJSON()
- .replace('T', ' ')
- .slice(0, -1);
- sinon.useFakeTimers(normalizedDate(nowStr).getTime());
- element.dateStr = dateStr;
- await element.updateComplete;
- const span = element.shadowRoot.querySelector('span');
- const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
- assert.equal(span.textContent.trim(), expected);
- assert.equal(tooltip.title, expectedTooltip);
- element.showDateAndTime = true;
- await element.updateComplete;
- assert.equal(span.textContent.trim(), expectedWithDateAndTime);
- }
-
- function stubRestAPI(preferences) {
- const loggedInPromise = Promise.resolve(preferences !== null);
- const preferencesPromise = Promise.resolve(preferences);
- stubRestApi('getLoggedIn').returns(loggedInPromise);
- stubRestApi('getPreferences').returns(preferencesPromise);
- return Promise.all([loggedInPromise, preferencesPromise]);
- }
-
- suite('STD + 24 hours time format preference', () => {
- setup(async () => {
- await stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'STD',
- relative_date_in_change_table: false,
- });
-
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('invalid dates are quietly rejected', () => {
- assert.notOk((new Date('foo')).valueOf());
- element.dateStr = 'foo';
- element.timeFormat = 'h:mm A';
- assert.equal(element._computeDateStr(), '');
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
-
- test('Within 24 hours on different days', async () => {
- await 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');
- });
-
- test('More than 24 hours but less than six months', async () => {
- await 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');
- });
-
- test('More than six months', async () => {
- await 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');
- });
- });
-
- suite('US + 24 hours time format preference', () => {
- setup(async () => {
- await stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'US',
- relative_date_in_change_table: false,
- });
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
-
- test('Within 24 hours on different days', async () => {
- await 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');
- });
-
- test('More than 24 hours but less than six months', async () => {
- await 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');
- });
- });
-
- suite('ISO + 24 hours time format preference', () => {
- setup(async () => {
- await stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'ISO',
- relative_date_in_change_table: false,
- });
-
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
-
- test('Within 24 hours on different days', async () => {
- await 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');
- });
-
- test('More than 24 hours but less than six months', async () => {
- await 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');
- });
- });
-
- suite('EURO + 24 hours time format preference', () => {
- setup(async () => {
- await stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'EURO',
- relative_date_in_change_table: false,
- });
-
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
-
- test('Within 24 hours on different days', async () => {
- await 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');
- });
-
- test('More than 24 hours but less than six months', async () => {
- await 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');
- });
- });
-
- suite('UK + 24 hours time format preference', () => {
- setup(async () => {
- stubRestAPI({
- time_format: 'HHMM_24',
- date_format: 'UK',
- relative_date_in_change_table: false,
- });
-
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
-
- test('Within 24 hours on different days', async () => {
- await 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');
- });
-
- test('More than 24 hours but less than six months', async () => {
- await 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');
- });
- });
-
- suite('STD + 12 hours time format preference', () => {
- setup(async () => {
- // relative_date_in_change_table is not set when false.
- await stubRestAPI({time_format: 'HHMM_12', date_format: 'STD'});
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
- });
-
- suite('US + 12 hours time format preference', () => {
- setup(async () => {
- // relative_date_in_change_table is not set when false.
- await stubRestAPI({time_format: 'HHMM_12', date_format: 'US'});
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
- });
-
- suite('ISO + 12 hours time format preference', () => {
- setup(async () => {
- // relative_date_in_change_table is not set when false.
- await stubRestAPI({time_format: 'HHMM_12', date_format: 'ISO'});
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
- });
-
- suite('EURO + 12 hours time format preference', () => {
- setup(async () => {
- // relative_date_in_change_table is not set when false.
- await stubRestAPI({time_format: 'HHMM_12', date_format: 'EURO'});
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
- });
-
- suite('UK + 12 hours time format preference', () => {
- setup(async () => {
- // relative_date_in_change_table is not set when false.
- stubRestAPI({time_format: 'HHMM_12', date_format: 'UK'});
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- await element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
- });
-
- suite('relative date preference', () => {
- setup(async () => {
- stubRestAPI({
- time_format: 'HHMM_12',
- date_format: 'STD',
- relative_date_in_change_table: true,
- });
- element = await fixture(basicTemplate);
- sinon.stub(element, '_getUtcOffsetString').returns('');
- return element._loadPreferences();
- });
-
- test('Within 24 hours on same day', async () => {
- await 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');
- });
-
- test('More than six months', async () => {
- await 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');
- });
- });
-
- suite('logged in', () => {
- setup(async () => {
- await stubRestAPI({
- time_format: 'HHMM_12',
- date_format: 'US',
- relative_date_in_change_table: true,
- });
- element = await fixture(basicTemplate);
- await 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(async () => {
- await stubRestAPI(null);
- element = await fixture(basicTemplate);
- await 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);
- });
- });
-
- suite('with tooltip', () => {
- setup(async () => {
- await stubRestAPI(null);
- element = await fixture(basicTemplate);
- await element._loadPreferences();
- await element.updateComplete;
- });
-
- test('Tooltip is present', () => {
- const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
- assert.isOk(tooltip);
- });
- });
-
- suite('without tooltip', () => {
- setup(async () => {
- await stubRestAPI(null);
- element = await fixture(lightTemplate);
- await element._loadPreferences();
- await element.updateComplete;
- });
-
- test('Tooltip is absent', () => {
- const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
- assert.isNotOk(tooltip);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
new file mode 100644
index 0000000..54518fe
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
@@ -0,0 +1,529 @@
+/**
+ * @license
+ * Copyright 2015 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-date-formatter';
+import {GrDateFormatter} from './gr-date-formatter';
+import {parseDate} from '../../../utils/date-util';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {query, queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {GrTooltipContent} from '../gr-tooltip-content/gr-tooltip-content';
+import {Timestamp} from '../../../api/rest-api';
+import {PreferencesInfo} from '../../../types/common';
+import {createPreferences} from '../../../test/test-data-generators';
+import {
+ createDefaultPreferences,
+ DateFormat,
+ TimeFormat,
+} from '../../../constants/constants';
+
+const basicTemplate = html`
+ <gr-date-formatter withTooltip dateStr="2015-09-24 23:30:17.033000000">
+ </gr-date-formatter>
+`;
+
+const lightTemplate = html`
+ <gr-date-formatter dateStr="2015-09-24 23:30:17.033000000">
+ </gr-date-formatter>
+`;
+
+suite('gr-date-formatter tests', () => {
+ let element: GrDateFormatter;
+
+ /**
+ * Parse server-formatted date and normalize into current timezone.
+ */
+ function normalizedDate(dateStr: Timestamp) {
+ const d = parseDate(dateStr);
+ d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
+ return d;
+ }
+
+ async function testDates(
+ nowStr: string,
+ dateStr: string,
+ expected: string,
+ expectedWithDateAndTime: string,
+ expectedTooltip: string
+ ) {
+ // Normalize and convert the date to mimic server response.
+ const normalizedDateStr = normalizedDate(dateStr as Timestamp)
+ .toJSON()
+ .replace('T', ' ')
+ .slice(0, -1);
+ sinon.useFakeTimers(normalizedDate(nowStr as Timestamp).getTime());
+ element.dateStr = normalizedDateStr;
+ await element.updateComplete;
+ const span = queryAndAssert<HTMLSpanElement>(element, 'span');
+ const tooltip = queryAndAssert<GrTooltipContent>(
+ element,
+ 'gr-tooltip-content'
+ );
+ assert.equal(span.textContent?.trim(), expected);
+ assert.equal(tooltip.title, expectedTooltip);
+ element.showDateAndTime = true;
+ await element.updateComplete;
+ assert.equal(span.textContent?.trim(), expectedWithDateAndTime);
+ }
+
+ function stubRestAPI(preferences?: PreferencesInfo) {
+ stubRestApi('getLoggedIn').resolves(preferences !== undefined);
+ stubRestApi('getPreferences').resolves(preferences);
+ }
+
+ suite('STD + 24 hours time format preference', () => {
+ setup(async () => {
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_24,
+ date_format: DateFormat.STD,
+ relative_date_in_change_table: false,
+ });
+
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+
+ test('Within 24 hours on different days', async () => {
+ await 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'
+ );
+ });
+
+ test('More than 24 hours but less than six months', async () => {
+ await 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'
+ );
+ });
+
+ test('More than six months', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('US + 24 hours time format preference', () => {
+ setup(async () => {
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_24,
+ date_format: DateFormat.US,
+ relative_date_in_change_table: false,
+ });
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+
+ test('Within 24 hours on different days', async () => {
+ await 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'
+ );
+ });
+
+ test('More than 24 hours but less than six months', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('ISO + 24 hours time format preference', () => {
+ setup(async () => {
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_24,
+ date_format: DateFormat.ISO,
+ relative_date_in_change_table: false,
+ });
+
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+
+ test('Within 24 hours on different days', async () => {
+ await 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'
+ );
+ });
+
+ test('More than 24 hours but less than six months', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('EURO + 24 hours time format preference', () => {
+ setup(async () => {
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_24,
+ date_format: DateFormat.EURO,
+ relative_date_in_change_table: false,
+ });
+
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+
+ test('Within 24 hours on different days', async () => {
+ await 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'
+ );
+ });
+
+ test('More than 24 hours but less than six months', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('UK + 24 hours time format preference', () => {
+ setup(async () => {
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_24,
+ date_format: DateFormat.UK,
+ relative_date_in_change_table: false,
+ });
+
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+
+ test('Within 24 hours on different days', async () => {
+ await 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'
+ );
+ });
+
+ test('More than 24 hours but less than six months', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('STD + 12 hours time format preference', () => {
+ setup(async () => {
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_12,
+ date_format: DateFormat.STD,
+ });
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('US + 12 hours time format preference', () => {
+ setup(async () => {
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_12,
+ date_format: DateFormat.US,
+ });
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('ISO + 12 hours time format preference', () => {
+ setup(async () => {
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_12,
+ date_format: DateFormat.ISO,
+ });
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('EURO + 12 hours time format preference', () => {
+ setup(async () => {
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_12,
+ date_format: DateFormat.EURO,
+ });
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('UK + 12 hours time format preference', () => {
+ setup(async () => {
+ // relative_date_in_change_table is not set when false.
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_12,
+ date_format: DateFormat.UK,
+ });
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('relative date preference', () => {
+ setup(async () => {
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_12,
+ date_format: DateFormat.STD,
+ relative_date_in_change_table: true,
+ });
+ element = await fixture(basicTemplate);
+ sinon.stub(element, '_getUtcOffsetString').returns('');
+ await element.loadPreferences();
+ });
+
+ test('Within 24 hours on same day', async () => {
+ await 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'
+ );
+ });
+
+ test('More than six months', async () => {
+ await 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'
+ );
+ });
+ });
+
+ suite('logged in', () => {
+ setup(async () => {
+ stubRestAPI({
+ ...createPreferences(),
+ time_format: TimeFormat.HHMM_12,
+ date_format: DateFormat.US,
+ relative_date_in_change_table: true,
+ });
+ element = await fixture(basicTemplate);
+ await 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(async () => {
+ stubRestAPI(undefined);
+ element = await fixture(basicTemplate);
+ await 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);
+ });
+ });
+
+ suite('with tooltip', () => {
+ setup(async () => {
+ stubRestAPI(createDefaultPreferences());
+ element = await fixture(basicTemplate);
+ await element.loadPreferences();
+ await element.updateComplete;
+ });
+
+ test('Tooltip is present', () => {
+ const tooltip = queryAndAssert<GrTooltipContent>(
+ element,
+ 'gr-tooltip-content'
+ );
+ assert.isOk(tooltip);
+ });
+ });
+
+ suite('without tooltip', () => {
+ setup(async () => {
+ stubRestAPI(createDefaultPreferences());
+ element = await fixture(lightTemplate);
+ await element.loadPreferences();
+ await element.updateComplete;
+ });
+
+ test('Tooltip is absent', () => {
+ const tooltip = query<GrTooltipContent>(element, 'gr-tooltip-content');
+ assert.isNotOk(tooltip);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 0663780..d05ab44 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -48,8 +48,6 @@
];
const SELECTED_SCHEME = 'http';
- setup(() => {});
-
suite('unauthenticated', () => {
setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
@@ -60,6 +58,48 @@
await element.updateComplete;
});
+ test('render', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div class="schemes">
+ <paper-tabs dir="null" id="downloadTabs" role="tablist" tabindex="0">
+ <paper-tab
+ aria-disabled="false"
+ aria-selected="true"
+ class="iron-selected"
+ data-scheme="http"
+ role="tab"
+ tabindex="0"
+ >
+ http
+ </paper-tab>
+ <paper-tab
+ aria-disabled="false"
+ aria-selected="false"
+ data-scheme="repo"
+ role="tab"
+ tabindex="-1"
+ >
+ repo
+ </paper-tab>
+ <paper-tab
+ aria-disabled="false"
+ aria-selected="false"
+ data-scheme="ssh"
+ role="tab"
+ tabindex="-1"
+ >
+ ssh
+ </paper-tab>
+ </paper-tabs>
+ </div>
+ <div class="commands"></div>
+ <gr-shell-command class="_label_checkout"> </gr-shell-command>
+ <gr-shell-command class="_label_cherrypick"> </gr-shell-command>
+ <gr-shell-command class="_label_formatpatch"> </gr-shell-command>
+ <gr-shell-command class="_label_pull"> </gr-shell-command>
+ `);
+ });
+
test('focusOnCopy', async () => {
const focusStub = sinon.stub(
queryAndAssert<GrShellCommand>(element, 'gr-shell-command'),
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts
index 10fe73e..76770b1 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.ts
@@ -22,6 +22,116 @@
);
});
+ test('render', async () => {
+ 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' as Timestamp,
+ text: 'Top Text 3',
+ mobileText: 'Mobile Text 3',
+ },
+ ];
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-button
+ aria-disabled="false"
+ class="dropdown-trigger"
+ down-arrow=""
+ id="trigger"
+ link=""
+ no-uppercase=""
+ role="button"
+ slot="dropdown-trigger"
+ tabindex="0"
+ >
+ <span id="triggerText"> Button Text 2 </span>
+ <gr-copy-clipboard hidden="" hideinput=""> </gr-copy-clipboard>
+ </gr-button>
+ <iron-dropdown
+ aria-disabled="false"
+ aria-hidden="true"
+ horizontal-align="left"
+ id="dropdown"
+ style="outline: none; display: none;"
+ vertical-align="top"
+ >
+ <paper-listbox
+ class="dropdown-content"
+ role="listbox"
+ slot="dropdown-content"
+ tabindex="0"
+ >
+ <paper-item
+ aria-disabled="false"
+ aria-selected="false"
+ data-value="1"
+ role="option"
+ tabindex="-1"
+ >
+ <div class="topContent">
+ <div>Top Text 1</div>
+ </div>
+ </paper-item>
+ <paper-item
+ aria-disabled="false"
+ aria-selected="true"
+ class="iron-selected"
+ data-value="2"
+ role="option"
+ tabindex="0"
+ >
+ <div class="topContent">
+ <div>Top Text 2</div>
+ </div>
+ <div class="bottomContent">
+ <div>Bottom Text 2</div>
+ </div>
+ </paper-item>
+ <paper-item
+ aria-disabled="true"
+ aria-selected="false"
+ data-value="3"
+ disabled=""
+ role="option"
+ style="pointer-events: none;"
+ tabindex="-1"
+ >
+ <div class="topContent">
+ <div>Top Text 3</div>
+ <gr-date-formatter> </gr-date-formatter>
+ </div>
+ <div class="bottomContent">
+ <div>Bottom Text 3</div>
+ </div>
+ </paper-item>
+ </paper-listbox>
+ </iron-dropdown>
+ <gr-select>
+ <select>
+ <option value="1">Top Text 1</option>
+ <option value="2">Mobile Text 2</option>
+ <option disabled="" value="3">Mobile Text 3</option>
+ </select>
+ </gr-select>
+ `);
+ });
+
test('hide copy by default', () => {
const copyEl = query<HTMLElement>(
element,
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
index 5fabee7..bc47945 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.ts
@@ -145,7 +145,7 @@
assert.isFalse(tooltipContents[1].hasTooltip);
});
- test('shadowDom', async () => {
+ test('render', async () => {
element.items = [
{name: 'item one', id: 'foo', tooltip: 'hello'},
{name: 'item two', id: 'bar', url: 'http://bar'},
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 88106fa..a3497df 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -107,13 +107,13 @@
}
code {
display: block;
- white-space: pre-wrap;
+ white-space: nowrap;
background-color: var(--background-color-secondary);
border: 1px solid var(--border-color);
border-left-width: var(--spacing-s);
margin: var(--spacing-m) 0;
padding: var(--spacing-s) var(--spacing-m);
- overflow-x: scroll;
+ overflow-x: auto;
}
li {
list-style-type: disc;
@@ -354,7 +354,7 @@
}
private renderLink(text: string, url: string): TemplateResult {
- return html`<a href=${url}>${text}</a>`;
+ return html`<a target="_blank" href=${url}>${text}</a>`;
}
private renderInlineCode(text: string): TemplateResult {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index d601dd0..6810f99 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -78,6 +78,20 @@
assert.lengthOf(element._computeBlocks(''), 0);
});
+ test('render', async () => {
+ element.content = 'text `code`';
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <p>
+ <gr-linked-text content="text " inline="" pre="">
+ <span id="output" slot="insert"> text </span>
+ </gr-linked-text>
+ <span class="inline-code"> code </span>
+ </p>
+ `);
+ });
+
for (const text of [
'Para1',
'Para 1\nStill para 1',
diff --git a/polygerrit-ui/app/elements/shared/gr-icon/gr-icon.ts b/polygerrit-ui/app/elements/shared/gr-icon/gr-icon.ts
new file mode 100644
index 0000000..13397b6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-icon/gr-icon.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-icon': GrIcon;
+ }
+}
+/**
+ * @attr {String} icon - the icon to display
+ * @attr {Boolean} filled - whether the icon should be filled.
+ */
+@customElement('gr-icon')
+export class GrIcon extends LitElement {
+ @property({type: String, reflect: true})
+ icon?: string;
+
+ @property({type: Boolean, reflect: true})
+ filled?: boolean;
+
+ static override get styles() {
+ return [
+ css`
+ :host {
+ /* Fallback rule for color */
+ color: var(--deemphasized-text-color);
+ font-family: var(--icon-font-family, 'Material Symbols Outlined');
+ font-weight: normal;
+ font-style: normal;
+ font-size: 20px;
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ font-variation-settings: 'FILL' 0;
+ vertical-align: top;
+ }
+ :host([filled]) {
+ font-variation-settings: 'FILL' 1;
+ }
+ /* This is the trick such that the name of the icon doesn't appear in
+ * search
+ */
+ :host::before {
+ content: attr(icon);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html``;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts
index e133a54..62fcae8 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.ts
@@ -19,6 +19,19 @@
);
});
+ test('render', async () => {
+ element.text = 'abc 123';
+ element.limit = 5;
+ element.tooltip = 'tip';
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <gr-tooltip-content has-tooltip="" title="abc 123 (tip)">
+ abc …
+ </gr-tooltip-content>
+ `);
+ });
+
test('tooltip without title input', async () => {
element.text = 'abc 123';
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
index 27a2a3a..0d9673d 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
@@ -44,7 +44,7 @@
},
googlesearch: {
match: 'google:(.+)',
- link: 'https://bing.com/search?q=$1', // html should supercede link.
+ link: 'https://bing.com/search?q=$1', // html should supersede link.
html: '<a href="https://google.com/search?q=$1">$1</a>',
},
hashedhtml: {
@@ -71,6 +71,25 @@
window.CANONICAL_PATH = originalCanonicalPath;
});
+ test('render', async () => {
+ element.content =
+ 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
+ await element.updateComplete;
+ expect(element).lightDom.to.equal(/* HTML */ `
+ <div id="output"></div>
+ <span id="output" slot="insert">
+ <a
+ href="https://bugs.chromium.org/p/gerrit/issues/detail?id=3650"
+ rel="noopener"
+ style="color: var(--link-color)"
+ target="_blank"
+ >
+ https://bugs.chromium.org/p/gerrit/issues/detail?id=3650
+ </a>
+ </span>
+ `);
+ });
+
test('URL pattern was parsed and linked.', async () => {
// Regular inline link.
const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
index f040554..2ceb5c9 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
@@ -21,6 +21,41 @@
await element.updateComplete;
});
+ test('render', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div id="topContainer">
+ <div class="filterContainer">
+ <label> Filter: </label>
+ <iron-input>
+ <input id="filter" type="text" />
+ </iron-input>
+ </div>
+ <div id="createNewContainer">
+ <gr-button
+ aria-disabled="false"
+ id="createNew"
+ link=""
+ primary=""
+ role="button"
+ tabindex="0"
+ >
+ Create New
+ </gr-button>
+ </div>
+ </div>
+ <slot> </slot>
+ <nav>
+ Page 1
+ <a hidden="" href="" id="prevArrow">
+ <span class="material-icon"> chevron_left </span>
+ </a>
+ <a hidden="" href=",25" id="nextArrow">
+ <span class="material-icon"> chevron_right </span>
+ </a>
+ </nav>
+ `);
+ });
+
test('computeNavLink', () => {
const offset = 25;
const projectsPerPage = 25;
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.ts
index da7ac48..009e5ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.ts
@@ -21,6 +21,11 @@
element = basicFixture.instantiate() as GrOverlay;
});
+ test('render', async () => {
+ await element.open();
+ expect(element).shadowDom.to.equal(/* HTML */ ' <slot></slot> ');
+ });
+
test('popstate listener is attached on open and removed on close', () => {
const addEventListenerStub = sinon.stub(window, 'addEventListener');
const removeEventListenerStub = sinon.stub(window, 'removeEventListener');
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
index ee688ec..5f54989 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
@@ -20,7 +20,14 @@
</ul>
</gr-page-nav>
`);
- await element.updateComplete;
+ });
+
+ test('render', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <nav aria-label="Sidebar">
+ <slot> </slot>
+ </nav>
+ `);
});
test('header is not pinned just below top', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
index ba41af1..0119aa0 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.ts
@@ -19,6 +19,27 @@
await element.updateComplete;
});
+ test('render', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div>
+ <gr-labeled-autocomplete
+ id="repoInput"
+ label="Repository"
+ placeholder="Select repo"
+ >
+ </gr-labeled-autocomplete>
+ <span class="material-icon"> chevron_right </span>
+ <gr-labeled-autocomplete
+ disabled=""
+ id="branchInput"
+ label="Branch"
+ placeholder="Select branch"
+ >
+ </gr-labeled-autocomplete>
+ </div>
+ `);
+ });
+
suite('getRepoSuggestions', () => {
let getReposStub: sinon.SinonStub;
setup(() => {
@@ -109,9 +130,8 @@
const repo = 'gerrit' as RepoName;
const branchInput = 'refs/heads/stable-2.1';
element.repo = repo;
- return element.getRepoBranchesSuggestions(branchInput).then(() => {
- assert.isTrue(getRepoBranchesStub.calledWith('stable-2.1', repo, 15));
- });
+ await element.getRepoBranchesSuggestions(branchInput);
+ assert.isTrue(getRepoBranchesStub.calledWith('stable-2.1', repo, 15));
});
test('does not query when repo is unset', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
index 808be4a..bc364e3 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.ts
@@ -23,6 +23,10 @@
`);
});
+ test('render', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ '<slot></slot>');
+ });
+
test('bindValue must be set to the first option value', () => {
assert.equal(element.bindValue, '1');
assert.equal(element.nativeSelect.value, '1');
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
index 6463b7a..9301662 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
@@ -21,6 +21,18 @@
await flush();
});
+ test('render', async () => {
+ element.label = 'label1';
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <label> label1 </label>
+ <div class="commandContainer">
+ <gr-copy-clipboard buttontitle="" hastooltip=""> </gr-copy-clipboard>
+ </div>
+ `);
+ });
+
test('focusOnCopy', async () => {
const focusStub = sinon.stub(
queryAndAssert<GrCopyClipboard>(element, 'gr-copy-clipboard')!,
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index ba2d9cc..db8a94a 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -18,11 +18,12 @@
import {addShortcut, Key} from '../../../utils/dom-util';
import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {fire} from '../../../utils/event-util';
-import {LitElement, css, html} from 'lit';
+import {LitElement, css, html, nothing} from 'lit';
import {customElement, property, query, state} from 'lit/decorators';
import {sharedStyles} from '../../../styles/shared-styles';
import {PropertyValues} from 'lit';
import {classMap} from 'lit/directives/class-map';
+import {KnownExperimentId} from '../../../services/flags/flags';
const MAX_ITEMS_DROPDOWN = 10;
@@ -103,6 +104,8 @@
// Accessed in tests.
readonly reporting = getAppContext().reportingService;
+ private readonly flagsService = getAppContext().flagsService;
+
private disableEnterKeyForSelectingSuggestion = false;
/** Called in disconnectedCallback. */
@@ -207,16 +210,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
- id="emojiSuggestions"
- .suggestions=${this.suggestions}
- .horizontalOffset=${20}
- .verticalOffset=${20}
- vertical-align="top"
- horizontal-align="left"
- @dropdown-closed=${this.resetEmojiDropdown}
- @item-selected=${this.handleEmojiSelect}
- >
+ ${this.renderEmojiDropdown()}
+ ${this.renderReviewerDropdown()}
</gr-autocomplete-dropdown>
<iron-autogrow-textarea
id="textarea"
@@ -235,6 +230,35 @@
`;
}
+ private renderEmojiDropdown() {
+ return html`
+ <gr-autocomplete-dropdown
+ id="emojiSuggestions"
+ .suggestions=${this.suggestions}
+ .horizontalOffset=${20}
+ .verticalOffset=${20}
+ vertical-align="top"
+ horizontal-align="left"
+ @dropdown-closed=${this.resetEmojiDropdown}
+ @item-selected=${this.handleEmojiSelect}
+ >
+ </gr-autocomplete-dropdown>
+ `;
+ }
+
+ private renderReviewerDropdown() {
+ if (!this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS))
+ return nothing;
+ return html` <gr-autocomplete-dropdown
+ id="reviewerSuggestions"
+ vertical-align="top"
+ horizontal-align="left"
+ .horizontalOffset=${20}
+ .verticalOffset=${20}
+ role="listbox"
+ ></gr-autocomplete-dropdown>`;
+ }
+
override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('text')) {
this.handleTextChanged(this.text);
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index f8d98e8..a8a30b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -8,7 +8,7 @@
import {GrTextarea} from './gr-textarea';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {waitUntil} from '../../../test/test-utils';
+import {stubFlags, waitUntil} from '../../../test/test-utils';
import {fixture, html} from '@open-wc/testing-helpers';
suite('gr-textarea tests', () => {
@@ -43,6 +43,48 @@
);
});
+ suite('mention users', () => {
+ setup(async () => {
+ stubFlags('isEnabled').returns(true);
+ element.requestUpdate();
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(
+ /* HTML */ `
+ <div id="hiddenText"></div>
+ <span id="caratSpan"> </span>
+ <gr-autocomplete-dropdown
+ horizontal-align="left"
+ id="emojiSuggestions"
+ is-hidden=""
+ style="position: fixed; top: 478px; left: 321px; box-sizing: border-box; max-height: 956px; max-width: 642px;"
+ vertical-align="top"
+ >
+ </gr-autocomplete-dropdown>
+ <gr-autocomplete-dropdown
+ horizontal-align="left"
+ id="reviewerSuggestions"
+ is-hidden=""
+ role="listbox"
+ style="position: fixed; top: 478px; left: 321px; box-sizing: border-box; max-height: 956px; max-width: 642px;"
+ vertical-align="top"
+ >
+ </gr-autocomplete-dropdown>
+ <iron-autogrow-textarea aria-disabled="false" id="textarea">
+ </iron-autogrow-textarea>
+ `,
+ {
+ // gr-autocomplete-dropdown sizing seems to vary between local & CI
+ ignoreAttributes: [
+ {tags: ['gr-autocomplete-dropdown'], attributes: ['style']},
+ ],
+ }
+ );
+ });
+ });
+
test('monospace is set properly', () => {
assert.isFalse(element.classList.contains('monospace'));
});
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
index dab1d98..e246be0 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.ts
@@ -36,6 +36,16 @@
await element.updateComplete;
});
+ test('render', async () => {
+ element.showIcon = true;
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <slot> </slot>
+ <span class="filled material-icon"> info </span>
+ `);
+ });
+
test('icon is not visible by default', () => {
assert.isNotOk(query(element, '.material-icon'));
});
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
index 71038eb..e1fb0237 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
@@ -18,6 +18,19 @@
await element.updateComplete;
});
+ test('render', async () => {
+ element.text = 'tooltipText';
+ await element.updateComplete;
+
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div class="tooltip">
+ <i class="arrow arrowPositionBelow" style="margin-left:0;"> </i>
+ tooltipText
+ <i class="arrow arrowPositionAbove" style="margin-left:0;"> </i>
+ </div>
+ `);
+ });
+
test('max-width is respected if set', async () => {
element.text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
index 207270c..eb99b32 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
@@ -472,7 +472,7 @@
cells[signCols.right].classList.add('sign', 'right');
}
const moveRangeHeader = createElementDiff('gr-range-header');
- moveRangeHeader.setAttribute('icon', 'gr-icons:move-item');
+ moveRangeHeader.setAttribute('icon', 'move_item');
moveRangeHeader.appendChild(descriptionTextDiv);
cells[descriptionIndex].classList.add('moveHeader');
cells[descriptionIndex].appendChild(moveRangeHeader);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util_test.js b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util_test.js
deleted file mode 100644
index 9dc198a..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util_test.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/**
- * @license
- * Copyright 2021 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup-karma.js';
-import {FrameConstrainer} from './util.js';
-
-suite('FrameConstrainer tests', () => {
- let constrainer;
-
- setup(() => {
- constrainer = new FrameConstrainer();
- constrainer.setBounds({width: 100, height: 100});
- constrainer.setFrameSize({width: 50, height: 50});
- constrainer.requestCenter({x: 50, y: 50});
- });
-
- suite('changing center', () => {
- test('moves frame to requested position', () => {
- constrainer.requestCenter({x: 30, y: 30});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 5, y: 5}, dimensions: {width: 50, height: 50}});
- });
-
- test('keeps frame in bounds for top left corner', () => {
- constrainer.requestCenter({x: 5, y: 5});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 0, y: 0}, dimensions: {width: 50, height: 50}});
- });
-
- test('keeps frame in bounds for bottom right corner', () => {
- constrainer.requestCenter({x: 95, y: 95});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
- });
-
- test('handles out-of-bounds center left', () => {
- constrainer.requestCenter({x: -5, y: 50});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 0, y: 25}, dimensions: {width: 50, height: 50}});
- });
-
- test('handles out-of-bounds center right', () => {
- constrainer.requestCenter({x: 105, y: 50});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 50, y: 25}, dimensions: {width: 50, height: 50}});
- });
-
- test('handles out-of-bounds center top', () => {
- constrainer.requestCenter({x: 50, y: -5});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 25, y: 0}, dimensions: {width: 50, height: 50}});
- });
-
- test('handles out-of-bounds center bottom', () => {
- constrainer.requestCenter({x: 50, y: 105});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 25, y: 50}, dimensions: {width: 50, height: 50}});
- });
- });
-
- suite('changing frame size', () => {
- test('maintains center when decreased', () => {
- constrainer.setFrameSize({width: 10, height: 10});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 45, y: 45}, dimensions: {width: 10, height: 10}});
- });
-
- test('maintains center when increased', () => {
- constrainer.setFrameSize({width: 80, height: 80});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 10, y: 10}, dimensions: {width: 80, height: 80}});
- });
-
- test('updates center to remain in bounds when increased', () => {
- constrainer.setFrameSize({width: 10, height: 10});
- constrainer.requestCenter({x: 95, y: 95});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
-
- constrainer.setFrameSize({width: 20, height: 20});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 80, y: 80}, dimensions: {width: 20, height: 20}});
- });
- });
-
- suite('changing scale', () => {
- suite('for unscaled frame', () => {
- test('adjusts origin to maintain center when zooming in', () => {
- constrainer.setScale(2);
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 75, y: 75}, dimensions: {width: 50, height: 50}});
- });
-
- test('adjusts origin to maintain center when zooming out', () => {
- constrainer.setFrameSize({width: 20, height: 20});
- constrainer.setScale(0.5);
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 15, y: 15}, dimensions: {width: 20, height: 20}});
- });
-
- test('keeps frame in bounds when zooming out', () => {
- constrainer.setScale(5);
- constrainer.requestCenter({x: 100, y: 100});
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 450, y: 450}, dimensions: {width: 50, height: 50}});
-
- constrainer.setScale(1);
- assert.deepEqual(
- constrainer.getUnscaledFrame(),
- {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
- });
- });
-
- suite('for scaled frame', () => {
- test('decreases frame size and maintains center when zooming in', () => {
- constrainer.setScale(2);
- assert.deepEqual(
- constrainer.getScaledFrame(),
- {origin: {x: 37.5, y: 37.5}, dimensions: {width: 25, height: 25}});
- });
-
- test('increases frame size and maintains center when zooming out', () => {
- constrainer.setFrameSize({width: 20, height: 20});
- constrainer.setScale(0.5);
- assert.deepEqual(
- constrainer.getScaledFrame(),
- {origin: {x: 30, y: 30}, dimensions: {width: 40, height: 40}});
- });
-
- test('keeps frame in bounds when zooming out', () => {
- constrainer.setScale(5);
- constrainer.requestCenter({x: 100, y: 100});
- assert.deepEqual(
- constrainer.getScaledFrame(),
- {origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
-
- constrainer.setScale(1);
- assert.deepEqual(
- constrainer.getScaledFrame(),
- {origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
- });
- });
- });
-});
\ No newline at end of file
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util_test.ts
new file mode 100644
index 0000000..2b8f754
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/util_test.ts
@@ -0,0 +1,179 @@
+/**
+ * @license
+ * Copyright 2021 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {FrameConstrainer} from './util';
+
+suite('FrameConstrainer tests', () => {
+ let constrainer: FrameConstrainer;
+
+ setup(() => {
+ constrainer = new FrameConstrainer();
+ constrainer.setBounds({width: 100, height: 100});
+ constrainer.setFrameSize({width: 50, height: 50});
+ constrainer.requestCenter({x: 50, y: 50});
+ });
+
+ suite('changing center', () => {
+ test('moves frame to requested position', () => {
+ constrainer.requestCenter({x: 30, y: 30});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 5, y: 5},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+
+ test('keeps frame in bounds for top left corner', () => {
+ constrainer.requestCenter({x: 5, y: 5});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 0, y: 0},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+
+ test('keeps frame in bounds for bottom right corner', () => {
+ constrainer.requestCenter({x: 95, y: 95});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 50, y: 50},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+
+ test('handles out-of-bounds center left', () => {
+ constrainer.requestCenter({x: -5, y: 50});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 0, y: 25},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+
+ test('handles out-of-bounds center right', () => {
+ constrainer.requestCenter({x: 105, y: 50});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 50, y: 25},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+
+ test('handles out-of-bounds center top', () => {
+ constrainer.requestCenter({x: 50, y: -5});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 25, y: 0},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+
+ test('handles out-of-bounds center bottom', () => {
+ constrainer.requestCenter({x: 50, y: 105});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 25, y: 50},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+ });
+
+ suite('changing frame size', () => {
+ test('maintains center when decreased', () => {
+ constrainer.setFrameSize({width: 10, height: 10});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 45, y: 45},
+ dimensions: {width: 10, height: 10},
+ });
+ });
+
+ test('maintains center when increased', () => {
+ constrainer.setFrameSize({width: 80, height: 80});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 10, y: 10},
+ dimensions: {width: 80, height: 80},
+ });
+ });
+
+ test('updates center to remain in bounds when increased', () => {
+ constrainer.setFrameSize({width: 10, height: 10});
+ constrainer.requestCenter({x: 95, y: 95});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 90, y: 90},
+ dimensions: {width: 10, height: 10},
+ });
+
+ constrainer.setFrameSize({width: 20, height: 20});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 80, y: 80},
+ dimensions: {width: 20, height: 20},
+ });
+ });
+ });
+
+ suite('changing scale', () => {
+ suite('for unscaled frame', () => {
+ test('adjusts origin to maintain center when zooming in', () => {
+ constrainer.setScale(2);
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 75, y: 75},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+
+ test('adjusts origin to maintain center when zooming out', () => {
+ constrainer.setFrameSize({width: 20, height: 20});
+ constrainer.setScale(0.5);
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 15, y: 15},
+ dimensions: {width: 20, height: 20},
+ });
+ });
+
+ test('keeps frame in bounds when zooming out', () => {
+ constrainer.setScale(5);
+ constrainer.requestCenter({x: 100, y: 100});
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 450, y: 450},
+ dimensions: {width: 50, height: 50},
+ });
+
+ constrainer.setScale(1);
+ assert.deepEqual(constrainer.getUnscaledFrame(), {
+ origin: {x: 50, y: 50},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+ });
+
+ suite('for scaled frame', () => {
+ test('decreases frame size and maintains center when zooming in', () => {
+ constrainer.setScale(2);
+ assert.deepEqual(constrainer.getScaledFrame(), {
+ origin: {x: 37.5, y: 37.5},
+ dimensions: {width: 25, height: 25},
+ });
+ });
+
+ test('increases frame size and maintains center when zooming out', () => {
+ constrainer.setFrameSize({width: 20, height: 20});
+ constrainer.setScale(0.5);
+ assert.deepEqual(constrainer.getScaledFrame(), {
+ origin: {x: 30, y: 30},
+ dimensions: {width: 40, height: 40},
+ });
+ });
+
+ test('keeps frame in bounds when zooming out', () => {
+ constrainer.setScale(5);
+ constrainer.requestCenter({x: 100, y: 100});
+ assert.deepEqual(constrainer.getScaledFrame(), {
+ origin: {x: 90, y: 90},
+ dimensions: {width: 10, height: 10},
+ });
+
+ constrainer.setScale(1);
+ assert.deepEqual(constrainer.getScaledFrame(), {
+ origin: {x: 50, y: 50},
+ dimensions: {width: 50, height: 50},
+ });
+ });
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index cc44f50..062347f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -359,7 +359,7 @@
ignoredWhitespaceOnly: !!chunk.common,
keyLocation: !!chunk.keyLocation,
};
- if (chunk.skip) {
+ if (chunk.skip !== undefined) {
return new GrDiffGroup({
type,
skip: chunk.skip,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
index 5e39dba..d99fd6a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
@@ -488,6 +488,26 @@
// group[4] is the displayed part of the second ab
});
+ test('works with skip === 0', async () => {
+ element.context = 3;
+ const content = [
+ {
+ skip: 0,
+ },
+ {
+ b: [
+ '/**',
+ ' * @license',
+ ' * Copyright 2015 Google LLC',
+ ' * SPDX-License-Identifier: Apache-2.0',
+ ' */',
+ "import '../../../test/common-test-setup-karma';",
+ ],
+ },
+ ];
+ await element.process(content, false);
+ });
+
test('break up common diff chunks', () => {
element.keyLocations = {
left: {1: true},
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index 7cb87ab..eac76da 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -254,7 +254,7 @@
throw new Error('Cannot set skip and lines');
}
this.skip = options.skip;
- if (options.skip) {
+ if (options.skip !== undefined) {
this.lineRange = {
left: {
start_line: options.offsetLeft,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index cb7d2db..86d4730 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -68,6 +68,7 @@
import {grSyntaxTheme} from '../gr-syntax-themes/gr-syntax-theme';
import {grRangedCommentTheme} from '../gr-ranged-comment-themes/gr-ranged-comment-theme';
import {classMap} from 'lit/directives/class-map';
+import {iconStyles} from '../../../styles/gr-icon-styles';
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -268,6 +269,7 @@
static override get styles() {
return [
+ iconStyles,
sharedStyles,
grSyntaxTheme,
grRangedCommentTheme,
@@ -668,10 +670,10 @@
--divider-height: var(--spacing-s);
--divider-border: 1px;
}
+ /* TODO: Is this still used? */
.contextControl gr-button iron-icon {
/* should match line-height of gr-button */
- width: var(--line-height-mono, 18px);
- height: var(--line-height-mono, 18px);
+ font-size: var(--line-height-mono, 18px);
}
.contextControl td:not(.lineNumButton) {
text-align: center;
@@ -790,7 +792,7 @@
font-size: var(--font-size-normal, 14px);
line-height: var(--line-height-normal);
}
- td.lost iron-icon {
+ td.lost .material-icon {
padding: 0 var(--spacing-s) 0 var(--spacing-m);
color: var(--blue-700);
}
@@ -1733,8 +1735,9 @@
private portedCommentsWithoutRangeMessage() {
const div = document.createElement('div');
- const icon = document.createElement('iron-icon');
- icon.setAttribute('icon', 'gr-icons:info-outline');
+ const icon = document.createElement('span');
+ icon.className = 'material-icon';
+ icon.innerText = 'info';
div.appendChild(icon);
const span = document.createElement('span');
span.innerText = 'Original comment position not found in this patchset';
diff --git a/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts
index c80a459..f386a47 100644
--- a/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header.ts
@@ -5,7 +5,7 @@
*/
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
-import '@polymer/iron-icon/iron-icon';
+import {iconStyles} from '../../../styles/gr-icon-styles';
/**
* Represents a header (label) for a code chunk whenever showing
@@ -18,8 +18,12 @@
@property({type: String})
icon?: string;
+ @property({type: Boolean})
+ filled?: boolean;
+
static override get styles() {
return [
+ iconStyles,
css`
.row {
color: var(--gr-range-header-color);
@@ -33,8 +37,7 @@
}
.icon {
color: var(--gr-range-header-color);
- height: var(--line-height-small, 16px);
- width: var(--line-height-small, 16px);
+ font-size: var(--line-height-small, 16px);
margin-right: var(--spacing-s);
}
`,
@@ -44,7 +47,11 @@
override render() {
const icon = this.icon ?? '';
return html` <div class="row">
- <iron-icon class="icon" .icon=${icon} aria-hidden="true"></iron-icon>
+ <span
+ class="icon material-icon ${this.filled ? 'filled' : ''}"
+ aria-hidden="true"
+ >${icon}</span
+ >
<slot></slot>
</div>`;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
index 235da89..35d496a 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
@@ -33,7 +33,7 @@
override render() {
return html`<div class="rangeHighlight row">
- <gr-range-header icon="gr-icons:comment"
+ <gr-range-header icon="mode_comment" filled
>${this._computeRangeLabel(this.range)}</gr-range-header
>
</div>`;
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index f38e060..8f692a8 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -36,7 +36,7 @@
case LinkIcon.HELP_PAGE:
return {name: 'help'};
case LinkIcon.REPORT_BUG:
- return {name: 'bug', filled: true};
+ return {name: 'bug_report', filled: true};
case LinkIcon.CODE:
return {name: 'code'};
case LinkIcon.FILE_PRESENT:
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 4a001ad..e2318e8 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -21,4 +21,5 @@
MORE_FILES_INFO = 'UiFeature__more_files_info',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
RELATED_CHANGES_SUBMITTABILITY = 'UiFeature__related_changes_submittability',
+ MENTION_USERS = 'UIFeature_mention_users',
}
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 95a3ddc..6eb779f 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -377,11 +377,15 @@
}
if (type !== ERROR.TYPE) {
if (value !== undefined) {
- console.debug(`Reporting: ${name}: ${value}`);
+ console.debug(
+ `Reporting(${new Date().toISOString()}): ${name}: ${value}`
+ );
} else if (eventDetails !== undefined) {
- console.debug(`Reporting: ${name}: ${eventDetails}`);
+ console.debug(
+ `Reporting(${new Date().toISOString()}): ${name}: ${eventDetails}`
+ );
} else {
- console.debug(`Reporting: ${name}`);
+ console.debug(`Reporting(${new Date().toISOString()}): ${name}`);
}
}
}
diff --git a/polygerrit-ui/app/styles/gr-submit-requirements-styles.ts b/polygerrit-ui/app/styles/gr-submit-requirements-styles.ts
index c35d8ed..948e9b0 100644
--- a/polygerrit-ui/app/styles/gr-submit-requirements-styles.ts
+++ b/polygerrit-ui/app/styles/gr-submit-requirements-styles.ts
@@ -6,15 +6,15 @@
import {css} from 'lit';
export const submitRequirementsStyles = css`
- .material-icon.check_circle,
- .material-icon.published_with_changes {
+ gr-icon.check_circle,
+ gr-icon.published_with_changes {
color: var(--success-foreground);
}
- .material-icon.block,
- .material-icon.error {
+ gr-icon.block,
+ gr-icon.error {
color: var(--deemphasized-text-color);
}
- .material-icon.cancel {
+ gr-icon.cancel {
color: var(--error-foreground);
}
`;
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 71e0a9c..4281f43 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -106,10 +106,10 @@
export const DELAYED_CANCELLATION = Symbol('Delayed Cancellation');
export class DelayedPromise<T> extends Promise<T> {
- private readonly resolve: (value: PromiseLike<T> | T) => void;
+ private resolve: (value: PromiseLike<T> | T) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- private readonly reject: (reason?: any) => void;
+ private reject: (reason?: any) => void;
private timer: number | undefined;
@@ -167,11 +167,11 @@
// that default behaviour by redefining its @@species property.
// NOTE: This is required otherwise .then and .catch on a DelayedPromise
// will try to instantiate a DelayedPromise with 'resolve, reject' arguments.
- static get [Symbol.species]() {
+ static override get [Symbol.species]() {
return Promise;
}
- get [Symbol.toStringTag]() {
+ override get [Symbol.toStringTag]() {
return 'DelayedPromise';
}
}
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 15b1c71..eccb38b 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -182,7 +182,6 @@
// Examples:
// '@polymer/polymer.js' -> '/node_modules/@polymer/polymer.js'
// 'page/page.mjs' -> '/node_modules/page.mjs'
- // '@polymer/iron-icon' -> '/node_modules/@polymer/iron-icon.js'
// './element/file' -> './element/file.js'
moduleImportRegexp = regexp.MustCompile(`(import[^'";]*|export[^'";]*from ?)['"]([^;\s]*?)(\.(m?)js)?['"];`)
data = moduleImportRegexp.ReplaceAll(data, []byte("$1'$2.${4}js';"))
diff --git a/resources/com/google/gerrit/server/commit-msg_test.sh b/resources/com/google/gerrit/server/commit-msg_test.sh
index 4f1a3f7..2c256ff 100755
--- a/resources/com/google/gerrit/server/commit-msg_test.sh
+++ b/resources/com/google/gerrit/server/commit-msg_test.sh
@@ -85,11 +85,11 @@
${hook} input || fail "failed hook execution"
- found=$(grep -c '^Change-Id' input)
+ found=$(grep -c '^Change-Id' input) || :
if [[ "${found}" != "1" ]]; then
fail "got ${found} Change-Ids, want 1"
fi
- found=$(grep -c '^Change-Id: I123' input)
+ found=$(grep -c '^Change-Id: I123' input) || :
if [[ "${found}" != "1" ]]; then
fail "got ${found} Change-Id: I123, want 1"
fi
@@ -104,6 +104,18 @@
git config gerrit.createChangeId false
${hook} input || fail "failed hook execution"
git config --unset gerrit.createChangeId
+ found=$(grep -c '^Change-Id' input) || :
+ if [[ "${found}" != "0" ]]; then
+ fail "got ${found} Change-Ids, want 0"
+ fi
+}
+
+function test_suppress_squash {
+ cat << EOF > input
+squash! bla bla
+EOF
+
+ ${hook} input || fail "failed hook execution"
found=$(grep -c '^Change-Id' input || true)
if [[ "${found}" != "0" ]]; then
fail "got ${found} Change-Ids, want 0"
@@ -119,11 +131,11 @@
git config gerrit.reviewUrl https://myhost/
${hook} input || fail "failed hook execution"
git config --unset gerrit.reviewUrl
- found=$(grep -c '^Change-Id' input || true)
+ found=$(grep -c '^Change-Id' input) || :
if [[ "${found}" != "0" ]]; then
fail "got ${found} Change-Ids, want 0"
fi
- found=$(grep -c '^Link: https://myhost/id/I' input || true)
+ found=$(grep -c '^Link: https://myhost/id/I' input) || :
if [[ "${found}" != "1" ]]; then
fail "got ${found} Link footers, want 1"
fi
@@ -138,7 +150,7 @@
EOF
${hook} input || fail "failed hook execution"
- result=$(tail -1 input | grep ^Change-Id)
+ result=$(tail -1 input | grep ^Change-Id) || :
if [[ -z "${result}" ]] ; then
echo "after: "
cat input
@@ -147,6 +159,25 @@
fi
}
+# Change-Id goes before Signed-off-by trailers.
+function test_before_signed_off_by {
+ cat << EOF > input
+bla bla
+
+Bug: #123
+Signed-off-by: Joe User
+EOF
+
+ ${hook} input || fail "failed hook execution"
+ result=$(tail -2 input | head -1 | grep ^Change-Id) || :
+ if [[ -z "${result}" ]] ; then
+ echo "after: "
+ cat input
+
+ fail "did not find Change-Id before Signed-off-by"
+ fi
+}
+
function test_dash_at_end {
if [[ ! -x /bin/dash ]] ; then
echo "/bin/dash not installed; skipping dash test."
@@ -161,7 +192,7 @@
/bin/dash ${hook} input || fail "failed hook execution"
- result=$(tail -1 input | grep ^Change-Id)
+ result=$(tail -1 input | grep ^Change-Id) || :
if [[ -z "${result}" ]] ; then
echo "after: "
cat input
@@ -184,11 +215,11 @@
/bin/dash ${hook} input || fail "failed hook execution"
- found=$(grep -c '^Change-Id' input)
+ found=$(grep -c '^Change-Id' input) || :
if [[ "${found}" != "1" ]]; then
fail "got ${found} Change-Ids, want 1"
fi
- found=$(grep -c '^Change-Id: I123' input)
+ found=$(grep -c '^Change-Id: I123' input) || :
if [[ "${found}" != "1" ]]; then
fail "got ${found} Change-Id: I123, want 1"
fi
diff --git a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index e1d6f22..1ee4423 100755
--- a/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -34,6 +34,11 @@
exit 0
fi
+# Do not create a change id for squash commits.
+if head -n1 "$1" | grep -q '^squash! '; then
+ exit 0
+fi
+
if git rev-parse --verify HEAD >/dev/null 2>&1; then
refhash="$(git rev-parse HEAD)"
else
@@ -43,7 +48,7 @@
random=$({ git var GIT_COMMITTER_IDENT ; echo "$refhash" ; cat "$1"; } | git hash-object --stdin)
dest="$1.tmp.${random}"
-trap 'rm -f "${dest}"' EXIT
+trap 'rm -f "$dest" "$dest-2"' EXIT
if ! git stripspace --strip-comments < "$1" > "${dest}" ; then
echo "cannot strip comments from $1"
@@ -57,21 +62,39 @@
reviewurl="$(git config --get gerrit.reviewUrl)"
if test -n "${reviewurl}" ; then
- if ! git interpret-trailers --parse < "$1" | grep -q '^Link:.*/id/I[0-9a-f]\{40\}$' ; then
- if ! git interpret-trailers \
- --trailer "Link: ${reviewurl%/}/id/I${random}" < "$1" > "${dest}" ; then
- echo "cannot insert link footer in $1"
- exit 1
- fi
- fi
+ token="Link"
+ value="${reviewurl%/}/id/I$random"
+ pattern=".*/id/I[0-9a-f]\{40\}$"
else
- # Avoid the --in-place option which only appeared in Git 2.8
- # Avoid the --if-exists option which only appeared in Git 2.15
- if ! git -c trailer.ifexists=doNothing interpret-trailers \
- --trailer "Change-Id: I${random}" < "$1" > "${dest}" ; then
- echo "cannot insert change-id line in $1"
- exit 1
- fi
+ token="Change-Id"
+ value="I$random"
+ pattern=".*"
+fi
+
+if git interpret-trailers --parse < "$1" | grep -q "^$token: $pattern$" ; then
+ exit 0
+fi
+
+# There must be a Signed-off-by trailer for the code below to work. Insert a
+# sentinel at the end to make sure there is one.
+# Avoid the --in-place option which only appeared in Git 2.8
+if ! git interpret-trailers \
+ --trailer "Signed-off-by: SENTINEL" < "$1" > "$dest-2" ; then
+ echo "cannot insert Signed-off-by sentinel line in $1"
+ exit 1
+fi
+
+# Make sure the trailer appears before any Signed-off-by trailers by inserting
+# it as if it was a Signed-off-by trailer and then use sed to remove the
+# Signed-off-by prefix and the Signed-off-by sentinel line.
+# Avoid the --in-place option which only appeared in Git 2.8
+# Avoid the --where option which only appeared in Git 2.15
+if ! git -c trailer.where=before interpret-trailers \
+ --trailer "Signed-off-by: $token: $value" < "$dest-2" |
+ sed -re "s/^Signed-off-by: ($token: )/\1/" \
+ -e "/^Signed-off-by: SENTINEL/d" > "$dest" ; then
+ echo "cannot insert $token line in $1"
+ exit 1
fi
if ! mv "${dest}" "$1" ; then