Merge "GitVisibleChangeFilter: Remove unnecessary nested else clause"
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 9df4b04..70352dc 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -456,6 +456,15 @@
=== Group
* `group/guess_relevant_groups_latency`: Latency for guessing relevant groups.
+* `group/handles_count`: Number of calls to GroupBackend.handles.
+* `group/get_count`: Number of calls to GroupBackend.get.
+* `group/suggest_count`: Number of calls to GroupBackend.suggest.
+* `group/contains_count`: Number of calls to GroupMemberships.contains.
+* `group/contains_any_of_count`: Number of calls to
+ GroupMemberships.containsAnyOf.
+* `group/intersection_count`: Number of calls to GroupMemberships.intersection.
+* `group/known_groups_count`: Number of calls to GroupMemberships.getKnownGroups.
+
=== Replication Plugin
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 5bd9bea..1587bc5 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -27,10 +27,16 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.StartupCheck;
import com.google.gerrit.server.StartupException;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
import com.google.gerrit.server.project.ProjectState;
@@ -49,11 +55,57 @@
public class UniversalGroupBackend implements GroupBackend {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Field<String> SYSTEM_FIELD =
+ Field.ofString("system", Metadata.Builder::groupSystem).build();
+
private final PluginSetContext<GroupBackend> backends;
+ private final Counter1<String> handlesCount;
+ private final Counter1<String> getCount;
+ private final Counter2<String, Integer> suggestCount;
+ private final Counter2<String, Boolean> containsCount;
+ private final Counter2<String, Boolean> containsAnyCount;
+ private final Counter2<String, Integer> intersectionCount;
+ private final Counter2<String, Integer> knownGroupsCount;
@Inject
- UniversalGroupBackend(PluginSetContext<GroupBackend> backends) {
+ UniversalGroupBackend(PluginSetContext<GroupBackend> backends, MetricMaker metricMaker) {
this.backends = backends;
+ this.handlesCount =
+ metricMaker.newCounter(
+ "group/handles_count", new Description("Calls to GroupBackend.handles"), SYSTEM_FIELD);
+ this.getCount =
+ metricMaker.newCounter(
+ "group/get_count", new Description("Calls to GroupBackend.get"), SYSTEM_FIELD);
+ this.suggestCount =
+ metricMaker.newCounter(
+ "group/suggest_count",
+ new Description("Calls to GroupBackend.suggest"),
+ SYSTEM_FIELD,
+ Field.ofInteger("num_suggested", (meta, value) -> {}).build());
+ this.containsCount =
+ metricMaker.newCounter(
+ "group/contains_count",
+ new Description("Calls to GroupMemberships.contains"),
+ SYSTEM_FIELD,
+ Field.ofBoolean("contains", (meta, value) -> {}).build());
+ this.containsAnyCount =
+ metricMaker.newCounter(
+ "group/contains_any_of_count",
+ new Description("Calls to GroupMemberships.containsAnyOf"),
+ SYSTEM_FIELD,
+ Field.ofBoolean("contains_any_of", (meta, value) -> {}).build());
+ this.intersectionCount =
+ metricMaker.newCounter(
+ "group/intersection_count",
+ new Description("Calls to GroupMemberships.intersection"),
+ SYSTEM_FIELD,
+ Field.ofInteger("num_intersection", (meta, value) -> {}).build());
+ this.knownGroupsCount =
+ metricMaker.newCounter(
+ "group/known_groups_count",
+ new Description("Calls to GroupMemberships.getKnownGroups"),
+ SYSTEM_FIELD,
+ Field.ofInteger("num_known_groups", (meta, value) -> {}).build());
}
@Nullable
@@ -70,7 +122,12 @@
@Override
public boolean handles(AccountGroup.UUID uuid) {
- return backend(uuid) != null;
+ GroupBackend b = backend(uuid);
+ if (b == null) {
+ return false;
+ }
+ handlesCount.increment(name(b));
+ return true;
}
@Override
@@ -83,13 +140,19 @@
logger.atFine().log("Unknown GroupBackend for UUID: %s", uuid);
return null;
}
+ getCount.increment(name(b));
return b.get(uuid);
}
@Override
public Collection<GroupReference> suggest(String name, ProjectState project) {
Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
- backends.runEach(g -> groups.addAll(g.suggest(name, project)));
+ backends.runEach(
+ g -> {
+ Collection<GroupReference> suggestions = g.suggest(name, project);
+ suggestCount.increment(name(g), suggestions.size());
+ groups.addAll(suggestions);
+ });
return groups;
}
@@ -108,11 +171,11 @@
}
@Nullable
- private GroupMembership membership(AccountGroup.UUID uuid) {
+ private Map.Entry<GroupBackend, GroupMembership> membership(AccountGroup.UUID uuid) {
if (uuid != null) {
for (Map.Entry<GroupBackend, GroupMembership> m : memberships.entrySet()) {
if (m.getKey().handles(uuid)) {
- return m.getValue();
+ return m;
}
}
}
@@ -125,51 +188,57 @@
if (uuid == null) {
return false;
}
- GroupMembership m = membership(uuid);
+ Map.Entry<GroupBackend, GroupMembership> m = membership(uuid);
if (m == null) {
return false;
}
- return m.contains(uuid);
+ boolean contains = m.getValue().contains(uuid);
+ containsCount.increment(name(m.getKey()), contains);
+ return contains;
}
@Override
public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) {
- ListMultimap<GroupMembership, AccountGroup.UUID> lookups =
+ ListMultimap<Map.Entry<GroupBackend, GroupMembership>, AccountGroup.UUID> lookups =
MultimapBuilder.hashKeys().arrayListValues().build();
for (AccountGroup.UUID uuid : uuids) {
if (uuid == null) {
continue;
}
- GroupMembership m = membership(uuid);
+ Map.Entry<GroupBackend, GroupMembership> m = membership(uuid);
if (m == null) {
continue;
}
lookups.put(m, uuid);
}
- for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
- lookups.asMap().entrySet()) {
- GroupMembership m = entry.getKey();
- Collection<AccountGroup.UUID> ids = entry.getValue();
+ for (Map.Entry<GroupBackend, GroupMembership> groupBackends : lookups.asMap().keySet()) {
+
+ GroupMembership m = groupBackends.getValue();
+ Collection<AccountGroup.UUID> ids = lookups.asMap().get(groupBackends);
if (ids.size() == 1) {
if (m.contains(Iterables.getOnlyElement(ids))) {
+ containsAnyCount.increment(name(groupBackends.getKey()), true);
return true;
}
} else if (m.containsAnyOf(ids)) {
+ containsAnyCount.increment(name(groupBackends.getKey()), true);
return true;
}
+ // We would have returned if contains was true.
+ containsAnyCount.increment(name(groupBackends.getKey()), false);
}
return false;
}
@Override
public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> uuids) {
- ListMultimap<GroupMembership, AccountGroup.UUID> lookups =
+ ListMultimap<Map.Entry<GroupBackend, GroupMembership>, AccountGroup.UUID> lookups =
MultimapBuilder.hashKeys().arrayListValues().build();
for (AccountGroup.UUID uuid : uuids) {
if (uuid == null) {
continue;
}
- GroupMembership m = membership(uuid);
+ Map.Entry<GroupBackend, GroupMembership> m = membership(uuid);
if (m == null) {
logger.atFine().log("Unknown GroupMembership for UUID: %s", uuid);
continue;
@@ -177,9 +246,11 @@
lookups.put(m, uuid);
}
Set<AccountGroup.UUID> groups = new HashSet<>();
- for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
- lookups.asMap().entrySet()) {
- groups.addAll(entry.getKey().intersection(entry.getValue()));
+ for (Map.Entry<GroupBackend, GroupMembership> groupBackend : lookups.asMap().keySet()) {
+ Set<AccountGroup.UUID> intersection =
+ groupBackend.getValue().intersection(lookups.asMap().get(groupBackend));
+ intersectionCount.increment(name(groupBackend.getKey()), intersection.size());
+ groups.addAll(intersection);
}
return groups;
}
@@ -187,8 +258,10 @@
@Override
public Set<AccountGroup.UUID> getKnownGroups() {
Set<AccountGroup.UUID> groups = new HashSet<>();
- for (GroupMembership m : memberships.values()) {
- groups.addAll(m.getKnownGroups());
+ for (Map.Entry<GroupBackend, GroupMembership> entry : memberships.entrySet()) {
+ Set<AccountGroup.UUID> knownGroups = entry.getValue().getKnownGroups();
+ knownGroupsCount.increment(name(entry.getKey()), knownGroups.size());
+ groups.addAll(knownGroups);
}
return groups;
}
@@ -204,6 +277,13 @@
return false;
}
+ private static String name(GroupBackend backend) {
+ if (backend == null) {
+ return "none";
+ }
+ return backend.getClass().getSimpleName();
+ }
+
public static class ConfigCheck implements StartupCheck {
private final Config cfg;
private final UniversalGroupBackend universalGroupBackend;
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 5cd0e98..b433e9f 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -102,6 +102,9 @@
/** The name of a group. */
public abstract Optional<String> groupName();
+ /** The group system being queried. */
+ public abstract Optional<String> groupSystem();
+
/** The UUID of a group. */
public abstract Optional<String> groupUuid();
@@ -328,6 +331,8 @@
public abstract Builder groupName(@Nullable String groupName);
+ public abstract Builder groupSystem(@Nullable String groupSystem);
+
public abstract Builder groupUuid(@Nullable String groupUuid);
public abstract Builder httpStatus(int httpStatus);
diff --git a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
index 1e3063e..5f062be 100644
--- a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
+++ b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -30,6 +30,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.AccountGroup.UUID;
import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
@@ -56,7 +57,8 @@
backends.add("gerrit", new SystemGroupBackend(new Config()));
backend =
new UniversalGroupBackend(
- new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE));
+ new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE),
+ new DisabledMetricMaker());
}
@Test
@@ -124,7 +126,8 @@
backends.add("gerrit", backend);
backend =
new UniversalGroupBackend(
- new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE));
+ new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE),
+ new DisabledMetricMaker());
GroupMembership checker = backend.membershipsOf(member);
assertFalse(checker.contains(REGISTERED_USERS));
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java
new file mode 100644
index 0000000..43153ae
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.gson.Gson;
+import com.google.inject.TypeLiteral;
+import java.util.Optional;
+import org.junit.Test;
+
+public class ChangeNoteJsonTest {
+ private final Gson gson = new ChangeNoteJson().getGson();
+
+ @Test
+ public void shouldSerializeAndDeserializeEmptyOptional() {
+ // given
+ Optional<?> empty = Optional.empty();
+
+ // when
+ String json = gson.toJson(empty);
+
+ // then
+ assertThat(json).isEqualTo("{}");
+
+ // and when
+ Optional<?> result = gson.fromJson(json, Optional.class);
+
+ // and then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void shouldSerializeAndDeserializeNonEmptyOptional() {
+ // given
+ String value = "foo";
+ Optional<String> nonEmpty = Optional.of(value);
+
+ // when
+ String json = gson.toJson(nonEmpty);
+
+ // then
+ assertThat(json).isEqualTo("{\n \"value\": \"" + value + "\"\n}");
+
+ // and when
+ Optional<String> result = gson.fromJson(json, new TypeLiteral<Optional<String>>() {}.getType());
+
+ // and then
+ assertThat(result).isPresent();
+ assertThat(result.get()).isEqualTo(value);
+ }
+}
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index c5c262b..732a82c 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -240,34 +240,6 @@
the "Before launch" section for IntelliJ. This is a temporary problem until
typescript migration is complete.
-## Running Templates Test
-The templates test validates polymer templates. The test convert polymer
-templates into a plain typescript code and then run TS compiler. The test fails
-if TS compiler reports errors; in this case you will see TS errors in
-the log/output. Gerrit-CI automatically runs templates test.
-
-**Note**: Files defined in `ignore_templates_list` (`polygerrit-ui/app/BUILD`)
-are excluded from code generation and checking. If you don't know how to fix
-a problem, you can add a problematic template in the list.
-
-* To run test locally, use npm command:
-``` sh
-npm run polytest
-```
-
-* Often, the output from the previous command is not clear (cryptic TS errors).
-In this case, run the command
-```sh
-npm run polytest:dev
-```
-This command (re)creates the `polygerrit-ui/app/tmpl_out` directory and put
-generated files into it. For each polygerrit .ts file there is a generated file
-in the `tmpl_out` directory. If an original file doesn't contain a polymer
-template, the generated file is empty.
-
-You can open a problematic file in IDE and fix the problem. Ensure, that IDE
-uses `polygerrit-ui/app/tsconfig.json` as a project (usually, this is default).
-
### Generated file overview
A generated file starts with imports followed by a static content with
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 6788aa3..4322c64 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -38,10 +38,16 @@
* If the weblinks-only parameter is specified, only the web_links field is set.
*/
export declare interface DiffInfo {
- /** Meta information about the file on side A as a DiffFileMetaInfo entity. */
- meta_a: DiffFileMetaInfo;
- /** Meta information about the file on side B as a DiffFileMetaInfo entity. */
- meta_b: DiffFileMetaInfo;
+ /**
+ * Meta information about the file on side A as a DiffFileMetaInfo entity.
+ * Not set when change_type is ADDED.
+ */
+ meta_a?: DiffFileMetaInfo;
+ /**
+ * Meta information about the file on side B as a DiffFileMetaInfo entity.
+ * Not set when change_type is DELETED.
+ */
+ meta_b?: DiffFileMetaInfo;
/** The type of change (ADDED, MODIFIED, DELETED, RENAMED COPIED, REWRITE). */
change_type: ChangeType;
/** Intraline status (OK, ERROR, TIMEOUT). */
@@ -167,7 +173,7 @@
* Indicates the range (line numbers) on the other side of the comparison
* where the code related to the current chunk came from/went to.
*/
- range: {
+ range?: {
start: number;
end: number;
};
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 933b300..b4ebd3c 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
@@ -40,12 +40,9 @@
ChangeInfo,
ServerInfo,
AccountInfo,
- QuickLabelInfo,
Timestamp,
} from '../../../types/common';
import {hasOwnProperty, assertIsDefined} from '../../../utils/common-util';
-import {pluralize} from '../../../utils/string-util';
-import {showNewSubmitRequirements} from '../../../utils/label-util';
import {changeListStyles} from '../../../styles/gr-change-list-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
@@ -248,22 +245,6 @@
.subject:hover .content {
text-decoration: underline;
}
- .u-monospace {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- .u-green,
- .u-green iron-icon {
- color: var(--positive-green-text-color);
- }
- .u-red,
- .u-red iron-icon {
- color: var(--negative-red-text-color);
- }
- .u-gray-background {
- background-color: var(--table-header-background-color);
- }
.comma,
.placeholder {
color: var(--deemphasized-text-color);
@@ -618,32 +599,13 @@
}
private renderChangeLabels(labelName: string) {
- if (showNewSubmitRequirements(this.flagsService, this.change)) {
- return html` <td class="cell label requirement">
- <gr-change-list-column-requirement
- .change=${this.change}
- .labelName=${labelName}
- >
- </gr-change-list-column-requirement>
- </td>`;
- }
- return html`
- <td
- title=${this.computeLabelTitle(labelName)}
- class=${this.computeLabelClass(labelName)}
+ return html` <td class="cell label requirement">
+ <gr-change-list-column-requirement
+ .change=${this.change}
+ .labelName=${labelName}
>
- ${this.renderChangeHasLabelIcon(labelName)}
- </td>
- `;
- }
-
- private renderChangeHasLabelIcon(labelName: string) {
- if (this.computeLabelIcon(labelName) === '')
- return html`<span>${this.computeLabelValue(labelName)}</span>`;
-
- return html`
- <iron-icon icon=${this.computeLabelIcon(labelName)}></iron-icon>
- `;
+ </gr-change-list-column-requirement>
+ </td>`;
}
private renderChangePluginEndpoint(pluginEndpointName: string) {
@@ -676,118 +638,6 @@
return GerritNav.getUrlForChange(this.change);
}
- // private but used in test
- computeLabelTitle(labelName: string) {
- const label: QuickLabelInfo | undefined = this.change?.labels?.[labelName];
- const category = this.computeLabelCategory(labelName);
- if (!label || category === LabelCategory.NOT_APPLICABLE) {
- return 'Label not applicable';
- }
- const titleParts: string[] = [];
- if (category === LabelCategory.UNRESOLVED_COMMENTS) {
- const num = this.change?.unresolved_comment_count ?? 0;
- titleParts.push(pluralize(num, 'unresolved comment'));
- }
- const significantLabel =
- label.rejected || label.approved || label.disliked || label.recommended;
- if (significantLabel?.name) {
- titleParts.push(`${labelName} by ${significantLabel.name}`);
- }
- if (titleParts.length > 0) {
- return titleParts.join(',\n');
- }
- return labelName;
- }
-
- // private but used in test
- computeLabelClass(labelName: string) {
- const classes = ['cell', 'label'];
- const category = this.computeLabelCategory(labelName);
- switch (category) {
- case LabelCategory.NOT_APPLICABLE:
- classes.push('u-gray-background');
- break;
- case LabelCategory.APPROVED:
- classes.push('u-green');
- break;
- case LabelCategory.POSITIVE:
- classes.push('u-monospace');
- classes.push('u-green');
- break;
- case LabelCategory.NEGATIVE:
- classes.push('u-monospace');
- classes.push('u-red');
- break;
- case LabelCategory.REJECTED:
- classes.push('u-red');
- break;
- }
- return classes.sort().join(' ');
- }
-
- // private but used in test
- computeLabelIcon(labelName: string): string {
- const category = this.computeLabelCategory(labelName);
- switch (category) {
- case LabelCategory.APPROVED:
- return 'gr-icons:check';
- case LabelCategory.UNRESOLVED_COMMENTS:
- return 'gr-icons:comment';
- case LabelCategory.REJECTED:
- return 'gr-icons:close';
- default:
- return '';
- }
- }
-
- // private but used in test
- computeLabelCategory(labelName: string) {
- const label: QuickLabelInfo | undefined = this.change?.labels?.[labelName];
- if (!label) {
- return LabelCategory.NOT_APPLICABLE;
- }
- if (label.rejected) {
- return LabelCategory.REJECTED;
- }
- if (label.value && label.value < 0) {
- return LabelCategory.NEGATIVE;
- }
- if (this.change?.unresolved_comment_count && labelName === 'Code-Review') {
- return LabelCategory.UNRESOLVED_COMMENTS;
- }
- if (label.approved) {
- return LabelCategory.APPROVED;
- }
- if (label.value && label.value > 0) {
- return LabelCategory.POSITIVE;
- }
- return LabelCategory.NEUTRAL;
- }
-
- // private but used in test
- computeLabelValue(labelName: string) {
- const label: QuickLabelInfo | undefined = this.change?.labels?.[labelName];
- const category = this.computeLabelCategory(labelName);
- switch (category) {
- case LabelCategory.NOT_APPLICABLE:
- return '';
- case LabelCategory.APPROVED:
- return '\u2713'; // ✓
- case LabelCategory.POSITIVE:
- return `+${label?.value}`;
- case LabelCategory.NEUTRAL:
- return '';
- case LabelCategory.UNRESOLVED_COMMENTS:
- return 'u';
- case LabelCategory.NEGATIVE:
- return `${label?.value}`;
- case LabelCategory.REJECTED:
- return '\u2715'; // ✕
- default:
- return '';
- }
- }
-
private computeRepoUrl() {
if (!this.change) return '';
return GerritNav.getUrlForProjectChanges(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index e0ce6a8..7216249 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -48,7 +48,7 @@
import {StandardLabels} from '../../../utils/label-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import './gr-change-list-item';
-import {GrChangeListItem, LabelCategory} from './gr-change-list-item';
+import {GrChangeListItem} from './gr-change-list-item';
import {
DIProviderElement,
wrapInProvider,
@@ -92,209 +92,6 @@
await element.updateComplete;
});
- test('computeLabelCategory', () => {
- element.change = {
- ...change,
- labels: {},
- };
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.NOT_APPLICABLE
- );
- element.change.labels = {Verified: {approved: account, value: 1}};
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.APPROVED
- );
- element.change.labels = {Verified: {rejected: account, value: -1}};
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.REJECTED
- );
- element.change.labels = {'Code-Review': {approved: account, value: 1}};
- element.change.unresolved_comment_count = 1;
- assert.equal(
- element.computeLabelCategory('Code-Review'),
- LabelCategory.UNRESOLVED_COMMENTS
- );
- element.change.labels = {'Code-Review': {value: 1}};
- element.change.unresolved_comment_count = 0;
- assert.equal(
- element.computeLabelCategory('Code-Review'),
- LabelCategory.POSITIVE
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelCategory('Code-Review'),
- LabelCategory.NEGATIVE
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.NOT_APPLICABLE
- );
- });
-
- test('computeLabelClass', () => {
- element.change = {
- ...change,
- labels: {},
- };
- assert.equal(
- element.computeLabelClass('Verified'),
- 'cell label u-gray-background'
- );
- element.change.labels = {Verified: {approved: account, value: 1}};
- assert.equal(element.computeLabelClass('Verified'), 'cell label u-green');
- element.change.labels = {Verified: {rejected: account, value: -1}};
- assert.equal(element.computeLabelClass('Verified'), 'cell label u-red');
- element.change.labels = {'Code-Review': {value: 1}};
- assert.equal(
- element.computeLabelClass('Code-Review'),
- 'cell label u-green u-monospace'
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelClass('Code-Review'),
- 'cell label u-monospace u-red'
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelClass('Verified'),
- 'cell label u-gray-background'
- );
- });
-
- test('computeLabelTitle', () => {
- element.change = {
- ...change,
- labels: {},
- };
- assert.equal(element.computeLabelTitle('Verified'), 'Label not applicable');
-
- element.change.labels = {Verified: {approved: {name: 'Diffy'}}};
- assert.equal(element.computeLabelTitle('Verified'), 'Verified by Diffy');
-
- element.change.labels = {Verified: {approved: {name: 'Diffy'}}};
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Label not applicable'
- );
-
- element.change.labels = {Verified: {rejected: {name: 'Diffy'}}};
- assert.equal(element.computeLabelTitle('Verified'), 'Verified by Diffy');
-
- element.change.labels = {
- 'Code-Review': {disliked: {name: 'Diffy'}, value: -1},
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Diffy'
- );
-
- element.change.labels = {
- 'Code-Review': {recommended: {name: 'Diffy'}, value: 1},
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Diffy'
- );
-
- element.change.labels = {
- 'Code-Review': {recommended: {name: 'Diffy'}, rejected: {name: 'Admin'}},
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Admin'
- );
-
- element.change.labels = {
- 'Code-Review': {approved: {name: 'Diffy'}, rejected: {name: 'Admin'}},
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Admin'
- );
-
- element.change.labels = {
- 'Code-Review': {
- recommended: {name: 'Diffy'},
- disliked: {name: 'Admin'},
- value: -1,
- },
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Admin'
- );
-
- element.change.labels = {
- 'Code-Review': {
- approved: {name: 'Diffy'},
- disliked: {name: 'Admin'},
- value: -1,
- },
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Diffy'
- );
-
- element.change.labels = {'Code-Review': {approved: account, value: 1}};
- element.change.unresolved_comment_count = 1;
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- '1 unresolved comment'
- );
-
- element.change.labels = {
- 'Code-Review': {approved: {name: 'Diffy'}, value: 1},
- };
- element.change.unresolved_comment_count = 1;
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- '1 unresolved comment,\nCode-Review by Diffy'
- );
-
- element.change.labels = {'Code-Review': {approved: account, value: 1}};
- element.change.unresolved_comment_count = 2;
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- '2 unresolved comments'
- );
- });
-
- test('computeLabelIcon', () => {
- element.change = {
- ...change,
- labels: {},
- };
- assert.equal(element.computeLabelIcon('missingLabel'), '');
- element.change.labels = {Verified: {approved: account, value: 1}};
- assert.equal(element.computeLabelIcon('Verified'), 'gr-icons:check');
- element.change.labels = {'Code-Review': {approved: account, value: 1}};
- element.change.unresolved_comment_count = 1;
- assert.equal(element.computeLabelIcon('Code-Review'), 'gr-icons:comment');
- });
-
- test('computeLabelValue', () => {
- element.change = {
- ...change,
- labels: {},
- };
- assert.equal(element.computeLabelValue('Verified'), '');
- element.change.labels = {Verified: {approved: account, value: 1}};
- assert.equal(element.computeLabelValue('Verified'), '✓');
- element.change.labels = {Verified: {value: 1}};
- assert.equal(element.computeLabelValue('Verified'), '+1');
- element.change.labels = {Verified: {value: -1}};
- assert.equal(element.computeLabelValue('Verified'), '-1');
- element.change.labels = {Verified: {approved: account}};
- assert.equal(element.computeLabelValue('Verified'), '✓');
- element.change.labels = {Verified: {rejected: account}};
- assert.equal(element.computeLabelValue('Verified'), '✕');
- });
-
test('no hidden columns', async () => {
element.visibleChangeTableColumns = [
ColumnNames.SUBJECT,
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 24719e8..34080a1 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
@@ -10,7 +10,7 @@
import {configModelToken} from '../../../models/config/config-model';
import {resolve} from '../../../models/dependency';
import {
- AccountInfo,
+ AccountDetailInfo,
ChangeInfo,
NumericChangeId,
ServerInfo,
@@ -30,8 +30,14 @@
import {allSettled} from '../../../utils/async-util';
import {listForSentence} from '../../../utils/string-util';
import {getDisplayName} from '../../../utils/display-name-util';
-import {AccountInputDetail} from '../../shared/gr-account-list/gr-account-list';
+import {
+ AccountInput,
+ AccountInputDetail,
+} from '../../shared/gr-account-list/gr-account-list';
import '@polymer/iron-icon/iron-icon';
+import {getReplyByReason} from '../../../utils/attention-set-util';
+import {intersection} from '../../../utils/common-util';
+import {accountOrGroupKey} from '../../../utils/account-util';
@customElement('gr-change-list-reviewer-flow')
export class GrChangeListReviewerFlow extends LitElement {
@@ -40,7 +46,7 @@
// contents are given to gr-account-lists to mutate
@state() private updatedAccountsByReviewerState: Map<
ReviewerState,
- AccountInfo[]
+ AccountInput[]
> = new Map([
[ReviewerState.REVIEWER, []],
[ReviewerState.CC, []],
@@ -72,6 +78,8 @@
private isLoggedIn = false;
+ private account?: AccountDetailInfo;
+
static override get styles() {
return css`
gr-dialog {
@@ -125,6 +133,11 @@
() => getAppContext().userModel.loggedIn$,
isLoggedIn => (this.isLoggedIn = isLoggedIn)
);
+ subscribe(
+ this,
+ () => getAppContext().userModel.account$,
+ account => (this.account = account)
+ );
}
override render() {
@@ -244,12 +257,11 @@
.filter(account => account?._account_id !== undefined);
return this.updatedAccountsByReviewerState
.get(updatedReviewerState)!
- .filter(
- account =>
- account._account_id !== undefined &&
- accountsInCurrentState.some(
- otherAccount => otherAccount._account_id === account._account_id
- )
+ .filter(account =>
+ accountsInCurrentState.some(
+ otherAccount =>
+ accountOrGroupKey(otherAccount) === accountOrGroupKey(account)
+ )
)
.map(reviewer => getDisplayName(this.serverConfig, reviewer));
}
@@ -292,7 +304,6 @@
reviewerState: ReviewerState,
event: CustomEvent<AccountInputDetail>
) {
- const account = event.detail.account as AccountInfo;
const oppositeReviewerState =
reviewerState === ReviewerState.CC
? ReviewerState.REVIEWER
@@ -301,7 +312,7 @@
oppositeReviewerState
)!;
const oppositeUpdatedAccountIndex = oppositeUpdatedAccounts.findIndex(
- acc => acc._account_id === account._account_id
+ acc => accountOrGroupKey(acc) === accountOrGroupKey(event.detail.account)
);
if (oppositeUpdatedAccountIndex >= 0) {
oppositeUpdatedAccounts.splice(oppositeUpdatedAccountIndex, 1);
@@ -335,7 +346,8 @@
])
);
const inFlightActions = this.getBulkActionsModel().addReviewers(
- this.updatedAccountsByReviewerState
+ this.updatedAccountsByReviewerState,
+ getReplyByReason(this.account, this.serverConfig)
);
await allSettled(
@@ -383,13 +395,7 @@
const reviewersPerChange = this.selectedChanges.map(
change => change.reviewers[reviewerState] ?? []
);
- if (reviewersPerChange.length === 0) {
- return [];
- }
- // Gets reviewers present in all changes
- return reviewersPerChange.reduce((a, b) =>
- a.filter(reviewer => b.includes(reviewer))
- );
+ return intersection(reviewersPerChange);
}
private createSuggestionsProvider(
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 edcad8f..4a142e4 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
@@ -5,7 +5,7 @@
*/
import {fixture, html} from '@open-wc/testing-helpers';
import {SinonStubbedMember} from 'sinon';
-import {AccountInfo, ReviewerState} from '../../../api/rest-api';
+import {AccountInfo, GroupInfo, ReviewerState} from '../../../api/rest-api';
import {
BulkActionsModel,
bulkActionsModelToken,
@@ -17,6 +17,7 @@
import {
createAccountWithIdNameAndEmail,
createChange,
+ createGroupInfo,
} from '../../../test/test-data-generators';
import {
MockPromise,
@@ -43,6 +44,7 @@
createAccountWithIdNameAndEmail(4),
createAccountWithIdNameAndEmail(5),
];
+const groups: GroupInfo[] = [createGroupInfo('groupId')];
const changes: ChangeInfo[] = [
{
...createChange(),
@@ -225,7 +227,7 @@
dialog,
'gr-account-list#cc-list'
);
- reviewerList.accounts.push(accounts[2]);
+ reviewerList.accounts.push(accounts[2], groups[0]);
ccList.accounts.push(accounts[5]);
await flush();
dialog.confirmButton!.click();
@@ -243,8 +245,21 @@
{
reviewers: [
{reviewer: accounts[2]._account_id, state: ReviewerState.REVIEWER},
+ {reviewer: groups[0].id, state: ReviewerState.REVIEWER},
{reviewer: accounts[5]._account_id, state: ReviewerState.CC},
],
+ ignore_automatic_attention_set_rules: true,
+ // only the reviewer is added to the attention set, not the cc
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: accounts[2]._account_id,
+ },
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: groups[0].id,
+ },
+ ],
},
]);
assert.sameDeepOrderedMembers(saveChangeReviewStub.secondCall.args, [
@@ -253,8 +268,21 @@
{
reviewers: [
{reviewer: accounts[2]._account_id, state: ReviewerState.REVIEWER},
+ {reviewer: groups[0].id, state: ReviewerState.REVIEWER},
{reviewer: accounts[5]._account_id, state: ReviewerState.CC},
],
+ ignore_automatic_attention_set_rules: true,
+ // only the reviewer is added to the attention set, not the cc
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: accounts[2]._account_id,
+ },
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: groups[0].id,
+ },
+ ],
},
]);
});
@@ -357,7 +385,7 @@
);
await flush();
- // prettier and shadoDom string don't agree on long text in divs
+ // prettier and shadowDom string don't agree on long text in divs
expect(element).shadowDom.to.equal(
/* prettier-ignore */
/* HTML */ `
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 2a90bfc..2f5965b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -45,7 +45,6 @@
import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {queryAll} from '../../../utils/common-util';
import {ValueChangedEvent} from '../../../types/events';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
import {Execution} from '../../../constants/reporting';
@@ -309,11 +308,7 @@
const prefColumns = this.preferences.change_table
.map(column => (column === 'Project' ? ColumnNames.REPO : column))
.map(column =>
- this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- ) && column === ColumnNames.STATUS
- ? ColumnNames.STATUS2
- : column
+ column === ColumnNames.STATUS ? ColumnNames.STATUS2 : column
);
this.reporting.reportExecution(Execution.USER_PREFERENCES_COLUMNS, {
statusColumn: prefColumns.includes(ColumnNames.STATUS2),
@@ -336,50 +331,23 @@
if (!config || !config.change) return true;
if (column === 'Comments')
return this.flagsService.isEnabled('comments-column');
- if (column === 'Status') {
- return !this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
- }
- if (column === ColumnNames.STATUS2)
- return this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
+ if (column === 'Status') return false;
+ if (column === ColumnNames.STATUS2) return true;
return true;
}
// private but used in test
computeLabelNames(sections: ChangeListSection[]) {
if (!sections) return [];
- let labels: string[] = [];
- const nonExistingLabel = function (item: string) {
- return !labels.includes(item);
- };
- for (const section of sections) {
- if (!section.results) {
- continue;
- }
- for (const change of section.results) {
- if (!change.labels) {
- continue;
- }
- const currentLabels = Object.keys(change.labels);
- labels = labels.concat(currentLabels.filter(nonExistingLabel));
- }
+ if (this.config?.submit_requirement_dashboard_columns?.length) {
+ return this.config?.submit_requirement_dashboard_columns;
}
-
- if (this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
- if (this.config?.submit_requirement_dashboard_columns?.length) {
- return this.config?.submit_requirement_dashboard_columns;
- } else {
- const changes = sections.map(section => section.results).flat();
- labels = (changes ?? [])
- .map(change => getRequirements(change))
- .flat()
- .map(requirement => requirement.name)
- .filter(unique);
- }
- }
+ const changes = sections.map(section => section.results).flat();
+ const labels = (changes ?? [])
+ .map(change => getRequirements(change))
+ .flat()
+ .map(requirement => requirement.name)
+ .filter(unique);
return labels.sort();
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
index 63a6d8f..1e81123 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -32,6 +32,7 @@
import {
createChange,
createServerInfo,
+ createSubmitRequirementResultInfo,
} from '../../../test/test-data-generators';
import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
@@ -165,23 +166,36 @@
{
...createChange(),
_number: 0 as NumericChangeId,
- labels: {Verified: {approved: {}}},
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Verified',
+ },
+ ],
},
{
...createChange(),
_number: 1 as NumericChangeId,
- labels: {
- Verified: {approved: {}},
- 'Code-Review': {approved: {}},
- },
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Verified',
+ },
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Code-Review',
+ },
+ ],
},
{
...createChange(),
_number: 2 as NumericChangeId,
- labels: {
- Verified: {approved: {}},
- 'Library-Compliance': {approved: {}},
- },
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Library-Compliance',
+ },
+ ],
},
],
},
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index 95ad376..9df22f1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -29,7 +29,6 @@
import '../../shared/gr-linked-chip/gr-linked-chip';
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../gr-submit-requirements/gr-submit-requirements';
-import '../gr-change-requirements/gr-change-requirements';
import '../gr-commit-info/gr-commit-info';
import '../gr-reviewer-list/gr-reviewer-list';
import '../../shared/gr-account-list/gr-account-list';
@@ -83,11 +82,7 @@
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getRevertCreatedChangeIds} from '../../../utils/message-util';
import {Interaction} from '../../../constants/reporting';
-import {
- getApprovalInfo,
- getCodeReviewLabel,
- showNewSubmitRequirements,
-} from '../../../utils/label-util';
+import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
import {LitElement, css, html, nothing, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -179,8 +174,6 @@
private readonly reporting = getAppContext().reportingService;
- private readonly flagsService = getAppContext().flagsService;
-
constructor() {
super();
this.queryTopic = (input: string) => this.getTopicSuggestions(input);
@@ -195,7 +188,6 @@
:host {
display: table;
}
- gr-change-requirements,
gr-submit-requirements {
--requirements-horizontal-padding: var(--metadata-horizontal-padding);
}
@@ -702,23 +694,13 @@
}
private renderSubmitRequirements() {
- if (this.showNewSubmitRequirements()) {
- return html`<div class="separatedSection">
- <gr-submit-requirements
- .change=${this.change}
- .account=${this.account}
- .mutable=${this.mutable}
- ></gr-submit-requirements>
- </div>`;
- } else {
- return html` <div class="oldSeparatedSection">
- <gr-change-requirements
- .change=${this.change}
- .account=${this.account}
- .mutable=${this.mutable}
- ></gr-change-requirements>
- </div>`;
- }
+ return html`<div class="separatedSection">
+ <gr-submit-requirements
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable}
+ ></gr-submit-requirements>
+ </div>`;
}
private renderWeblinks() {
@@ -1213,10 +1195,6 @@
);
}
- private showNewSubmitRequirements() {
- return showNewSubmitRequirements(this.flagsService, this.change);
- }
-
private computeVoteForRole(role: ChangeRole) {
const reviewer = this.getNonOwnerRole(role);
if (reviewer && isAccount(reviewer)) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 2b48697..a383cb1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -199,8 +199,8 @@
<span class="title"> Hashtags </span>
<span class="value"> </span>
</section>
- <div class="oldSeparatedSection">
- <gr-change-requirements></gr-change-requirements>
+ <div class="separatedSection">
+ <gr-submit-requirements></gr-submit-requirements>
</div>
<gr-endpoint-decorator name="change-metadata-item">
<gr-endpoint-param name="labels"> </gr-endpoint-param>
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
deleted file mode 100644
index 821e1ce..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../../styles/shared-styles';
-import '../../../styles/gr-font-styles';
-import '../../shared/gr-button/gr-button';
-import '../../shared/gr-icons/gr-icons';
-import '../../shared/gr-label-info/gr-label-info';
-import '../../shared/gr-limited-text/gr-limited-text';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-requirements_html';
-import {customElement, property, observe} from '@polymer/decorators';
-import {
- ChangeInfo,
- AccountInfo,
- QuickLabelInfo,
- Requirement,
- RequirementType,
- LabelNameToInfoMap,
- LabelInfo,
-} from '../../../types/common';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {getAppContext} from '../../../services/app-context';
-import {labelCompare} from '../../../utils/label-util';
-import {Interaction} from '../../../constants/reporting';
-
-interface ChangeRequirement extends Requirement {
- satisfied: boolean;
- style: string;
-}
-
-interface ChangeWIP {
- type: RequirementType;
- fallback_text: string;
- tooltip: string;
-}
-
-export interface Label {
- labelName: string;
- labelInfo: LabelInfo;
- icon: string;
- style: string;
-}
-
-@customElement('gr-change-requirements')
-export class GrChangeRequirements extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Object})
- change?: ChangeInfo;
-
- @property({type: Object})
- account?: AccountInfo;
-
- @property({type: Boolean})
- mutable?: boolean;
-
- @property({type: Array, computed: '_computeRequirements(change)'})
- _requirements?: Array<ChangeRequirement | ChangeWIP>;
-
- @property({type: Array})
- _requiredLabels: Label[] = [];
-
- @property({type: Array})
- _optionalLabels: Label[] = [];
-
- @property({type: Boolean, computed: '_computeShowWip(change)'})
- _showWip?: boolean;
-
- @property({type: Boolean})
- _showOptionalLabels = true;
-
- private readonly reporting = getAppContext().reportingService;
-
- _computeShowWip(change: ChangeInfo) {
- return change.work_in_progress;
- }
-
- _computeRequirements(change: ChangeInfo) {
- const _requirements: Array<ChangeRequirement | ChangeWIP> = [];
-
- if (change.requirements) {
- for (const requirement of change.requirements) {
- const satisfied = requirement.status === 'OK';
- const style = this._computeRequirementClass(satisfied);
- _requirements.push({...requirement, satisfied, style});
- }
- }
- if (change.work_in_progress) {
- _requirements.push({
- type: 'wip' as RequirementType,
- fallback_text: 'Work-in-progress',
- tooltip: "Change must not be in 'Work in Progress' state.",
- });
- }
-
- return _requirements;
- }
-
- _computeRequirementClass(requirementStatus: boolean) {
- return requirementStatus ? 'approved' : '';
- }
-
- _computeRequirementIcon(requirementStatus: boolean) {
- return requirementStatus ? 'gr-icons:check' : 'gr-icons:schedule';
- }
-
- @observe('change.labels.*')
- _computeLabels(
- labelsRecord: PolymerDeepPropertyChange<
- LabelNameToInfoMap,
- LabelNameToInfoMap
- >
- ) {
- const labels = labelsRecord.base || {};
- const allLabels: Label[] = [];
-
- for (const label of Object.keys(labels).sort(labelCompare)) {
- allLabels.push({
- labelName: label,
- icon: this._computeLabelIcon(labels[label]),
- style: this._computeLabelClass(labels[label]),
- labelInfo: labels[label],
- });
- }
- this._optionalLabels = allLabels.filter(label => label.labelInfo.optional);
- this._requiredLabels = allLabels.filter(label => !label.labelInfo.optional);
- }
-
- /**
- * @return The icon name, or undefined if no icon should
- * be used.
- */
- _computeLabelIcon(labelInfo: QuickLabelInfo) {
- if (labelInfo.approved) {
- return 'gr-icons:check';
- }
- if (labelInfo.rejected) {
- return 'gr-icons:close';
- }
- return 'gr-icons:schedule';
- }
-
- _computeLabelClass(labelInfo: QuickLabelInfo) {
- if (labelInfo.approved) {
- return 'approved';
- }
- if (labelInfo.rejected) {
- return 'rejected';
- }
- return '';
- }
-
- _computeShowOptional(
- optionalFieldsRecord: PolymerDeepPropertyChange<Label[], Label[]>
- ) {
- return optionalFieldsRecord.base.length ? '' : 'hidden';
- }
-
- _computeLabelValue(value: number) {
- return `${value > 0 ? '+' : ''}${value}`;
- }
-
- _computeSectionClass(show: boolean) {
- return show ? '' : 'hidden';
- }
-
- _handleShowHide() {
- this._showOptionalLabels = !this._showOptionalLabels;
- this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, {
- sectionName: 'optional labels',
- toState: this._showOptionalLabels ? 'Show all' : 'Show less',
- });
- }
-
- _computeSubmitRequirementEndpoint(item: ChangeRequirement | ChangeWIP) {
- return `submit-requirement-item-${item.type}`;
- }
-
- _computeShowAllLabelText(_showOptionalLabels: boolean) {
- if (_showOptionalLabels) {
- return 'Show less';
- } else {
- return 'Show all';
- }
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-change-requirements': GrChangeRequirements;
- }
-}
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
deleted file mode 100644
index 0005c90..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- display: table;
- width: 100%;
- }
- .status {
- color: var(--warning-foreground);
- display: inline-block;
- text-align: center;
- vertical-align: top;
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- .approved.status {
- color: var(--positive-green-text-color);
- }
- .rejected.status {
- color: var(--negative-red-text-color);
- }
- iron-icon {
- color: inherit;
- }
- .status iron-icon {
- vertical-align: top;
- }
- gr-endpoint-decorator.submit-requirement-endpoints,
- section {
- display: table-row;
- }
- .show-hide {
- float: right;
- }
- .title {
- min-width: 10em;
- padding: var(--spacing-s) var(--spacing-m) 0
- var(--requirements-horizontal-padding);
- }
- .value {
- padding: var(--spacing-s) 0 0 0;
- }
- .title,
- .value {
- display: table-cell;
- vertical-align: top;
- }
- .hidden {
- display: none;
- }
- .showHide {
- cursor: pointer;
- }
- .showHide .title {
- padding-bottom: var(--spacing-m);
- padding-top: var(--spacing-l);
- }
- .showHide .value {
- padding-top: 0;
- vertical-align: middle;
- }
- .showHide iron-icon {
- color: var(--deemphasized-text-color);
- float: right;
- }
- .show-all-button {
- float: right;
- }
- .show-all-button iron-icon {
- color: inherit;
- --iron-icon-height: 18px;
- --iron-icon-width: 18px;
- }
- .spacer {
- height: var(--spacing-m);
- }
- gr-endpoint-param {
- display: none;
- }
- .metadata-title {
- font-weight: var(--font-weight-bold);
- color: var(--deemphasized-text-color);
- padding-left: var(--metadata-horizontal-padding);
- }
- .title .metadata-title {
- padding-left: 0;
- }
- </style>
- <h3 class="metadata-title heading-3">Submit requirements</h3>
- <template is="dom-repeat" items="[[_requirements]]">
- <gr-endpoint-decorator
- class="submit-requirement-endpoints"
- name$="[[_computeSubmitRequirementEndpoint(item)]]"
- >
- <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
- <gr-endpoint-param name="requirement" value="[[item]]">
- </gr-endpoint-param>
- <div class="title requirement">
- <span class$="status [[item.style]]">
- <iron-icon
- class="icon"
- icon="[[_computeRequirementIcon(item.satisfied)]]"
- ></iron-icon>
- </span>
- <gr-limited-text
- class="name"
- tooltip="[[item.tooltip]]"
- text="[[item.fallback_text]]"
- ></gr-limited-text>
- </div>
- <div class="value">
- <gr-endpoint-slot name="value"></gr-endpoint-slot>
- </div>
- </gr-endpoint-decorator>
- </template>
- <template is="dom-repeat" items="[[_requiredLabels]]">
- <section>
- <div class="title">
- <span class$="status [[item.style]]">
- <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
- </span>
- <gr-limited-text
- class="name"
- text="[[item.labelName]]"
- ></gr-limited-text>
- </div>
- <div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.labelName]]"
- label-info="[[item.labelInfo]]"
- ></gr-label-info>
- </div>
- </section>
- </template>
- <section class="spacer"></section>
- <section
- class$="spacer [[_computeShowOptional(_optionalLabels.*)]]"
- ></section>
- <section class$="showHide [[_computeShowOptional(_optionalLabels.*)]]">
- <div class="title">
- <h3 class="metadata-title">Other labels</h3>
- </div>
- <div class="value">
- <gr-button link="" class="show-all-button" on-click="_handleShowHide"
- >[[_computeShowAllLabelText(_showOptionalLabels)]]
- <iron-icon
- icon="gr-icons:expand-more"
- hidden$="[[_showOptionalLabels]]"
- ></iron-icon
- ><iron-icon
- icon="gr-icons:expand-less"
- hidden$="[[!_showOptionalLabels]]"
- ></iron-icon>
- </gr-button>
- </div>
- </section>
- <template is="dom-repeat" items="[[_optionalLabels]]">
- <section class$="optional [[_computeSectionClass(_showOptionalLabels)]]">
- <div class="title">
- <span class$="status [[item.style]]">
- <template is="dom-if" if="[[item.icon]]">
- <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
- </template>
- <template is="dom-if" if="[[!item.icon]]">
- <span>[[_computeLabelValue(item.labelInfo.value)]]</span>
- </template>
- </span>
- <gr-limited-text
- class="name"
- text="[[item.labelName]]"
- ></gr-limited-text>
- </div>
- <div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.labelName]]"
- label-info="[[item.labelInfo]]"
- ></gr-label-info>
- </div>
- </section>
- </template>
- <section
- class$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"
- ></section>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js
deleted file mode 100644
index 90f9d29..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js
+++ /dev/null
@@ -1,222 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-change-requirements.js';
-import {isHidden} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-change-requirements');
-
-suite('gr-change-metadata tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('requirements computed fields', () => {
- assert.isTrue(element._computeShowWip({work_in_progress: true}));
- assert.isFalse(element._computeShowWip({work_in_progress: false}));
-
- assert.equal(element._computeRequirementClass(true), 'approved');
- assert.equal(element._computeRequirementClass(false), '');
-
- assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
- assert.equal(element._computeRequirementIcon(false),
- 'gr-icons:schedule');
- });
-
- test('label computed fields', () => {
- assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
- assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
- assert.equal(element._computeLabelIcon({}), 'gr-icons:schedule');
-
- assert.equal(element._computeLabelClass({approved: []}), 'approved');
- assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
- assert.equal(element._computeLabelClass({}), '');
- assert.equal(element._computeLabelClass({value: 0}), '');
-
- assert.equal(element._computeLabelValue(1), '+1');
- assert.equal(element._computeLabelValue(-1), '-1');
- assert.equal(element._computeLabelValue(0), '0');
- });
-
- test('_computeLabels', () => {
- assert.equal(element._optionalLabels.length, 0);
- assert.equal(element._requiredLabels.length, 0);
- element._computeLabels({base: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- value: 1,
- },
- opt_test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- optional: true,
- },
- }});
- assert.equal(element._optionalLabels.length, 1);
- assert.equal(element._requiredLabels.length, 1);
-
- assert.equal(element._optionalLabels[0].labelName, 'opt_test');
- assert.equal(element._optionalLabels[0].icon, 'gr-icons:schedule');
- assert.equal(element._optionalLabels[0].style, '');
- assert.ok(element._optionalLabels[0].labelInfo);
- });
-
- test('optional show/hide', () => {
- element._optionalLabels = [{label: 'test'}];
- flush();
-
- assert.ok(element.shadowRoot
- .querySelector('section.optional'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('.show-all-button'));
- flush();
-
- assert.isFalse(element._showOptionalLabels);
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('section.optional')));
- });
-
- test('properly converts satisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: [],
- },
- },
- requirements: [],
- };
- flush();
-
- assert.ok(element.shadowRoot
- .querySelector('.approved'));
- assert.ok(element.shadowRoot
- .querySelector('.name'));
- assert.equal(element.shadowRoot
- .querySelector('.name').text, 'Verified');
- });
-
- test('properly converts unsatisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: false,
- },
- },
- };
- flush();
-
- const name = element.shadowRoot
- .querySelector('.name');
- assert.ok(name);
- assert.isFalse(name.hasAttribute('hidden'));
- assert.equal(name.text, 'Verified');
- });
-
- test('properly displays Work In Progress', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [],
- work_in_progress: true,
- };
- flush();
-
- const changeIsWip = element.shadowRoot
- .querySelector('.title');
- assert.ok(changeIsWip);
- });
-
- test('properly displays a satisfied requirement', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.isFalse(requirement.hasAttribute('hidden'));
- assert.ok(requirement.querySelector('.approved'));
- assert.equal(requirement.querySelector('.name').text,
- 'Resolve all comments');
- });
-
- test('satisfied class is applied with OK', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.ok(requirement.querySelector('.approved'));
- });
-
- test('satisfied class is not applied with NOT_READY', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'NOT_READY',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
-
- test('satisfied class is not applied with RULE_ERROR', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'RULE_ERROR',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 105c0d8..3db1012 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -633,6 +633,12 @@
private connected$ = new BehaviorSubject(false);
+ /**
+ * For `connectedCallback()` to distinguish between connecting to the DOM for
+ * the first time or if just re-connecting.
+ */
+ private isFirstConnection = true;
+
/** Simply reflects the router-model value. */
// visible for testing
routerPatchNum?: PatchSetNum;
@@ -649,12 +655,29 @@
'fullscreen-overlay-opened',
() => this._handleHideBackgroundContent()
);
-
this.addEventListener('fullscreen-overlay-closed', () =>
this._handleShowBackgroundContent()
);
-
this.addEventListener('open-reply-dialog', () => this._openReplyDialog());
+ this.addEventListener('change-message-deleted', () => fireReload(this));
+ this.addEventListener('editable-content-save', e =>
+ this._handleCommitMessageSave(e)
+ );
+ this.addEventListener('editable-content-cancel', () =>
+ this._handleCommitMessageCancel()
+ );
+ this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
+ this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
+
+ this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
+ this._setActivePrimaryTab(e)
+ );
+ this.addEventListener('reload', e => {
+ this.loadData(
+ /* isLocationChange= */ false,
+ /* clearPatchset= */ e.detail && e.detail.clearPatchset
+ );
+ });
}
private setupSubscriptions() {
@@ -699,24 +722,23 @@
override connectedCallback() {
super.connectedCallback();
+ this.firstConnectedCallback();
this.connected$.next(true);
- this.setupSubscriptions();
- this._throttledToggleChangeStar = throttleWrap<KeyboardEvent>(_ =>
- this._handleToggleChangeStar()
- );
- this._getServerConfig().then(config => {
- this._serverConfig = config;
- this._replyDisabled = false;
- });
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this.restApiService.getAccount().then(acct => {
- this._account = acct;
- });
- }
- });
+ // Make sure to reverse everything below this line in disconnectedCallback().
+ // Or consider using either firstConnectedCallback() or constructor().
+ this.setupSubscriptions();
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ document.addEventListener('scroll', this.handleScroll);
+ }
+
+ /**
+ * For initialization that should only happen once, not again when
+ * re-connecting to the DOM later.
+ */
+ private firstConnectedCallback() {
+ if (!this.isFirstConnection) return;
+ this.isFirstConnection = false;
getPluginLoader()
.awaitPluginsLoaded()
@@ -734,26 +756,21 @@
})
.then(() => this._initActiveTabs(this.params));
- this.addEventListener('change-message-deleted', () => fireReload(this));
- this.addEventListener('editable-content-save', e =>
- this._handleCommitMessageSave(e)
+ this._throttledToggleChangeStar = throttleWrap<KeyboardEvent>(_ =>
+ this._handleToggleChangeStar()
);
- this.addEventListener('editable-content-cancel', () =>
- this._handleCommitMessageCancel()
- );
- this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
- this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
- document.addEventListener('visibilitychange', this.handleVisibilityChange);
- document.addEventListener('scroll', this.handleScroll);
+ this._getServerConfig().then(config => {
+ this._serverConfig = config;
+ this._replyDisabled = false;
+ });
- this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
- this._setActivePrimaryTab(e)
- );
- this.addEventListener('reload', e => {
- this.loadData(
- /* isLocationChange= */ false,
- /* clearPatchset= */ e.detail && e.detail.clearPatchset
- );
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this.restApiService.getAccount().then(acct => {
+ this._account = acct;
+ });
+ }
});
}
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 8d82e41..66df866 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
@@ -17,11 +17,9 @@
import '../../../test/common-test-setup-karma';
import '../../shared/gr-date-formatter/gr-date-formatter';
import './gr-file-list';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
import {FilesExpandedState} from '../gr-file-list-constants';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {runA11yAudit} from '../../../test/a11y-test-utils';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {
listenOnce,
mockPromise,
@@ -61,12 +59,7 @@
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {GrEditFileControls} from '../../edit/gr-edit-file-controls/gr-edit-file-controls';
-const commentApiMock = createCommentApiMockWithTemplateElement(
- 'gr-file-list-comment-api-mock',
- html` <gr-file-list id="fileList"></gr-file-list> `
-);
-
-const basicFixture = fixtureFromElement(commentApiMock.is);
+const basicFixture = fixtureFromElement('gr-file-list');
suite('gr-diff a11y test', () => {
test('audit', async () => {
@@ -85,7 +78,6 @@
suite('gr-file-list tests', () => {
let element: GrFileList;
- let commentApiWrapper: any;
let saveStub: sinon.SinonStub;
@@ -103,8 +95,7 @@
// Element must be wrapped in an element with direct access to the
// comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = commentApiWrapper.$.fileList;
+ element = basicFixture.instantiate();
element._loading = false;
element.diffPrefs = {} as DiffPreferencesInfo;
@@ -1976,8 +1967,7 @@
// Element must be wrapped in an element with direct access to the
// comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = commentApiWrapper.$.fileList;
+ element = basicFixture.instantiate();
element.diffPrefs = {} as DiffPreferencesInfo;
element.change = {
...createParsedChange(),
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 2988bc6..6bd3f62 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -27,9 +27,6 @@
DetailedLabelInfo,
} from '../../../types/common';
import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
-import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
-import {classMap} from 'lit/directives/class-map';
import {Label} from '../../../utils/label-util';
import {LabelNameToValuesMap} from '../../../api/rest-api';
@@ -68,12 +65,6 @@
@state()
private selectedValueText = 'No value selected';
- private readonly flagsService = getAppContext().flagsService;
-
- private readonly isSubmitRequirementsUiEnabled = this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
-
static override get styles() {
return [
sharedStyles,
@@ -87,8 +78,6 @@
/* We want the :hover highlight to extend to the border of the dialog. */
.labelNameCell {
padding-left: var(--label-score-padding-left, 0);
- }
- .labelNameCell.newSubmitRequirements {
width: 160px;
}
.selectedValueCell {
@@ -100,9 +89,6 @@
white-space: nowrap;
}
.selectedValueCell {
- width: 75%;
- }
- .selectedValueCell.newSubmitRequirements {
width: 52%;
}
.labelMessage {
@@ -175,13 +161,7 @@
override render() {
return html`
- <span
- class=${classMap({
- labelNameCell: true,
- newSubmitRequirements: this.isSubmitRequirementsUiEnabled,
- })}
- id="labelName"
- aria-hidden="true"
+ <span class="labelNameCell" id="labelName" aria-hidden="true"
>${this.label?.name ?? ''}</span
>
${this.renderButtonsCell()} ${this.renderSelectedValue()}
@@ -257,12 +237,7 @@
private renderSelectedValue() {
return html`
- <div
- class=${classMap({
- selectedValueCell: true,
- newSubmitRequirements: this.isSubmitRequirementsUiEnabled,
- })}
- >
+ <div class="selectedValueCell">
<span id="selectedValueLabel">${this.selectedValueText}</span>
</div>
`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index 27c445e..8e757da 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -24,10 +24,8 @@
LabelNameToValueMap,
} from '../../../types/common';
import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
-import {getAppContext} from '../../../services/app-context';
import {
getTriggerVotes,
- showNewSubmitRequirements,
computeLabels,
Label,
computeOrderedLabelValues,
@@ -48,8 +46,6 @@
@property({type: Object})
account?: AccountInfo;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
fontStyles,
@@ -57,8 +53,6 @@
.scoresTable {
display: table;
width: 100%;
- }
- .scoresTable.newSubmitRequirements {
table-layout: fixed;
}
.mergedMessage,
@@ -91,19 +85,6 @@
}
override render() {
- if (showNewSubmitRequirements(this.flagsService, this.change)) {
- return this.renderNewSubmitRequirements();
- } else {
- return this.renderOldSubmitRequirements();
- }
- }
-
- private renderOldSubmitRequirements() {
- const labels = computeLabels(this.account, this.change);
- return html`${this.renderLabels(labels)}${this.renderErrorMessages()}`;
- }
-
- private renderNewSubmitRequirements() {
return html`${this.renderSubmitReqsLabels()}${this.renderTriggerVotes()}
${this.renderErrorMessages()}`;
}
@@ -145,13 +126,7 @@
}
private renderLabels(labels: Label[]) {
- const newSubReqs = showNewSubmitRequirements(
- this.flagsService,
- this.change
- );
- return html`<div
- class="scoresTable ${newSubReqs ? 'newSubmitRequirements' : ''}"
- >
+ return html`<div class="scoresTable">
${labels
.filter(
label =>
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
index 4bf9d10..f204d76 100644
--- a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
@@ -25,8 +25,6 @@
} from '../../../utils/comment-util';
import {hasOwnProperty} from '../../../utils/common-util';
import {getTriggerVotes} from '../../../utils/label-util';
-import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
const VOTE_RESET_TEXT = '0 (vote reset)';
@@ -102,8 +100,6 @@
`;
}
- private readonly flagsService = getAppContext().flagsService;
-
override render() {
const scores = this._getScores(this.message, this.labelExtremes);
const triggerVotes = getTriggerVotes(this.change);
@@ -112,7 +108,6 @@
private renderScore(score: Score, triggerVotes: string[]) {
if (
- this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI) &&
score.label &&
triggerVotes.includes(score.label) &&
!score.value?.includes(VOTE_RESET_TEXT)
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index b9cb616..ac487f8 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -17,10 +17,8 @@
import '../../../test/common-test-setup-karma';
import './gr-messages-list';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
import {CombinedMessage, GrMessagesList, TEST_ONLY} from './gr-messages-list';
import {MessageTag} from '../../../constants/constants';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {
query,
queryAll,
@@ -43,16 +41,7 @@
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {assertIsDefined} from '../../../utils/common-util';
-createCommentApiMockWithTemplateElement(
- 'gr-messages-list-comment-mock-api',
- html` <gr-messages-list id="messagesList"></gr-messages-list> `
-);
-
-const basicFixture = fixtureFromTemplate(html`
- <gr-messages-list-comment-mock-api>
- <gr-messages-list></gr-messages-list>
- </gr-messages-list-comment-mock-api>
-`);
+const basicFixture = fixtureFromElement('gr-messages-list');
const author = {
_account_id: 42 as AccountId,
@@ -99,8 +88,6 @@
let element: GrMessagesList;
let messages: ChangeMessageInfo[];
- let commentApiWrapper: any;
-
const getMessages = function () {
return queryAll<GrMessage>(element, 'gr-message');
};
@@ -156,13 +143,7 @@
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
messages = generateRandomMessages(3);
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = queryAndAssert<GrMessagesList>(
- commentApiWrapper,
- '#messagesList'
- );
+ element = basicFixture.instantiate();
await element.getCommentsModel().reloadComments(0 as NumericChangeId);
element.messages = messages;
await flush();
@@ -507,8 +488,6 @@
let element: GrMessagesList;
let messages: ChangeMessageInfo[];
- let commentApiWrapper: any;
-
setup(() => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getDiffComments').returns(Promise.resolve({}));
@@ -529,13 +508,7 @@
}),
];
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = queryAndAssert<GrMessagesList>(
- commentApiWrapper,
- '#messagesList'
- );
+ element = basicFixture.instantiate();
element.messages = messages;
flush();
});
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 1f57837..6a9bd92 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
@@ -261,7 +261,7 @@
account?: AccountInfo;
@state()
- ccs: (AccountInfoInput | GroupInfoInput)[] = [];
+ ccs: AccountInput[] = [];
@state()
attentionCcsCount = 0;
@@ -1527,8 +1527,8 @@
if (!this.change?.owner || !this.change?.reviewers) return;
this.owner = this.change.owner;
- const reviewers = [];
- const ccs = [];
+ const reviewers: AccountInput[] = [];
+ const ccs: AccountInput[] = [];
if (this.change.reviewers) {
for (const key of Object.keys(this.change.reviewers)) {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index 6740977..d5ce654 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -29,12 +29,7 @@
LabelInfo,
} from '../../../types/common';
import {hasOwnProperty} from '../../../utils/common-util';
-import {getAppContext} from '../../../services/app-context';
-import {
- getApprovalInfo,
- getCodeReviewLabel,
- showNewSubmitRequirements,
-} from '../../../utils/label-util';
+import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
import {sortReviewers} from '../../../utils/attention-set-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {css} from 'lit';
@@ -68,8 +63,6 @@
@state() showAllReviewers = false;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
sharedStyles,
@@ -166,14 +159,12 @@
.vote=${this.computeVote(reviewer)}
.label=${this.computeCodeReviewLabel()}
>
- ${showNewSubmitRequirements(this.flagsService, this.change)
- ? html`<gr-vote-chip
- slot="vote-chip"
- .vote=${this.computeVote(reviewer)}
- .label=${this.computeCodeReviewLabel()}
- circle-shape
- ></gr-vote-chip>`
- : nothing}
+ <gr-vote-chip
+ slot="vote-chip"
+ .vote=${this.computeVote(reviewer)}
+ .label=${this.computeCodeReviewLabel()}
+ circle-shape
+ ></gr-vote-chip>
</gr-account-chip>
`;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index dda8490..88aae5c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -313,7 +313,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
const leftImage =
@@ -381,7 +381,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
const leftImage =
@@ -445,7 +445,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
@@ -493,7 +493,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
@@ -543,7 +543,7 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
const leftImage =
element.$.diff.$.diffTable.querySelector('td.left img');
assert.isNotOk(leftImage);
@@ -623,7 +623,7 @@
test('clearBlame', () => {
element._blame = [];
- const setBlameSpy = sinon.spy(element.$.diff.$.diffBuilder, 'setBlame');
+ const setBlameSpy = sinon.spy(element.$.diff.diffBuilder, 'setBlame');
element.clearBlame();
assert.isNull(element._blame);
assert.isTrue(setBlameSpy.calledWithExactly(null));
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 dc501c8..2e48771 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
@@ -20,7 +20,6 @@
import '../../shared/revision-info/revision-info';
import './gr-patch-range-select';
import {GrPatchRangeSelect} from './gr-patch-range-select';
-import '../../../test/mocks/comment-api';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {stubRestApi} from '../../../test/test-utils';
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index 73be9f3..a09cdbc 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -17,7 +17,6 @@
import '../../shared/gr-button/gr-button';
import {ServerInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -136,14 +135,7 @@
if (!this.serverConfig?.change) return true;
if (column === ColumnNames.COMMENTS)
return this.flagsService.isEnabled('comments-column');
- if (column === ColumnNames.STATUS)
- return !this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
- if (column === ColumnNames.STATUS2)
- return this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
+ if (column === ColumnNames.STATUS) return false;
return true;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index fdea387..42ef8f4 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -71,12 +71,6 @@
</td>
</tr>
<tr>
- <td><label for="Status"> Status </label></td>
- <td class="checkboxContainer">
- <input checked="" id="Status" name="Status" type="checkbox" />
- </td>
- </tr>
- <tr>
<td><label for="Owner"> Owner </label></td>
<td class="checkboxContainer">
<input checked="" id="Owner" name="Owner" type="checkbox" />
@@ -117,6 +111,12 @@
<input id="Size" name="Size" type="checkbox" />
</td>
</tr>
+ <tr>
+ <td><label for=" Status "> Status </label></td>
+ <td class="checkboxContainer">
+ <input id=" Status " name=" Status " type="checkbox" />
+ </td>
+ </tr>
</tbody>
</table>
</div>`);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 81661e2..76259b9 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -376,9 +376,7 @@
@change=${this.handleToggleDark}
@click=${this.onTapDarkToggle}
></paper-toggle-button>
- <div id="darkThemeToggleLabel">
- Dark theme (the toggle reloads the page)
- </div>
+ <div id="darkThemeToggleLabel">Dark theme</div>
</div>
</section>
<h2
@@ -1161,6 +1159,7 @@
// private but used in test
reloadPage() {
+ fireAlert(this, 'Reloading...');
windowLocationReload();
}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index a514f00..ae42619 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -177,7 +177,7 @@
>
</paper-toggle-button>
<div id="darkThemeToggleLabel">
- Dark theme (the toggle reloads the page)
+ Dark theme
</div>
</div>
</section>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index 689a9fb..7f20a1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -27,7 +27,6 @@
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {ClassInfo, classMap} from 'lit/directives/class-map';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {getLabelStatus, hasVoted, LabelStatus} from '../../../utils/label-util';
@customElement('gr-account-chip')
@@ -94,8 +93,6 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
css`
@@ -252,12 +249,7 @@
}
private computeVoteClasses(): ClassInfo {
- if (
- !this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI) ||
- !this.label ||
- !this.account ||
- !hasVoted(this.label, this.account)
- ) {
+ if (!this.label || !this.account || !hasVoted(this.label, this.account)) {
return {};
}
const status = getLabelStatus(this.label, this.vote?.value);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index 48d6998..3fecd63 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -167,9 +167,6 @@
@property({type: Array})
removableValues?: AccountInput[];
- @property({type: Number})
- maxCount = 0;
-
/**
* Returns suggestion items
*/
@@ -203,7 +200,7 @@
.group {
--account-label-suffix: ' (group)';
}
- .pending-add {
+ .pendingAdd {
font-style: italic;
}
.list {
@@ -234,8 +231,7 @@
</div>
<gr-account-entry
borderless=""
- ?hidden=${(this.maxCount && this.maxCount <= this.accounts.length) ||
- this.readonly}
+ ?hidden=${this.readonly}
id="entry"
.placeholder=${this.placeholder}
@add=${this.handleAdd}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 7b3a93d..26566a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -400,16 +400,6 @@
assert.equal(element.accounts.length, 1);
});
- test('max-count', async () => {
- element.maxCount = 1;
- const acct = makeAccount();
- handleAdd({account: acct, count: 1});
- await element.updateComplete;
- assert.isTrue(
- queryAndAssert<GrAccountEntry>(element, '#entry').hasAttribute('hidden')
- );
- });
-
test('enter text calls suggestions provider', async () => {
const suggestions: Suggestion[] = [
{
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index 48f5d09..3181445 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -22,7 +22,6 @@
import {addShortcut, getEventPath, Key} from '../../../utils/dom-util';
import {getAppContext} from '../../../services/app-context';
import {classMap} from 'lit/directives/class-map';
-import {KnownExperimentId} from '../../../services/flags/flags';
declare global {
interface HTMLElementTagNameMap {
@@ -206,12 +205,6 @@
];
}
- private readonly flagsService = getAppContext().flagsService;
-
- private readonly isSubmitRequirementsUiEnabled = this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
-
override render() {
return html`<paper-button
?raised=${!this.link && !this.flatten}
@@ -220,8 +213,7 @@
tabindex="-1"
part="paper-button"
class=${classMap({
- voteChip: this.voteChip && !this.isSubmitRequirementsUiEnabled,
- newVoteChip: this.voteChip && this.isSubmitRequirementsUiEnabled,
+ newVoteChip: this.voteChip,
})}
>
${this.loading ? html`<span class="loadingSpin"></span>` : ''}
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index b1d3914..ee3bec7 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -18,11 +18,9 @@
import '../../../styles/gr-voting-styles';
import '../../../styles/shared-styles';
import '../gr-vote-chip/gr-vote-chip';
-import '../gr-account-label/gr-account-label';
import '../gr-account-chip/gr-account-chip';
import '../gr-button/gr-button';
import '../gr-icons/gr-icons';
-import '../gr-label/gr-label';
import '../gr-tooltip-content/gr-tooltip-content';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {
@@ -30,9 +28,7 @@
LabelInfo,
ApprovalInfo,
AccountId,
- isQuickLabelInfo,
isDetailedLabelInfo,
- LabelNameToInfoMap,
} from '../../../types/common';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
@@ -40,10 +36,8 @@
import {
canVote,
getApprovalInfo,
- getVotingRangeOrDefault,
hasNeutralStatus,
hasVoted,
- showNewSubmitRequirements,
valueString,
} from '../../../utils/label-util';
import {getAppContext} from '../../../services/app-context';
@@ -61,19 +55,6 @@
}
}
-enum LabelClassName {
- NEGATIVE = 'negative',
- POSITIVE = 'positive',
- MIN = 'min',
- MAX = 'max',
-}
-
-interface FormattedLabel {
- className?: LabelClassName;
- account: ApprovalInfo | AccountInfo;
- value: string;
-}
-
@customElement('gr-label-info')
export class GrLabelInfo extends LitElement {
@property({type: Object})
@@ -107,8 +88,6 @@
private readonly reporting = getAppContext().reportingService;
- private readonly flagsService = getAppContext().flagsService;
-
// TODO(TS): not used, remove later
_xhrPromise?: Promise<void>;
@@ -118,9 +97,6 @@
fontStyles,
votingStyles,
css`
- .placeholder {
- color: var(--deemphasized-text-color);
- }
.hidden {
display: none;
}
@@ -132,33 +108,6 @@
margin-right: var(--spacing-s);
padding: 1px;
}
- .max {
- background-color: var(--vote-color-approved);
- }
- .min {
- background-color: var(--vote-color-rejected);
- }
- .positive {
- background-color: var(--vote-color-recommended);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-recommended);
- color: var(--chip-color);
- }
- .negative {
- background-color: var(--vote-color-disliked);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-disliked);
- color: var(--chip-color);
- }
- .hidden {
- display: none;
- }
- td {
- vertical-align: top;
- }
- tr {
- min-height: var(--line-height-normal);
- }
gr-tooltip-content {
display: block;
}
@@ -173,17 +122,10 @@
gr-button[disabled] iron-icon {
color: var(--border-color);
}
- gr-account-label {
- --account-max-length: 100px;
- margin-right: var(--spacing-xs);
- }
iron-icon {
height: calc(var(--line-height-normal) - 2px);
width: calc(var(--line-height-normal) - 2px);
}
- .labelValueContainer:not(:first-of-type) td {
- padding-top: var(--spacing-s);
- }
.reviewer-row {
padding-top: var(--spacing-s);
}
@@ -208,14 +150,6 @@
}
override render() {
- if (showNewSubmitRequirements(this.flagsService, this.change)) {
- return this.renderNewSubmitRequirements();
- } else {
- return this.renderOldSubmitRequirements();
- }
- }
-
- private renderNewSubmitRequirements() {
const labelInfo = this.labelInfo;
if (!labelInfo) return;
const reviewers = (this.change?.reviewers['REVIEWER'] ?? [])
@@ -238,23 +172,6 @@
</div>`;
}
- private renderOldSubmitRequirements() {
- const labelInfo = this.labelInfo;
- return html` <p
- class="placeholder ${this.computeShowPlaceholder(
- labelInfo,
- this.change?.labels
- )}"
- >
- No votes
- </p>
- <table>
- ${this.mapLabelInfo(labelInfo, this.account, this.change?.labels).map(
- mappedLabel => this.renderLabel(mappedLabel)
- )}
- </table>`;
- }
-
renderReviewerVote(reviewer: AccountInfo) {
const labelInfo = this.labelInfo;
if (!labelInfo) return;
@@ -285,30 +202,6 @@
</div>`;
}
- renderLabel(mappedLabel: FormattedLabel) {
- const {labelInfo, change} = this;
- return html` <tr class="labelValueContainer">
- <td>
- <gr-tooltip-content
- has-tooltip
- title=${this._computeValueTooltip(labelInfo, mappedLabel.value)}
- >
- <gr-label class="${mappedLabel.className} voteChip font-small">
- ${mappedLabel.value}
- </gr-label>
- </gr-tooltip-content>
- </td>
- <td>
- <gr-account-label
- clickable
- .account=${mappedLabel.account}
- .change=${change}
- ></gr-account-label>
- </td>
- <td>${this.renderRemoveVote(mappedLabel.account)}</td>
- </tr>`;
- }
-
private renderVoteAbility(reviewer: AccountInfo) {
if (this.labelInfo && isDetailedLabelInfo(this.labelInfo)) {
const approvalInfo = getApprovalInfo(this.labelInfo, reviewer);
@@ -341,83 +234,6 @@
}
/**
- * This method also listens on change.labels.*,
- * to trigger computation when a label is removed from the change.
- *
- * The third parameter is just for *triggering* computation.
- */
- private mapLabelInfo(
- labelInfo?: LabelInfo,
- account?: AccountInfo,
- _?: LabelNameToInfoMap
- ): FormattedLabel[] {
- const result: FormattedLabel[] = [];
- if (!labelInfo) {
- return result;
- }
- if (!isDetailedLabelInfo(labelInfo)) {
- if (
- isQuickLabelInfo(labelInfo) &&
- (labelInfo.rejected || labelInfo.approved)
- ) {
- const ok = labelInfo.approved || !labelInfo.rejected;
- return [
- {
- value: ok ? '👍️' : '👎️',
- className: ok ? LabelClassName.POSITIVE : LabelClassName.NEGATIVE,
- // executed only if approved or rejected is not undefined
- account: ok ? labelInfo.approved! : labelInfo.rejected!,
- },
- ];
- }
- return result;
- }
-
- // Sort votes by positivity.
- // TODO(TS): maybe mark value as required if always present
- const votes = (labelInfo.all || []).sort(
- (a, b) => (a.value || 0) - (b.value || 0)
- );
- const votingRange = getVotingRangeOrDefault(labelInfo);
- for (const label of votes) {
- if (
- label.value &&
- (!isQuickLabelInfo(labelInfo) ||
- label.value !== labelInfo.default_value)
- ) {
- let labelClassName;
- let labelValPrefix = '';
- if (label.value > 0) {
- labelValPrefix = '+';
- if (label.value === votingRange.max) {
- labelClassName = LabelClassName.MAX;
- } else {
- labelClassName = LabelClassName.POSITIVE;
- }
- } else if (label.value < 0) {
- if (label.value === votingRange.min) {
- labelClassName = LabelClassName.MIN;
- } else {
- labelClassName = LabelClassName.NEGATIVE;
- }
- }
- const formattedLabel: FormattedLabel = {
- value: `${labelValPrefix}${label.value}`,
- className: labelClassName,
- account: label,
- };
- if (label._account_id === account?._account_id) {
- // Put self-votes at the top.
- result.unshift(formattedLabel);
- } else {
- result.push(formattedLabel);
- }
- }
- }
- return result;
- }
-
- /**
* A user is able to delete a vote iff the mutable property is true and the
* reviewer that left the vote exists in the list of removable_reviewers
* received from the backend.
@@ -488,39 +304,4 @@
}
return labelInfo.values[score];
}
-
- /**
- * This method also listens change.labels.* in
- * order to trigger computation when a label is removed from the change.
- *
- * The second parameter is just for *triggering* computation.
- */
- private computeShowPlaceholder(
- labelInfo?: LabelInfo,
- _?: LabelNameToInfoMap
- ) {
- if (!labelInfo) {
- return '';
- }
- if (
- !isDetailedLabelInfo(labelInfo) &&
- isQuickLabelInfo(labelInfo) &&
- (labelInfo.rejected || labelInfo.approved)
- ) {
- return 'hidden';
- }
-
- if (isDetailedLabelInfo(labelInfo) && labelInfo.all) {
- for (const label of labelInfo.all) {
- if (
- label.value &&
- (!isQuickLabelInfo(labelInfo) ||
- label.value !== labelInfo.default_value)
- ) {
- return 'hidden';
- }
- }
- }
- return '';
- }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index 0ac49a7..f1336b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -20,20 +20,18 @@
import {
isHidden,
mockPromise,
- queryAll,
queryAndAssert,
stubRestApi,
} from '../../../test/test-utils';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {GrLabelInfo} from './gr-label-info';
import {GrButton} from '../gr-button/gr-button';
-import {GrLabel} from '../gr-label/gr-label';
import {
createAccountWithIdNameAndEmail,
+ createDetailedLabelInfo,
createParsedChange,
} from '../../../test/test-data-generators';
-import {LabelInfo} from '../../../types/common';
-import {GrAccountLabel} from '../gr-account-label/gr-account-label';
+import {ApprovalInfo, LabelInfo} from '../../../types/common';
const basicFixture = fixtureFromElement('gr-label-info');
@@ -41,12 +39,51 @@
let element: GrLabelInfo;
const account = createAccountWithIdNameAndEmail(5);
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
// Needed to trigger computed bindings.
element.account = {};
- element.change = {...createParsedChange(), labels: {}};
+ element.change = {
+ ...createParsedChange(),
+ labels: {},
+ reviewers: {
+ REVIEWER: [account],
+ CC: [],
+ },
+ };
+ const approval: ApprovalInfo = {
+ value: 2,
+ _account_id: account._account_id,
+ };
+ element.labelInfo = {
+ ...createDetailedLabelInfo(),
+ all: [approval],
+ };
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `<div>
+ <div class="reviewer-row">
+ <gr-account-chip>
+ <gr-vote-chip circle-shape="" slot="vote-chip"> </gr-vote-chip>
+ </gr-account-chip>
+ <gr-tooltip-content has-tooltip="" title="Remove vote">
+ <gr-button
+ aria-disabled="false"
+ aria-label="Remove vote"
+ class="deleteBtn hidden"
+ data-account-id="5"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:delete"> </iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ </div>
+ </div>`);
});
suite('remove reviewer votes', () => {
@@ -62,6 +99,10 @@
element.change = {
...createParsedChange(),
labels: {'Code-Review': label},
+ reviewers: {
+ REVIEWER: [account],
+ CC: [],
+ },
};
element.labelInfo = label;
element.label = 'Code-Review';
@@ -108,101 +149,6 @@
});
});
- suite('label color and order', () => {
- test('valueless label rejected', async () => {
- element.labelInfo = {rejected: {name: 'someone'}};
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('negative'));
- });
-
- test('valueless label approved', async () => {
- element.labelInfo = {approved: {name: 'someone'}};
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('positive'));
- });
-
- test('-2 to +2', async () => {
- element.labelInfo = {
- all: [
- {value: 2, name: 'user 2'},
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 3'},
- {value: -2, name: 'user 4'},
- ],
- values: {
- '-2': 'Awful',
- '-1': "Don't submit as-is",
- ' 0': 'No score',
- '+1': 'Looks good to me',
- '+2': 'Ready to submit',
- },
- };
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- assert.isTrue(labels[2].classList.contains('negative'));
- assert.isTrue(labels[3].classList.contains('min'));
- });
-
- test('-1 to +1', async () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 2'},
- ],
- values: {
- '-1': "Don't submit as-is",
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('min'));
- });
-
- test('0 to +2', async () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 2'},
- {value: 2, name: 'user '},
- ],
- values: {
- ' 0': "Don't submit as-is",
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
- };
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- });
-
- test('self votes at top', async () => {
- const otherAccount = createAccountWithIdNameAndEmail(8);
- element.account = account;
- element.labelInfo = {
- all: [
- {...otherAccount, value: 1},
- {...account, value: -1},
- ],
- values: {
- '-1': "Don't submit as-is",
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- await element.updateComplete;
- const chips = queryAll<GrAccountLabel>(element, 'gr-account-label');
- assert.equal(chips[0].account!._account_id, element.account._account_id);
- });
- });
-
test('_computeValueTooltip', () => {
// Existing label.
let labelInfo: LabelInfo = {values: {0: 'Baz'}};
@@ -218,49 +164,4 @@
score = '0';
assert.equal(element._computeValueTooltip(labelInfo, score), '');
});
-
- test('placeholder', async () => {
- const values = {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- };
- element.labelInfo = {};
- await element.updateComplete;
- assert.isFalse(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {all: [], values};
- await element.updateComplete;
- assert.isFalse(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {all: [{value: 1}], values};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {rejected: account};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {rejected: account, all: [{value: 1}], values};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {approved: account};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {approved: account, all: [{value: 1}], values};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
deleted file mode 100644
index 842b35e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @fileoverview Consider removing this element as
- * its functionality seems to be duplicated with gr-tooltip and only
- * used in gr-label-info.
- */
-
-import {html, LitElement} from 'lit';
-import {customElement} from 'lit/decorators';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-label': GrLabel;
- }
-}
-
-@customElement('gr-label')
-export class GrLabel extends LitElement {
- static override get styles() {
- return [];
- }
-
- override render() {
- return html` <slot></slot> `;
- }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
deleted file mode 100644
index 94196df..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html` <slot></slot> `;
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 146a01e..5859731 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -23,8 +23,6 @@
isQuickLabelInfo,
LabelInfo,
} from '../../../api/rest-api';
-import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {
classForLabelStatus,
getLabelStatus,
@@ -61,8 +59,6 @@
@property({type: Boolean, attribute: 'tooltip-with-who-voted'})
tooltipWithWhoVoted = false;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
css`
@@ -131,9 +127,6 @@
}
override render() {
- if (!this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI))
- return;
-
const renderValue = this.renderValue();
if (!renderValue) return;
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index a5effaf..47f5f81 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -462,7 +462,7 @@
numLines: number,
referenceLine: number
) {
- assertIsDefined(this.diff, 'diff');
+ if (!this.diff?.meta_b) return;
const syntaxTree = this.diff.meta_b.syntax_tree;
const outlineSyntaxPath = findBlockTreePathForLine(
referenceLine,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 269b56d..d200c75 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -1,24 +1,11 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import '../gr-diff-processor/gr-diff-processor';
import '../../../elements/shared/gr-hovercard/gr-hovercard';
import './gr-diff-builder-side-by-side';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-builder-element_html';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
import {DiffBuilder, DiffContextExpandedEventDetail} from './gr-diff-builder';
import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
@@ -26,7 +13,6 @@
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
import {CancelablePromise, makeCancelable} from '../../../scripts/util';
-import {customElement, property, observe} from '@polymer/decorators';
import {BlameInfo, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {CoverageRange, DiffLayer} from '../../../types/types';
@@ -53,18 +39,35 @@
hideInContextControl,
} from '../gr-diff/gr-diff-group';
import {getLineNumber, getSideByLineEl} from '../gr-diff/gr-diff-utils';
-import {fireAlert, fireEvent, fire} from '../../../utils/event-util';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
+import {
+ fireAlert,
+ fire,
+ HTMLElementEventDetailType,
+} from '../../../utils/event-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {afterNextRender} from '../../../utils/dom-util';
const TRAILING_WHITESPACE_PATTERN = /\s+$/;
-
-// https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
const COMMIT_MSG_PATH = '/COMMIT_MSG';
const COMMIT_MSG_LINE_LENGTH = 72;
declare global {
interface HTMLElementEventMap {
+ /**
+ * Fired when the diff begins rendering - both for full renders and for
+ * partial rerenders.
+ */
+ 'render-start': CustomEvent<{}>;
+ /**
+ * Fired whenever a new chunk of lines has been rendered synchronously - this
+ * only happens for full renders.
+ */
'render-progress': CustomEvent<RenderProgressEventDetail>;
+ /**
+ * Fired when the diff finishes rendering text content - both for full
+ * renders and for partial rerenders.
+ */
+ 'render-content': CustomEvent<{}>;
}
}
@@ -97,112 +100,59 @@
}
}
-@customElement('gr-diff-builder')
-export class GrDiffBuilderElement
- extends PolymerElement
- implements GroupConsumer
-{
- static get template() {
- return htmlTemplate;
- }
-
- /**
- * Fired when the diff begins rendering - both for full renders and for
- * partial rerenders.
- *
- * @event render-start
- */
-
- /**
- * Fired whenever a new chunk of lines has been rendered synchronously - this
- * only happens for full renders.
- *
- * @event render-progress
- */
-
- /**
- * Fired when the diff finishes rendering text content - both for full
- * renders and for partial rerenders.
- *
- * @event render-content
- */
-
- @property({type: Object})
+// TODO: Rename the class and the file and remove "element". This is not an
+// element anymore.
+export class GrDiffBuilderElement implements GroupConsumer {
diff?: DiffInfo;
- @property({type: String})
+ diffElement?: HTMLTableElement;
+
viewMode?: string;
- @property({type: Boolean})
isImageDiff?: boolean;
- @property({type: Object})
baseImage: ImageInfo | null = null;
- @property({type: Object})
revisionImage: ImageInfo | null = null;
- @property({type: Number})
- parentIndex?: number;
-
- @property({type: String})
path?: string;
- @property({type: Object})
prefs: DiffPreferencesInfo = createDefaultDiffPrefs();
- @property({type: Object})
renderPrefs?: RenderPreferences;
- @property({type: Object})
- _builder?: DiffBuilder;
-
- /**
- * The gr-diff-processor adds (and only adds!) to this array. It does so by
- * using `this.push()` and Polymer's two-way data binding.
- * Below (@observe('_groups.splices')) we are observing the groups that the
- * processor adds, and pass them on to the builder for rendering. Henceforth
- * the builder groups are the source of truth, because when
- * expanding/collapsing groups only the builder is updated. This field and the
- * corresponsing one in the processor are not updated.
- */
- @property({type: Array})
- _groups: GrDiffGroup[] = [];
+ useNewImageDiffUi = false;
/**
* Layers passed in from the outside.
+ *
+ * See `layersInternal` for where these layers will end up together with the
+ * internal layers.
*/
- @property({type: Array})
layers: DiffLayer[] = [];
+ // visible for testing
+ builder?: DiffBuilder;
+
/**
- * All layers, both from the outside and the default ones.
+ * All layers, both from the outside and the default ones. See `layers` for
+ * the property that can be set from the outside.
*/
- @property({type: Array})
- _layers: DiffLayer[] = [];
+ // visible for testing
+ layersInternal: DiffLayer[] = [];
- @property({type: Boolean})
- _showTabs?: boolean;
+ // visible for testing
+ showTabs?: boolean;
- @property({type: Boolean})
- _showTrailingWhitespace?: boolean;
-
- @property({type: Array})
- commentRanges: CommentRangeLayer[] = [];
-
- @property({type: Array, observer: 'coverageObserver'})
- coverageRanges: CoverageRange[] = [];
-
- @property({type: Boolean})
- useNewImageDiffUi = false;
+ // visible for testing
+ showTrailingWhitespace?: boolean;
/**
* The promise last returned from `render()` while the asynchronous
* rendering is running - `null` otherwise. Provides a `cancel()`
* method that rejects it with `{isCancelled: true}`.
*/
- @property({type: Object})
- _cancelableRenderPromise: CancelablePromise<unknown> | null = null;
+ private cancelableRenderPromise: CancelablePromise<unknown> | null = null;
private coverageLayerLeft = new GrCoverageLayer(Side.LEFT);
@@ -210,51 +160,20 @@
private rangeLayer = new GrRangedCommentLayer();
- private processor = new GrDiffProcessor();
+ // visible for testing
+ processor = new GrDiffProcessor();
constructor() {
- super();
- afterNextRender(this, () => {
- this.addEventListener(
- 'diff-context-expanded',
- (e: CustomEvent<DiffContextExpandedEventDetail>) => {
- // Don't stop propagation. The host may listen for reporting or
- // resizing.
- this.replaceGroup(e.detail.contextGroup, e.detail.groups);
- }
- );
- });
this.processor.consumer = this;
}
- override disconnectedCallback() {
- this.processor.cancel();
- if (this._builder) {
- this._builder.clear();
- }
- super.disconnectedCallback();
+ updateCommentRanges(ranges: CommentRangeLayer[]) {
+ this.rangeLayer.updateRanges(ranges);
}
- get diffElement(): HTMLTableElement {
- // Not searching in shadowRoot, because the diff table is slotted!
- return this.querySelector('#diffTable') as HTMLTableElement;
- }
-
- @observe('commentRanges.*')
- rangeObserver() {
- this.rangeLayer.updateRanges(this.commentRanges);
- }
-
- coverageObserver(coverageRanges: CoverageRange[]) {
- const leftRanges = coverageRanges.filter(
- range => range && range.side === Side.LEFT
- );
- this.coverageLayerLeft.setRanges(leftRanges);
-
- const rightRanges = coverageRanges.filter(
- range => range && range.side === Side.RIGHT
- );
- this.coverageLayerRight.setRanges(rightRanges);
+ updateCoverageRanges(rs: CoverageRange[]) {
+ this.coverageLayerLeft.setRanges(rs.filter(r => r?.side === Side.LEFT));
+ this.coverageLayerRight.setRanges(rs.filter(r => r?.side === Side.RIGHT));
}
render(keyLocations: KeyLocations): void {
@@ -262,42 +181,44 @@
// installed, and |render| satisfies the requirement, however,
// |attached| doesn't because in the diff view page, the element is
// attached before plugins are installed.
- this._setupAnnotationLayers();
+ this.setupAnnotationLayers();
- this._showTabs = this.prefs.show_tabs;
- this._showTrailingWhitespace = this.prefs.show_whitespace_errors;
+ this.showTabs = this.prefs.show_tabs;
+ this.showTrailingWhitespace = this.prefs.show_whitespace_errors;
// Stop the processor if it's running.
this.cancel();
- if (this._builder) {
- this._builder.clear();
- }
- if (!this.diff) {
- throw Error('Cannot render a diff without DiffInfo.');
- }
- this._builder = this._getDiffBuilder();
+ this.builder?.clear();
+ assertIsDefined(this.diff, 'diff');
+ assertIsDefined(this.diffElement, 'diff table');
+ this.builder = this.getDiffBuilder();
this.processor.context = this.prefs.context;
this.processor.keyLocations = keyLocations;
- this._clearDiffContent();
- this._builder.addColumns(
+ this.diffElement.addEventListener(
+ 'diff-context-expanded',
+ this.onDiffContextExpanded
+ );
+
+ this.clearDiffContent();
+ this.builder.addColumns(
this.diffElement,
getLineNumberCellWidth(this.prefs)
);
const isBinary = !!(this.isImageDiff || this.diff.binary);
- fireEvent(this, 'render-start');
- this._cancelableRenderPromise = makeCancelable(
+ this.fireDiffEvent('render-start', {});
+ this.cancelableRenderPromise = makeCancelable(
this.processor
.process(this.diff.content, isBinary)
.then(() => {
if (this.isImageDiff) {
- (this._builder as GrDiffBuilderImage).renderDiff();
+ (this.builder as GrDiffBuilderImage).renderDiff();
}
- afterNextRender(this, () => fireEvent(this, 'render-content'));
+ afterNextRender(() => this.fireDiffEvent('render-content', {}));
})
// Mocha testing does not like uncaught rejections, so we catch
// the cancels which are expected and should not throw errors in
@@ -307,17 +228,39 @@
return;
})
.finally(() => {
- this._cancelableRenderPromise = null;
+ this.cancelableRenderPromise = null;
})
);
}
- _setupAnnotationLayers() {
+ private onDiffContextExpanded = (
+ e: CustomEvent<DiffContextExpandedEventDetail>
+ ) => {
+ // Don't stop propagation. The host may listen for reporting or
+ // resizing.
+ this.replaceGroup(e.detail.contextGroup, e.detail.groups);
+ };
+
+ private fireDiffEvent<K extends keyof HTMLElementEventMap>(
+ type: K,
+ detail: HTMLElementEventDetailType<K>
+ ) {
+ assertIsDefined(this.diffElement, 'diff table');
+ fire(this.diffElement, type, detail);
+ }
+
+ private fireDiffEventRenderProgress(detail: RenderProgressEventDetail) {
+ assertIsDefined(this.diffElement, 'diff table');
+ fire(this.diffElement, 'render-progress', detail);
+ }
+
+ // visible for testing
+ setupAnnotationLayers() {
const layers: DiffLayer[] = [
- this._createTrailingWhitespaceLayer(),
- this._createIntralineLayer(),
- this._createTabIndicatorLayer(),
- this._createSpecialCharacterIndicatorLayer(),
+ this.createTrailingWhitespaceLayer(),
+ this.createIntralineLayer(),
+ this.createTabIndicatorLayer(),
+ this.createSpecialCharacterIndicatorLayer(),
this.rangeLayer,
this.coverageLayerLeft,
this.coverageLayerRight,
@@ -326,15 +269,15 @@
if (this.layers) {
layers.push(...this.layers);
}
- this._layers = layers;
+ this.layersInternal = layers;
}
getContentTdByLine(lineNumber: LineNumber, side?: Side, root?: Element) {
- if (!this._builder) return null;
- return this._builder.getContentTdByLine(lineNumber, side, root);
+ if (!this.builder) return null;
+ return this.builder.getContentTdByLine(lineNumber, side, root);
}
- _getDiffRowByChild(child: Element) {
+ private getDiffRowByChild(child: Element) {
while (!child.classList.contains('diff-row') && child.parentElement) {
child = child.parentElement;
}
@@ -348,23 +291,23 @@
const side = getSideByLineEl(lineEl);
// Performance optimization because we already have an element in the
// correct row
- const row = this._getDiffRowByChild(lineEl);
+ const row = this.getDiffRowByChild(lineEl);
return this.getContentTdByLine(line, side, row);
}
getLineElByNumber(lineNumber: LineNumber, side?: Side) {
- if (!this._builder) return null;
- return this._builder.getLineElByNumber(lineNumber, side);
+ if (!this.builder) return null;
+ return this.builder.getLineElByNumber(lineNumber, side);
}
getLineNumberRows() {
- if (!this._builder) return [];
- return this._builder.getLineNumberRows();
+ if (!this.builder) return [];
+ return this.builder.getLineNumberRows();
}
getLineNumEls(side: Side) {
- if (!this._builder) return [];
- return this._builder.getLineNumEls(side);
+ if (!this.builder) return [];
+ return this.builder.getLineNumEls(side);
}
/**
@@ -376,8 +319,8 @@
* @param side The side the line number refer to.
*/
unhideLine(lineNum: number, side: Side) {
- if (!this._builder) return;
- const group = this._builder.findGroup(side, lineNum);
+ if (!this.builder) return;
+ const group = this.builder.findGroup(side, lineNum);
// Cannot unhide a line that is not part of the diff.
if (!group) return;
// If it's already visible, great!
@@ -420,45 +363,50 @@
contextGroup: GrDiffGroup,
newGroups: readonly GrDiffGroup[]
) {
- if (!this._builder) return;
- fireEvent(this, 'render-start');
+ if (!this.builder) return;
+ this.fireDiffEvent('render-start', {});
const linesRendered = newGroups.reduce(
(sum, group) => sum + group.lines.length,
0
);
- this._builder.replaceGroup(contextGroup, newGroups);
- afterNextRender(this, () => {
- fire(this, 'render-progress', {linesRendered});
- fireEvent(this, 'render-content');
+ this.builder.replaceGroup(contextGroup, newGroups);
+ afterNextRender(() => {
+ this.fireDiffEvent('render-progress', {linesRendered});
+ this.fireDiffEvent('render-content', {});
});
}
cancel() {
this.processor.cancel();
- if (this._cancelableRenderPromise) {
- this._cancelableRenderPromise.cancel();
- this._cancelableRenderPromise = null;
- }
+ this.builder?.clear();
+ this.cancelableRenderPromise?.cancel();
+ this.cancelableRenderPromise = null;
+ this.diffElement?.removeEventListener(
+ 'diff-context-expanded',
+ this.onDiffContextExpanded
+ );
}
- _handlePreferenceError(pref: string): never {
+ // visible for testing
+ handlePreferenceError(pref: string): never {
const message =
`The value of the '${pref}' user preference is ` +
'invalid. Fix in diff preferences';
- fireAlert(this, message);
+ assertIsDefined(this.diffElement, 'diff table');
+ fireAlert(this.diffElement, message);
throw Error(`Invalid preference value: ${pref}`);
}
- _getDiffBuilder(): DiffBuilder {
- if (!this.diff) {
- throw Error('Cannot render a diff without DiffInfo.');
- }
+ // visible for testing
+ getDiffBuilder(): DiffBuilder {
+ assertIsDefined(this.diff, 'diff');
+ assertIsDefined(this.diffElement, 'diff table');
if (isNaN(this.prefs.tab_size) || this.prefs.tab_size <= 0) {
- this._handlePreferenceError('tab size');
+ this.handlePreferenceError('tab size');
}
if (isNaN(this.prefs.line_length) || this.prefs.line_length <= 0) {
- this._handlePreferenceError('diff width');
+ this.handlePreferenceError('diff width');
}
const localPrefs = {...this.prefs};
@@ -487,7 +435,7 @@
this.diff,
localPrefs,
this.diffElement,
- this._layers,
+ this.layersInternal,
this.renderPrefs
);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
@@ -495,7 +443,7 @@
this.diff,
localPrefs,
this.diffElement,
- this._layers,
+ this.layersInternal,
this.renderPrefs
);
}
@@ -505,7 +453,8 @@
return builder;
}
- _clearDiffContent() {
+ private clearDiffContent() {
+ assertIsDefined(this.diffElement, 'diff table');
this.diffElement.innerHTML = '';
}
@@ -514,22 +463,23 @@
* server into chunks.
*/
clearGroups() {
- if (!this._builder) return;
- this._builder.clearGroups();
+ if (!this.builder) return;
+ this.builder.clearGroups();
}
/**
* Called when the processor is done converting a chunk of the diff.
*/
addGroup(group: GrDiffGroup) {
- if (!this._builder) return;
- this._builder.addGroups([group]);
- afterNextRender(this, () =>
- fire(this, 'render-progress', {linesRendered: group.lines.length})
+ if (!this.builder) return;
+ this.builder.addGroups([group]);
+ afterNextRender(() =>
+ this.fireDiffEventRenderProgress({linesRendered: group.lines.length})
);
}
- _createIntralineLayer(): DiffLayer {
+ // visible for testing
+ createIntralineLayer(): DiffLayer {
return {
// Take a DIV.contentText element and a line object with intraline
// differences to highlight and apply them to the element as
@@ -561,8 +511,9 @@
};
}
- _createTabIndicatorLayer(): DiffLayer {
- const show = () => this._showTabs;
+ // visible for testing
+ createTabIndicatorLayer(): DiffLayer {
+ const show = () => this.showTabs;
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
// If visible tabs are disabled, do nothing.
@@ -576,7 +527,7 @@
};
}
- _createSpecialCharacterIndicatorLayer(): DiffLayer {
+ private createSpecialCharacterIndicatorLayer(): DiffLayer {
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
// Find and annotate the locations of soft hyphen (\u00AD)
@@ -592,8 +543,9 @@
};
}
- _createTrailingWhitespaceLayer(): DiffLayer {
- const show = () => this._showTrailingWhitespace;
+ // visible for testing
+ createTrailingWhitespaceLayer(): DiffLayer {
+ const show = () => this.showTrailingWhitespace;
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
@@ -621,18 +573,12 @@
}
setBlame(blame: BlameInfo[] | null) {
- if (!this._builder) return;
- this._builder.setBlame(blame ?? []);
+ if (!this.builder) return;
+ this.builder.setBlame(blame ?? []);
}
updateRenderPrefs(renderPrefs: RenderPreferences) {
- this._builder?.updateRenderPrefs(renderPrefs);
+ this.builder?.updateRenderPrefs(renderPrefs);
this.processor.updateRenderPrefs(renderPrefs);
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-builder': GrDiffBuilderElement;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
deleted file mode 100644
index bd0e034..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <div class="contentWrapper">
- <slot></slot>
- </div>
-`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js
deleted file mode 100644
index 8c15ddd..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ /dev/null
@@ -1,1084 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../../test/common-test-setup-karma.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import './gr-diff-builder-element.js';
-import {stubBaseUrl} from '../../../test/test-utils.js';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
-import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {DiffViewMode, Side} from '../../../api/diff.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy.js';
-import {waitForEventOnce} from '../../../utils/event-util.js';
-
-const basicFixture = fixtureFromTemplate(html`
- <gr-diff-builder>
- <table id="diffTable"></table>
- </gr-diff-builder>
-`);
-
-const divWithTextFixture = fixtureFromTemplate(html`
-<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
-`);
-
-const mockDiffFixture = fixtureFromTemplate(html`
-<gr-diff-builder view-mode="SIDE_BY_SIDE">
- <table id="diffTable"></table>
- </gr-diff-builder>
-`);
-
-// GrDiffBuilderElement forces these prefs to be set - tests that do not care
-// about these values can just set these defaults.
-const DEFAULT_PREFS = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
-};
-
-suite('gr-diff-builder tests', () => {
- let prefs;
- let element;
- let builder;
-
- const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
- const WBR_HTML = '<wbr class="style-scope gr-diff">';
-
- setup(() => {
- element = basicFixture.instantiate();
- stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- stubRestApi('getProjectConfig').returns(Promise.resolve({}));
- stubBaseUrl('/r');
- prefs = {...DEFAULT_PREFS};
- builder = new GrDiffBuilderLegacy({content: []}, prefs);
- });
-
- test('line_length applied with <wbr> if line_wrapping is true', () => {
- builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
-
- const line = {text, highlights: []};
- const expected = 'a'.repeat(50) + WBR_HTML + 'a';
- const result = builder.createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, expected);
- });
-
- test('line_length applied with line break if line_wrapping is false', () => {
- builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
-
- const line = {text, highlights: []};
- const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
- const result = builder.createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, expected);
- });
-
- [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE]
- .forEach(mode => {
- test(`line_length used for regular files under ${mode}`, () => {
- element.path = '/a.txt';
- element.viewMode = mode;
- element.diff = {};
- element.prefs = {tab_size: 4, line_length: 50};
- builder = element._getDiffBuilder();
- assert.equal(builder._prefs.line_length, 50);
- });
-
- test(`line_length ignored for commit msg under ${mode}`, () => {
- element.path = '/COMMIT_MSG';
- element.viewMode = mode;
- element.diff = {};
- element.prefs = {tab_size: 4, line_length: 50};
- builder = element._getDiffBuilder();
- assert.equal(builder._prefs.line_length, 72);
- });
- });
-
- test('createTextEl linewrap with tabs', () => {
- const text = '\t'.repeat(7) + '!';
- const line = {text, highlights: []};
- const el = builder.createTextEl(undefined, line);
- assert.equal(el.innerText, text);
- // With line length 10 and tab size 2, there should be a line break
- // after every two tabs.
- const newlineEl = el.querySelector('.contentText > .br');
- assert.isOk(newlineEl);
- assert.equal(
- el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
- newlineEl);
- });
-
- test('_handlePreferenceError throws with invalid preference', () => {
- element.prefs = {tab_size: 0};
- assert.throws(() => element._getDiffBuilder());
- });
-
- test('_handlePreferenceError triggers alert and javascript error', () => {
- const errorStub = sinon.stub();
- element.addEventListener('show-alert', errorStub);
- assert.throws(() => element._handlePreferenceError('tab size'));
- assert.equal(errorStub.lastCall.args[0].detail.message,
- `The value of the 'tab size' user preference is invalid. ` +
- `Fix in diff preferences`);
- });
-
- suite('intraline differences', () => {
- let el;
- let str;
- let annotateElementSpy;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- function slice(str, start, end) {
- return Array.from(str).slice(start, end)
- .join('');
- }
-
- setup(() => {
- el = divWithTextFixture.instantiate();
- str = el.textContent;
- annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
- layer = document.createElement('gr-diff-builder')
- ._createIntralineLayer();
- });
-
- test('annotate no highlights', () => {
- const line = {
- text: str,
- highlights: [],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- // The content is unchanged.
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(str, el.childNodes[0].textContent);
- });
-
- test('annotate with highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- {startIndex: 18, endIndex: 22},
- ],
- };
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12, 18);
- const str3 = slice(str, 18, 22);
- const str4 = slice(str, 22);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 5);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
-
- assert.notInstanceOf(el.childNodes[3], Text);
- assert.equal(el.childNodes[3].textContent, str3);
-
- assert.instanceOf(el.childNodes[4], Text);
- assert.equal(el.childNodes[4].textContent, str4);
- });
-
- test('annotate without endIndex', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28},
- ],
- };
-
- const str0 = slice(str, 0, 28);
- const str1 = slice(str, 28);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
-
- test('annotate ignores empty highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28, endIndex: 28},
- ],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- });
-
- test('annotate handles unicode', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 3);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
- });
-
- test('annotate handles unicode w/o endIndex', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
-
- const line = {
- text: str,
- highlights: [
- {startIndex: 6},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
- });
-
- suite('tab indicators', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = basicFixture.instantiate();
- element._showTabs = true;
- layer = element._createTabIndicatorLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no tabs', () => {
- const str = 'lorem ipsum no tabs';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates tab at beginning', () => {
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('does not annotate when disabled', () => {
- element._showTabs = false;
-
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates multiple in beginning', () => {
- const str = '\t\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 2);
-
- let args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
-
- args = annotateElementStub.getCalls()[1].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 1, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('annotates intermediate tabs', () => {
- const str = 'lorem\tupsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 5, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
- });
-
- suite('layers', () => {
- let element;
- let initialLayersCount;
- let withLayerCount;
- setup(() => {
- const layers = [];
- element = basicFixture.instantiate();
- element.layers = layers;
- element._showTrailingWhitespace = true;
- element._setupAnnotationLayers();
- initialLayersCount = element._layers.length;
- });
-
- test('no layers', () => {
- element._setupAnnotationLayers();
- assert.equal(element._layers.length, initialLayersCount);
- });
-
- suite('with layers', () => {
- const layers = [{}, {}];
- setup(() => {
- element = basicFixture.instantiate();
- element.layers = layers;
- element._showTrailingWhitespace = true;
- element._setupAnnotationLayers();
- withLayerCount = element._layers.length;
- });
- test('with layers', () => {
- element._setupAnnotationLayers();
- assert.equal(element._layers.length, withLayerCount);
- assert.equal(initialLayersCount + layers.length,
- withLayerCount);
- });
- });
- });
-
- suite('trailing whitespace', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = basicFixture.instantiate();
- element._showTrailingWhitespace = true;
- layer = element._createTrailingWhitespaceLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no trailing whitespace', () => {
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates trailing spaces', () => {
- const str = 'lorem ipsum ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates trailing tabs', () => {
- const str = 'lorem ipsum\t\t\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates mixed trailing whitespace', () => {
- const str = 'lorem ipsum\t \t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('unicode preceding trailing whitespace', () => {
- const str = '💢\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 1);
- assert.equal(annotateElementStub.lastCall.args[2], 1);
- });
-
- test('does not annotate when disabled', () => {
- element._showTrailingWhitespace = false;
- const str = 'lorem upsum\t \t ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
- });
-
- suite('rendering text, images and binary files', () => {
- let processStub;
- let keyLocations;
- let content;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.viewMode = 'SIDE_BY_SIDE';
- processStub = sinon.stub(element.processor, 'process')
- .returns(Promise.resolve());
- keyLocations = {left: {}, right: {}};
- element.prefs = {
- ...DEFAULT_PREFS,
- context: -1,
- syntax_highlighting: true,
- };
- content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
- });
-
- test('text', async () => {
- element.diff = {content};
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isFalse(processStub.lastCall.args[1]);
- });
-
- test('image', async () => {
- element.diff = {content, binary: true};
- element.isImageDiff = true;
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
-
- test('binary', async () => {
- element.diff = {content, binary: true};
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
- });
-
- suite('rendering', () => {
- let content;
- let outputEl;
- let keyLocations;
-
- setup(async () => {
- const prefs = {...DEFAULT_PREFS};
- content = [
- {
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- },
- {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- },
- ];
- element = basicFixture.instantiate();
- sinon.stub(element, 'dispatchEvent');
- outputEl = element.querySelector('#diffTable');
- keyLocations = {left: {}, right: {}};
- sinon.stub(element, '_getDiffBuilder').callsFake(() => {
- const builder = new GrDiffBuilderSideBySide({content}, prefs, outputEl);
- sinon.stub(builder, 'addColumns');
- builder.buildSectionElement = function(group) {
- const section = document.createElement('stub');
- section.textContent = group.lines
- .reduce((acc, line) => acc + line.text, '');
- return section;
- };
- return builder;
- });
- element.diff = {content};
- element.prefs = prefs;
- await element.render(keyLocations);
- });
-
- test('addColumns is called', () => {
- assert.isTrue(element._builder.addColumns.called);
- });
-
- test('getGroupsByLineRange one line', () => {
- const section = outputEl.querySelector('stub:nth-of-type(3)');
- const groups = element._builder.getGroupsByLineRange(1, 1, 'left');
- assert.equal(groups.length, 1);
- assert.strictEqual(groups[0].element, section);
- });
-
- test('getGroupsByLineRange over diff', () => {
- const section = [
- outputEl.querySelector('stub:nth-of-type(3)'),
- outputEl.querySelector('stub:nth-of-type(4)'),
- ];
- const groups = element._builder.getGroupsByLineRange(1, 2, 'left');
- assert.equal(groups.length, 2);
- assert.strictEqual(groups[0].element, section[0]);
- assert.strictEqual(groups[1].element, section[1]);
- });
-
- test('render-start and render-content are fired', async () => {
- await new Promise(resolve => afterNextRender(element, resolve));
- const firedEventTypes = element.dispatchEvent.getCalls()
- .map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-start');
- assert.include(firedEventTypes, 'render-content');
- });
-
- test('cancel cancels the processor', () => {
- const processorCancelStub = sinon.stub(element.processor, 'cancel');
- element.cancel();
- assert.isTrue(processorCancelStub.called);
- });
- });
-
- suite('context hiding and expanding', () => {
- setup(async () => {
- element = basicFixture.instantiate();
- sinon.stub(element, 'dispatchEvent');
- const afterNextRenderPromise = new Promise((resolve, reject) => {
- afterNextRender(element, resolve);
- });
- element.diff = {
- content: [
- {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${i}`)},
- {a: ['before'], b: ['after']},
- {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${10 + i}`)},
- ],
- };
- element.viewMode = DiffViewMode.SIDE_BY_SIDE;
-
- const keyLocations = {left: {}, right: {}};
- element.prefs = {
- ...DEFAULT_PREFS,
- context: 1,
- };
- await element.render(keyLocations);
- // Make sure all listeners are installed.
- await afterNextRenderPromise;
- });
-
- test('hides lines behind two context controls', () => {
- const contextControls = element.querySelectorAll('gr-context-controls');
- assert.equal(contextControls.length, 2);
-
- const diffRows = element.querySelectorAll('.diff-row');
- // The first two are LOST and FILE line
- assert.equal(diffRows.length, 2 + 1 + 1 + 1);
- assert.include(diffRows[2].textContent, 'unchanged 10');
- assert.include(diffRows[3].textContent, 'before');
- assert.include(diffRows[3].textContent, 'after');
- assert.include(diffRows[4].textContent, 'unchanged 11');
- });
-
- test('clicking +x common lines expands those lines', () => {
- const contextControls = element.querySelectorAll('gr-context-controls');
- const topExpandCommonButton = contextControls[0].shadowRoot
- .querySelectorAll('.showContext')[0];
- assert.include(topExpandCommonButton.textContent, '+9 common lines');
- topExpandCommonButton.click();
- const diffRows = element.querySelectorAll('.diff-row');
- // The first two are LOST and FILE line
- assert.equal(diffRows.length, 2 + 10 + 1 + 1);
- assert.include(diffRows[2].textContent, 'unchanged 1');
- assert.include(diffRows[3].textContent, 'unchanged 2');
- assert.include(diffRows[4].textContent, 'unchanged 3');
- assert.include(diffRows[5].textContent, 'unchanged 4');
- assert.include(diffRows[6].textContent, 'unchanged 5');
- assert.include(diffRows[7].textContent, 'unchanged 6');
- assert.include(diffRows[8].textContent, 'unchanged 7');
- assert.include(diffRows[9].textContent, 'unchanged 8');
- assert.include(diffRows[10].textContent, 'unchanged 9');
- assert.include(diffRows[11].textContent, 'unchanged 10');
- assert.include(diffRows[12].textContent, 'before');
- assert.include(diffRows[12].textContent, 'after');
- assert.include(diffRows[13].textContent, 'unchanged 11');
- });
-
- test('unhideLine shows the line with context', async () => {
- element.dispatchEvent.reset();
- element.unhideLine(4, Side.LEFT);
-
- const diffRows = element.querySelectorAll('.diff-row');
- // The first two are LOST and FILE line
- // Lines 3-5 (Line 4 plus 1 context in each direction) will be expanded
- // Because context expanders do not hide <3 lines, lines 1-2 will also
- // be shown.
- // Lines 6-9 continue to be hidden
- assert.equal(diffRows.length, 2 + 5 + 1 + 1 + 1);
- assert.include(diffRows[2].textContent, 'unchanged 1');
- assert.include(diffRows[3].textContent, 'unchanged 2');
- assert.include(diffRows[4].textContent, 'unchanged 3');
- assert.include(diffRows[5].textContent, 'unchanged 4');
- assert.include(diffRows[6].textContent, 'unchanged 5');
- assert.include(diffRows[7].textContent, 'unchanged 10');
- assert.include(diffRows[8].textContent, 'before');
- assert.include(diffRows[8].textContent, 'after');
- assert.include(diffRows[9].textContent, 'unchanged 11');
-
- await new Promise(resolve => afterNextRender(element, resolve));
- const firedEventTypes = element.dispatchEvent.getCalls()
- .map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-content');
- });
- });
-
- suite('mock-diff', () => {
- let element;
- let builder;
- let diff;
- let keyLocations;
-
- setup(async () => {
- element = mockDiffFixture.instantiate();
- diff = createDiff();
- element.diff = diff;
-
- keyLocations = {left: {}, right: {}};
-
- element.prefs = {
- line_length: 80,
- show_tabs: true,
- tab_size: 4,
- };
- await element.render(keyLocations);
- builder = element._builder;
- });
-
- test('aria-labels on added line numbers', () => {
- const deltaLineNumberButton = element.diffElement.querySelectorAll(
- '.lineNumButton.right')[5];
-
- assert.isOk(deltaLineNumberButton);
- assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added');
- });
-
- test('aria-labels on removed line numbers', () => {
- const deltaLineNumberButton = element.diffElement.querySelectorAll(
- '.lineNumButton.left')[10];
-
- assert.isOk(deltaLineNumberButton);
- assert.equal(
- deltaLineNumberButton.getAttribute('aria-label'), '10 removed');
- });
-
- test('getContentByLine', () => {
- let actual;
-
- actual = builder.getContentByLine(2, 'left');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
-
- actual = builder.getContentByLine(2, 'right');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
-
- actual = builder.getContentByLine(5, 'left');
- assert.equal(actual.textContent, diff.content[2].ab[0]);
-
- actual = builder.getContentByLine(5, 'right');
- assert.equal(actual.textContent, diff.content[1].b[0]);
- });
-
- test('getContentTdByLineEl works both with button and td', () => {
- const diffRow = element.diffElement.querySelectorAll('tr.diff-row')[2];
-
- const lineNumTdLeft = diffRow.querySelector('td.lineNum.left');
- const lineNumButtonLeft = lineNumTdLeft.querySelector('button');
- const contentTdLeft = diffRow.querySelectorAll('.content')[0];
-
- const lineNumTdRight = diffRow.querySelector('td.lineNum.right');
- const lineNumButtonRight = lineNumTdRight.querySelector('button');
- const contentTdRight = diffRow.querySelectorAll('.content')[1];
-
- assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
- assert.equal(
- element.getContentTdByLineEl(lineNumButtonLeft), contentTdLeft);
- assert.equal(
- element.getContentTdByLineEl(lineNumTdRight), contentTdRight);
- assert.equal(
- element.getContentTdByLineEl(lineNumButtonRight), contentTdRight);
- });
-
- test('findLinesByRange', () => {
- const lines = [];
- const elems = [];
- const start = 6;
- const end = 10;
- const count = end - start + 1;
-
- builder.findLinesByRange(start, end, 'right', lines, elems);
-
- assert.equal(lines.length, count);
- assert.equal(elems.length, count);
-
- for (let i = 0; i < 5; i++) {
- assert.instanceOf(lines[i], GrDiffLine);
- assert.equal(lines[i].afterNumber, start + i);
- assert.instanceOf(elems[i], HTMLElement);
- assert.equal(lines[i].text, elems[i].textContent);
- }
- });
-
- test('renderContentByRange', () => {
- const spy = sinon.spy(builder, 'createTextEl');
- const start = 9;
- const end = 14;
- const count = end - start + 1;
-
- builder.renderContentByRange(start, end, 'left');
-
- assert.equal(spy.callCount, count);
- spy.getCalls().forEach((call, i) => {
- assert.equal(call.args[1].beforeNumber, start + i);
- });
- });
-
- test('renderContentByRange non-existent elements', () => {
- const spy = sinon.spy(builder, 'createTextEl');
-
- sinon.stub(builder, 'getLineNumberEl').returns(
- document.createElement('div')
- );
- sinon.stub(builder, 'findLinesByRange').callsFake(
- (s, e, d, lines, elements) => {
- // Add a line and a corresponding element.
- lines.push(new GrDiffLine(GrDiffLineType.BOTH));
- const tr = document.createElement('tr');
- const td = document.createElement('td');
- const el = document.createElement('div');
- tr.appendChild(td);
- td.appendChild(el);
- elements.push(el);
-
- // Add 2 lines without corresponding elements.
- lines.push(new GrDiffLine(GrDiffLineType.BOTH));
- lines.push(new GrDiffLine(GrDiffLineType.BOTH));
- });
-
- builder.renderContentByRange(1, 10, 'left');
- // Should be called only once because only one line had a corresponding
- // element.
- assert.equal(spy.callCount, 1);
- });
-
- test('getLineNumberEl side-by-side left', () => {
- const contentEl = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'left');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('left'));
- });
-
- test('getLineNumberEl side-by-side right', () => {
- const contentEl = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'right');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('right'));
- });
-
- test('getLineNumberEl unified left', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const contentEl = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'left');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('left'));
- });
-
- test('getLineNumberEl unified right', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const contentEl = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'right');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('right'));
- });
-
- test('getNextContentOnSide side-by-side left', () => {
- const startElem = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const expectedStartString = diff.content[2].ab[0];
- const expectedNextString = diff.content[2].ab[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'left');
- assert.equal(nextElem.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide side-by-side right', () => {
- const startElem = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const expectedStartString = diff.content[1].b[0];
- const expectedNextString = diff.content[1].b[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'right');
- assert.equal(nextElem.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide unified left', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const expectedStartString = diff.content[2].ab[0];
- const expectedNextString = diff.content[2].ab[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'left');
- assert.equal(nextElem.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide unified right', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const expectedStartString = diff.content[1].b[0];
- const expectedNextString = diff.content[1].b[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'right');
- assert.equal(nextElem.textContent, expectedNextString);
- });
- });
-
- suite('blame', () => {
- let mockBlame;
-
- setup(() => {
- mockBlame = [
- {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
- {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
- ];
- });
-
- test('setBlame attempts to render each blamed line', () => {
- const getBlameStub = sinon.stub(builder, 'getBlameTdByLine')
- .returns(null);
- builder.setBlame(mockBlame);
- assert.equal(getBlameStub.callCount, 32);
- });
-
- test('getBlameCommitForBaseLine', () => {
- sinon.stub(builder, 'getBlameTdByLine').returns(undefined);
- builder.setBlame(mockBlame);
- assert.isOk(builder.getBlameCommitForBaseLine(1));
- assert.equal(builder.getBlameCommitForBaseLine(1).id, 'commit 1');
-
- assert.isOk(builder.getBlameCommitForBaseLine(11));
- assert.equal(builder.getBlameCommitForBaseLine(11).id, 'commit 1');
-
- assert.isOk(builder.getBlameCommitForBaseLine(32));
- assert.equal(builder.getBlameCommitForBaseLine(32).id, 'commit 2');
-
- assert.isUndefined(builder.getBlameCommitForBaseLine(33));
- });
-
- test('getBlameCommitForBaseLine w/o blame returns null', () => {
- assert.isUndefined(builder.getBlameCommitForBaseLine(1));
- assert.isUndefined(builder.getBlameCommitForBaseLine(11));
- assert.isUndefined(builder.getBlameCommitForBaseLine(31));
- });
-
- test('createBlameCell', () => {
- const mockBlameInfo = {
- time: 1576155200,
- id: 1234567890,
- author: 'Clark Kent',
- commit_msg: 'Testing Commit',
- ranges: [1],
- };
- const getBlameStub = sinon.stub(builder, 'getBlameCommitForBaseLine')
- .returns(mockBlameInfo);
- const line = new GrDiffLine(GrDiffLineType.BOTH);
- line.beforeNumber = 3;
- line.afterNumber = 5;
-
- const result = builder.createBlameCell(line.beforeNumber);
-
- assert.isTrue(getBlameStub.calledWithExactly(3));
- assert.equal(result.getAttribute('data-line-number'), '3');
- expect(result).dom.to.equal(/* HTML */`
- <span class="gr-diff style-scope">
- <a class="blameDate gr-diff style-scope" href="/r/q/1234567890">
- 12/12/2019
- </a>
- <span class="blameAuthor gr-diff style-scope">Clark</span>
- <gr-hovercard class="gr-diff style-scope">
- <span class="blameHoverCard gr-diff style-scope">
- Commit 1234567890<br>
- Author: Clark Kent<br>
- Date: 12/12/2019<br>
- <br>
- Testing Commit
- </span>
- </gr-hovercard>
- </span>
- `);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
new file mode 100644
index 0000000..8ae08f4
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -0,0 +1,1131 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {
+ createConfig,
+ createDiff,
+ createEmptyDiff,
+} from '../../../test/test-data-generators';
+import './gr-diff-builder-element';
+import {
+ nextRender,
+ queryAndAssert,
+ stubBaseUrl,
+} from '../../../test/test-utils';
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
+import {
+ DiffContent,
+ DiffInfo,
+ DiffLayer,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ Side,
+} from '../../../api/diff';
+import {stubRestApi} from '../../../test/test-utils';
+import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiffBuilderElement} from './gr-diff-builder-element';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
+import {BlameInfo} from '../../../types/common';
+import {fixture, html} from '@open-wc/testing-helpers';
+
+const DEFAULT_PREFS = createDefaultDiffPrefs();
+
+suite('gr-diff-builder tests', () => {
+ let element: GrDiffBuilderElement;
+ let builder: GrDiffBuilderLegacy;
+ let diffTable: HTMLTableElement;
+
+ const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
+ const WBR_HTML = '<wbr class="style-scope gr-diff">';
+
+ const setBuilderPrefs = (prefs: Partial<DiffPreferencesInfo>) => {
+ builder = new GrDiffBuilderSideBySide(
+ createEmptyDiff(),
+ {...createDefaultDiffPrefs(), ...prefs},
+ diffTable
+ );
+ };
+
+ const line = (text: string) => {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.text = text;
+ return line;
+ };
+
+ setup(async () => {
+ diffTable = await fixture(html`<table id="diffTable"></table>`);
+ element = new GrDiffBuilderElement();
+ element.diffElement = diffTable;
+ stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+ stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
+ stubBaseUrl('/r');
+ setBuilderPrefs({});
+ });
+
+ test('line_length applied with <wbr> if line_wrapping is true', () => {
+ setBuilderPrefs({line_wrapping: true, tab_size: 4, line_length: 50});
+ const text = 'a'.repeat(51);
+ const expected = 'a'.repeat(50) + WBR_HTML + 'a';
+ const result = builder.createTextEl(null, line(text)).firstElementChild
+ ?.innerHTML;
+ assert.equal(result, expected);
+ });
+
+ test('line_length applied with line break if line_wrapping is false', () => {
+ setBuilderPrefs({line_wrapping: false, tab_size: 4, line_length: 50});
+ const text = 'a'.repeat(51);
+ const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
+ const result = builder.createTextEl(null, line(text)).firstElementChild
+ ?.innerHTML;
+ assert.equal(result, expected);
+ });
+
+ [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE].forEach(mode => {
+ test(`line_length used for regular files under ${mode}`, () => {
+ element.path = '/a.txt';
+ element.viewMode = mode;
+ element.diff = createEmptyDiff();
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ tab_size: 4,
+ line_length: 50,
+ };
+ builder = element.getDiffBuilder() as GrDiffBuilderLegacy;
+ assert.equal(builder._prefs.line_length, 50);
+ });
+
+ test(`line_length ignored for commit msg under ${mode}`, () => {
+ element.path = '/COMMIT_MSG';
+ element.viewMode = mode;
+ element.diff = createEmptyDiff();
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ tab_size: 4,
+ line_length: 50,
+ };
+ builder = element.getDiffBuilder() as GrDiffBuilderLegacy;
+ assert.equal(builder._prefs.line_length, 72);
+ });
+ });
+
+ test('createTextEl linewrap with tabs', () => {
+ setBuilderPrefs({tab_size: 4, line_length: 10});
+ const text = '\t'.repeat(7) + '!';
+ const el = builder.createTextEl(null, line(text));
+ assert.equal(el.innerText, text);
+ // With line length 10 and tab size 4, there should be a line break
+ // after every two tabs.
+ const newlineEl = el.querySelector('.contentText > .br');
+ assert.isOk(newlineEl);
+ assert.equal(
+ el.querySelector('.contentText .tab:nth-child(2)')?.nextSibling,
+ newlineEl
+ );
+ });
+
+ test('_handlePreferenceError throws with invalid preference', () => {
+ element.prefs = {...createDefaultDiffPrefs(), tab_size: 0};
+ assert.throws(() => element.getDiffBuilder());
+ });
+
+ test('_handlePreferenceError triggers alert and javascript error', () => {
+ const errorStub = sinon.stub();
+ diffTable.addEventListener('show-alert', errorStub);
+ assert.throws(() => element.handlePreferenceError('tab size'));
+ assert.equal(
+ errorStub.lastCall.args[0].detail.message,
+ "The value of the 'tab size' user preference is invalid. " +
+ 'Fix in diff preferences'
+ );
+ });
+
+ suite('intraline differences', () => {
+ let el: HTMLElement;
+ let str: string;
+ let annotateElementSpy: sinon.SinonSpy;
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ function slice(str: string, start: number, end?: number) {
+ return Array.from(str).slice(start, end).join('');
+ }
+
+ setup(async () => {
+ el = await fixture(html`
+ <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+ `);
+ str = el.textContent ?? '';
+ annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+ layer = element.createIntralineLayer();
+ });
+
+ test('annotate no highlights', () => {
+ layer.annotate(el, lineNumberEl, line(str), Side.LEFT);
+
+ // The content is unchanged.
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(str, el.childNodes[0].textContent);
+ });
+
+ test('annotate with highlights', () => {
+ const l = line(str);
+ l.highlights = [
+ {contentIndex: 0, startIndex: 6, endIndex: 12},
+ {contentIndex: 0, startIndex: 18, endIndex: 22},
+ ];
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12, 18);
+ const str3 = slice(str, 18, 22);
+ const str4 = slice(str, 22);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 5);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+
+ assert.notInstanceOf(el.childNodes[3], Text);
+ assert.equal(el.childNodes[3].textContent, str3);
+
+ assert.instanceOf(el.childNodes[4], Text);
+ assert.equal(el.childNodes[4].textContent, str4);
+ });
+
+ test('annotate without endIndex', () => {
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 28}];
+
+ const str0 = slice(str, 0, 28);
+ const str1 = slice(str, 28);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+
+ test('annotate ignores empty highlights', () => {
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 28, endIndex: 28}];
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ });
+
+ test('annotate handles unicode', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 6, endIndex: 12}];
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 3);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+ });
+
+ test('annotate handles unicode w/o endIndex', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 6}];
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+ });
+
+ suite('tab indicators', () => {
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element.showTabs = true;
+ layer = element.createTabIndicatorLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const l = line('');
+ const el = document.createElement('div');
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no tabs', () => {
+ const str = 'lorem ipsum no tabs';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates tab at beginning', () => {
+ const str = '\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('does not annotate when disabled', () => {
+ element.showTabs = false;
+
+ const str = '\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates multiple in beginning', () => {
+ const str = '\t\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.equal(annotateElementStub.callCount, 2);
+
+ let args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+
+ args = annotateElementStub.getCalls()[1].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 1, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('annotates intermediate tabs', () => {
+ const str = 'lorem\tupsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 5, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+ });
+
+ suite('layers', () => {
+ let initialLayersCount = 0;
+ let withLayerCount = 0;
+ setup(() => {
+ const layers: DiffLayer[] = [];
+ element.layers = layers;
+ element.showTrailingWhitespace = true;
+ element.setupAnnotationLayers();
+ initialLayersCount = element.layersInternal.length;
+ });
+
+ test('no layers', () => {
+ element.setupAnnotationLayers();
+ assert.equal(element.layersInternal.length, initialLayersCount);
+ });
+
+ suite('with layers', () => {
+ const layers: DiffLayer[] = [{annotate: () => {}}, {annotate: () => {}}];
+ setup(() => {
+ element.layers = layers;
+ element.showTrailingWhitespace = true;
+ element.setupAnnotationLayers();
+ withLayerCount = element.layersInternal.length;
+ });
+ test('with layers', () => {
+ element.setupAnnotationLayers();
+ assert.equal(element.layersInternal.length, withLayerCount);
+ assert.equal(initialLayersCount + layers.length, withLayerCount);
+ });
+ });
+ });
+
+ suite('trailing whitespace', () => {
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element.showTrailingWhitespace = true;
+ layer = element.createTrailingWhitespaceLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const l = line('');
+ const el = document.createElement('div');
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no trailing whitespace', () => {
+ const str = 'lorem ipsum blah blah';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates trailing spaces', () => {
+ const str = 'lorem ipsum ';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates trailing tabs', () => {
+ const str = 'lorem ipsum\t\t\t';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates mixed trailing whitespace', () => {
+ const str = 'lorem ipsum\t \t';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('unicode preceding trailing whitespace', () => {
+ const str = '💢\t';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 1);
+ assert.equal(annotateElementStub.lastCall.args[2], 1);
+ });
+
+ test('does not annotate when disabled', () => {
+ element.showTrailingWhitespace = false;
+ const str = 'lorem upsum\t \t ';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+ });
+
+ suite('rendering text, images and binary files', () => {
+ let processStub: sinon.SinonStub;
+ let keyLocations: KeyLocations;
+ let content: DiffContent[] = [];
+
+ setup(() => {
+ element.viewMode = 'SIDE_BY_SIDE';
+ processStub = sinon
+ .stub(element.processor, 'process')
+ .returns(Promise.resolve());
+ keyLocations = {left: {}, right: {}};
+ element.prefs = {
+ ...DEFAULT_PREFS,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
+ });
+
+ test('text', async () => {
+ element.diff = {...createEmptyDiff(), content};
+ element.render(keyLocations);
+ await waitForEventOnce(diffTable, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isFalse(processStub.lastCall.args[1]);
+ });
+
+ test('image', async () => {
+ element.diff = {...createEmptyDiff(), content, binary: true};
+ element.isImageDiff = true;
+ element.render(keyLocations);
+ await waitForEventOnce(diffTable, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+
+ test('binary', async () => {
+ element.diff = {...createEmptyDiff(), content, binary: true};
+ element.render(keyLocations);
+ await waitForEventOnce(diffTable, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+ });
+
+ suite('rendering', () => {
+ let content: DiffContent[];
+ let outputEl: HTMLTableElement;
+ let keyLocations: KeyLocations;
+ let addColumnsStub: sinon.SinonStub;
+ let dispatchStub: sinon.SinonStub;
+ let builder: GrDiffBuilderSideBySide;
+
+ setup(() => {
+ const prefs = {...DEFAULT_PREFS};
+ content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
+ dispatchStub = sinon.stub(diffTable, 'dispatchEvent');
+ outputEl = element.diffElement!;
+ keyLocations = {left: {}, right: {}};
+ sinon.stub(element, 'getDiffBuilder').callsFake(() => {
+ builder = new GrDiffBuilderSideBySide(
+ {...createEmptyDiff(), content},
+ prefs,
+ outputEl
+ );
+ addColumnsStub = sinon.stub(builder, 'addColumns');
+ builder.buildSectionElement = function (group) {
+ const section = document.createElement('stub');
+ section.textContent = group.lines.reduce(
+ (acc, line) => acc + line.text,
+ ''
+ );
+ return section;
+ };
+ return builder;
+ });
+ element.diff = {...createEmptyDiff(), content};
+ element.prefs = prefs;
+ element.render(keyLocations);
+ });
+
+ test('addColumns is called', () => {
+ assert.isTrue(addColumnsStub.called);
+ });
+
+ test('getGroupsByLineRange one line', () => {
+ const section = outputEl.querySelector<HTMLElement>(
+ 'stub:nth-of-type(3)'
+ );
+ const groups = builder.getGroupsByLineRange(1, 1, Side.LEFT);
+ assert.equal(groups.length, 1);
+ assert.strictEqual(groups[0].element, section);
+ });
+
+ test('getGroupsByLineRange over diff', () => {
+ const section = [
+ outputEl.querySelector<HTMLElement>('stub:nth-of-type(3)'),
+ outputEl.querySelector<HTMLElement>('stub:nth-of-type(4)'),
+ ];
+ const groups = builder.getGroupsByLineRange(1, 2, Side.LEFT);
+ assert.equal(groups.length, 2);
+ assert.strictEqual(groups[0].element, section[0]);
+ assert.strictEqual(groups[1].element, section[1]);
+ });
+
+ test('render-start and render-content are fired', async () => {
+ await nextRender();
+ let firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-start');
+ assert.include(firedEventTypes, 'render-progress');
+
+ await nextRender();
+ firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-content');
+ });
+
+ test('cancel cancels the processor', () => {
+ const processorCancelStub = sinon.stub(element.processor, 'cancel');
+ element.cancel();
+ assert.isTrue(processorCancelStub.called);
+ });
+ });
+
+ suite('context hiding and expanding', () => {
+ let dispatchStub: sinon.SinonStub;
+
+ setup(async () => {
+ dispatchStub = sinon.stub(diffTable, 'dispatchEvent');
+ element.diff = {
+ ...createEmptyDiff(),
+ content: [
+ {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${i}`)},
+ {a: ['before'], b: ['after']},
+ {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${10 + i}`)},
+ ],
+ };
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
+
+ const keyLocations: KeyLocations = {left: {}, right: {}};
+ element.prefs = {
+ ...DEFAULT_PREFS,
+ context: 1,
+ };
+ element.render(keyLocations);
+ // Make sure all listeners are installed.
+ await nextRender();
+ });
+
+ test('hides lines behind two context controls', () => {
+ const contextControls = diffTable.querySelectorAll('gr-context-controls');
+ assert.equal(contextControls.length, 2);
+
+ const diffRows = diffTable.querySelectorAll('.diff-row');
+ // The first two are LOST and FILE line
+ assert.equal(diffRows.length, 2 + 1 + 1 + 1);
+ assert.include(diffRows[2].textContent, 'unchanged 10');
+ assert.include(diffRows[3].textContent, 'before');
+ assert.include(diffRows[3].textContent, 'after');
+ assert.include(diffRows[4].textContent, 'unchanged 11');
+ });
+
+ test('clicking +x common lines expands those lines', () => {
+ const contextControls = diffTable.querySelectorAll('gr-context-controls');
+ const topExpandCommonButton =
+ contextControls[0].shadowRoot?.querySelectorAll<HTMLElement>(
+ '.showContext'
+ )[0];
+ assert.isOk(topExpandCommonButton);
+ assert.include(topExpandCommonButton!.textContent, '+9 common lines');
+ topExpandCommonButton!.click();
+ const diffRows = diffTable.querySelectorAll('.diff-row');
+ // The first two are LOST and FILE line
+ assert.equal(diffRows.length, 2 + 10 + 1 + 1);
+ assert.include(diffRows[2].textContent, 'unchanged 1');
+ assert.include(diffRows[3].textContent, 'unchanged 2');
+ assert.include(diffRows[4].textContent, 'unchanged 3');
+ assert.include(diffRows[5].textContent, 'unchanged 4');
+ assert.include(diffRows[6].textContent, 'unchanged 5');
+ assert.include(diffRows[7].textContent, 'unchanged 6');
+ assert.include(diffRows[8].textContent, 'unchanged 7');
+ assert.include(diffRows[9].textContent, 'unchanged 8');
+ assert.include(diffRows[10].textContent, 'unchanged 9');
+ assert.include(diffRows[11].textContent, 'unchanged 10');
+ assert.include(diffRows[12].textContent, 'before');
+ assert.include(diffRows[12].textContent, 'after');
+ assert.include(diffRows[13].textContent, 'unchanged 11');
+ });
+
+ test('unhideLine shows the line with context', async () => {
+ dispatchStub.reset();
+ element.unhideLine(4, Side.LEFT);
+
+ const diffRows = diffTable.querySelectorAll('.diff-row');
+ // The first two are LOST and FILE line
+ // Lines 3-5 (Line 4 plus 1 context in each direction) will be expanded
+ // Because context expanders do not hide <3 lines, lines 1-2 will also
+ // be shown.
+ // Lines 6-9 continue to be hidden
+ assert.equal(diffRows.length, 2 + 5 + 1 + 1 + 1);
+ assert.include(diffRows[2].textContent, 'unchanged 1');
+ assert.include(diffRows[3].textContent, 'unchanged 2');
+ assert.include(diffRows[4].textContent, 'unchanged 3');
+ assert.include(diffRows[5].textContent, 'unchanged 4');
+ assert.include(diffRows[6].textContent, 'unchanged 5');
+ assert.include(diffRows[7].textContent, 'unchanged 10');
+ assert.include(diffRows[8].textContent, 'before');
+ assert.include(diffRows[8].textContent, 'after');
+ assert.include(diffRows[9].textContent, 'unchanged 11');
+
+ await nextRender();
+ const firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-content');
+ });
+ });
+
+ suite('mock-diff', () => {
+ let builder: GrDiffBuilderSideBySide;
+ let diff: DiffInfo;
+ let keyLocations: KeyLocations;
+
+ setup(() => {
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
+ diff = createDiff();
+ element.diff = diff;
+
+ keyLocations = {left: {}, right: {}};
+
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 80,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+ });
+
+ test('aria-labels on added line numbers', () => {
+ const deltaLineNumberButton = diffTable.querySelectorAll(
+ '.lineNumButton.right'
+ )[5];
+
+ assert.isOk(deltaLineNumberButton);
+ assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added');
+ });
+
+ test('aria-labels on removed line numbers', () => {
+ const deltaLineNumberButton = diffTable.querySelectorAll(
+ '.lineNumButton.left'
+ )[10];
+
+ assert.isOk(deltaLineNumberButton);
+ assert.equal(
+ deltaLineNumberButton.getAttribute('aria-label'),
+ '10 removed'
+ );
+ });
+
+ test('getContentByLine', () => {
+ let actual: HTMLElement | null;
+
+ actual = builder.getContentByLine(2, Side.LEFT);
+ assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+ actual = builder.getContentByLine(2, Side.RIGHT);
+ assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+ actual = builder.getContentByLine(5, Side.LEFT);
+ assert.equal(actual?.textContent, diff.content[2].ab?.[0]);
+
+ actual = builder.getContentByLine(5, Side.RIGHT);
+ assert.equal(actual?.textContent, diff.content[1].b?.[0]);
+ });
+
+ test('getContentTdByLineEl works both with button and td', () => {
+ const diffRow = diffTable.querySelectorAll('tr.diff-row')[2];
+
+ const lineNumTdLeft = queryAndAssert(diffRow, 'td.lineNum.left');
+ const lineNumButtonLeft = queryAndAssert(lineNumTdLeft, 'button');
+ const contentTdLeft = diffRow.querySelectorAll('.content')[0];
+
+ const lineNumTdRight = queryAndAssert(diffRow, 'td.lineNum.right');
+ const lineNumButtonRight = queryAndAssert(lineNumTdRight, 'button');
+ const contentTdRight = diffRow.querySelectorAll('.content')[1];
+
+ assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
+ assert.equal(
+ element.getContentTdByLineEl(lineNumButtonLeft),
+ contentTdLeft
+ );
+ assert.equal(
+ element.getContentTdByLineEl(lineNumTdRight),
+ contentTdRight
+ );
+ assert.equal(
+ element.getContentTdByLineEl(lineNumButtonRight),
+ contentTdRight
+ );
+ });
+
+ test('findLinesByRange', () => {
+ const lines: GrDiffLine[] = [];
+ const elems: HTMLElement[] = [];
+ const start = 6;
+ const end = 10;
+ const count = end - start + 1;
+
+ builder.findLinesByRange(start, end, Side.RIGHT, lines, elems);
+
+ assert.equal(lines.length, count);
+ assert.equal(elems.length, count);
+
+ for (let i = 0; i < 5; i++) {
+ assert.instanceOf(lines[i], GrDiffLine);
+ assert.equal(lines[i].afterNumber, start + i);
+ assert.instanceOf(elems[i], HTMLElement);
+ assert.equal(lines[i].text, elems[i].textContent);
+ }
+ });
+
+ test('renderContentByRange', () => {
+ const spy = sinon.spy(builder, 'createTextEl');
+ const start = 9;
+ const end = 14;
+ const count = end - start + 1;
+
+ builder.renderContentByRange(start, end, Side.LEFT);
+
+ assert.equal(spy.callCount, count);
+ spy.getCalls().forEach((call, i: number) => {
+ assert.equal(call.args[1].beforeNumber, start + i);
+ });
+ });
+
+ test('renderContentByRange non-existent elements', () => {
+ const spy = sinon.spy(builder, 'createTextEl');
+
+ sinon
+ .stub(builder, 'getLineNumberEl')
+ .returns(document.createElement('div'));
+ sinon
+ .stub(builder, 'findLinesByRange')
+ .callsFake((_1, _2, _3, lines, elements) => {
+ // Add a line and a corresponding element.
+ lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+ const tr = document.createElement('tr');
+ const td = document.createElement('td');
+ const el = document.createElement('div');
+ tr.appendChild(td);
+ td.appendChild(el);
+ elements?.push(el);
+
+ // Add 2 lines without corresponding elements.
+ lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+ lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+ });
+
+ builder.renderContentByRange(1, 10, Side.LEFT);
+ // Should be called only once because only one line had a corresponding
+ // element.
+ assert.equal(spy.callCount, 1);
+ });
+
+ test('getLineNumberEl side-by-side left', () => {
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+ });
+
+ test('getLineNumberEl side-by-side right', () => {
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+ });
+
+ test('getLineNumberEl unified left', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+ });
+
+ test('getLineNumberEl unified right', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+ });
+
+ test('getNextContentOnSide side-by-side left', () => {
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(startElem);
+ const expectedStartString = diff.content[2].ab?.[0];
+ const expectedNextString = diff.content[2].ab?.[1];
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide side-by-side right', () => {
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ const expectedStartString = diff.content[1].b?.[0];
+ const expectedNextString = diff.content[1].b?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide unified left', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ const expectedStartString = diff.content[2].ab?.[0];
+ const expectedNextString = diff.content[2].ab?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide unified right', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ const expectedStartString = diff.content[1].b?.[0];
+ const expectedNextString = diff.content[1].b?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+ });
+
+ suite('blame', () => {
+ let mockBlame: BlameInfo[];
+
+ setup(() => {
+ mockBlame = [
+ {
+ author: 'test-author',
+ time: 314,
+ commit_msg: 'test-commit-message',
+ id: 'commit 1',
+ ranges: [
+ {start: 1, end: 2},
+ {start: 10, end: 16},
+ ],
+ },
+ {
+ author: 'test-author',
+ time: 314,
+ commit_msg: 'test-commit-message',
+ id: 'commit 2',
+ ranges: [
+ {start: 4, end: 10},
+ {start: 17, end: 32},
+ ],
+ },
+ ];
+ });
+
+ test('setBlame attempts to render each blamed line', () => {
+ const getBlameStub = sinon
+ .stub(builder, 'getBlameTdByLine')
+ .returns(undefined);
+ builder.setBlame(mockBlame);
+ assert.equal(getBlameStub.callCount, 32);
+ });
+
+ test('getBlameCommitForBaseLine', () => {
+ sinon.stub(builder, 'getBlameTdByLine').returns(undefined);
+ builder.setBlame(mockBlame);
+ assert.isOk(builder.getBlameCommitForBaseLine(1));
+ assert.equal(builder.getBlameCommitForBaseLine(1)?.id, 'commit 1');
+
+ assert.isOk(builder.getBlameCommitForBaseLine(11));
+ assert.equal(builder.getBlameCommitForBaseLine(11)?.id, 'commit 1');
+
+ assert.isOk(builder.getBlameCommitForBaseLine(32));
+ assert.equal(builder.getBlameCommitForBaseLine(32)?.id, 'commit 2');
+
+ assert.isUndefined(builder.getBlameCommitForBaseLine(33));
+ });
+
+ test('getBlameCommitForBaseLine w/o blame returns null', () => {
+ assert.isUndefined(builder.getBlameCommitForBaseLine(1));
+ assert.isUndefined(builder.getBlameCommitForBaseLine(11));
+ assert.isUndefined(builder.getBlameCommitForBaseLine(31));
+ });
+
+ test('createBlameCell', () => {
+ const mockBlameInfo = {
+ time: 1576155200,
+ id: '1234567890',
+ author: 'Clark Kent',
+ commit_msg: 'Testing Commit',
+ ranges: [{start: 4, end: 10}],
+ };
+ const getBlameStub = sinon
+ .stub(builder, 'getBlameCommitForBaseLine')
+ .returns(mockBlameInfo);
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.beforeNumber = 3;
+ line.afterNumber = 5;
+
+ const result = builder.createBlameCell(line.beforeNumber);
+
+ assert.isTrue(getBlameStub.calledWithExactly(3));
+ assert.equal(result.getAttribute('data-line-number'), '3');
+ expect(result).dom.to.equal(/* HTML */ `
+ <span class="gr-diff style-scope">
+ <a class="blameDate gr-diff style-scope" href="/r/q/1234567890">
+ 12/12/2019
+ </a>
+ <span class="blameAuthor gr-diff style-scope">Clark</span>
+ <gr-hovercard class="gr-diff style-scope">
+ <span class="blameHoverCard gr-diff style-scope">
+ Commit 1234567890<br />
+ Author: Clark Kent<br />
+ Date: 12/12/2019<br />
+ <br />
+ Testing Commit
+ </span>
+ </gr-hovercard>
+ </span>
+ `);
+ });
+ });
+});
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 ceadc94..c04d156 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
@@ -162,10 +162,8 @@
*
* TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
*/
- private getLineNumberEl(
- content: HTMLElement,
- side: Side
- ): HTMLElement | null {
+ // visible for testing
+ getLineNumberEl(content: HTMLElement, side: Side): HTMLElement | null {
let row: HTMLElement | null = content;
while (row && !row.classList.contains('diff-row')) row = row.parentElement;
return row ? (row.querySelector('.lineNum.' + side) as HTMLElement) : null;
@@ -349,7 +347,8 @@
});
}
- protected createTextEl(
+ // visible for testing
+ createTextEl(
lineNumberEl: HTMLElement | null,
line: GrDiffLine,
side?: Side
@@ -491,7 +490,8 @@
* Create a blame cell for the given base line. Blame information will be
* included in the cell if available.
*/
- protected createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
+ // visible for testing
+ createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
const blameTd = createElementDiff('td', 'blame') as HTMLTableCellElement;
blameTd.setAttribute('data-line-number', lineNumber.toString());
if (!lineNumber) return blameTd;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index a711215..f2690bc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -44,7 +44,8 @@
};
}
- protected override buildSectionElement(group: GrDiffGroup) {
+ // visible for testing
+ override buildSectionElement(group: GrDiffGroup) {
const sectionEl = createElementDiff('tbody', 'section');
sectionEl.classList.add(group.type);
if (group.isTotal()) {
@@ -147,7 +148,8 @@
return td;
}
- protected override getNextContentOnSide(
+ // visible for testing
+ override getNextContentOnSide(
content: HTMLElement,
side: Side
): HTMLElement | null {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
index 4145485..0c9d1d9 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -43,7 +43,8 @@
};
}
- protected override buildSectionElement(group: GrDiffGroup): HTMLElement {
+ // visible for testing
+ override buildSectionElement(group: GrDiffGroup): HTMLElement {
const sectionEl = createElementDiff('tbody', 'section');
sectionEl.classList.add(group.type);
if (group.isTotal()) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js
deleted file mode 100644
index 5f3fb72..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js
+++ /dev/null
@@ -1,242 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../gr-diff/gr-diff-group.js';
-import './gr-diff-builder.js';
-import './gr-diff-builder-unified.js';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group.js';
-import {GrDiffBuilderUnified} from './gr-diff-builder-unified.js';
-
-suite('GrDiffBuilderUnified tests', () => {
- let prefs;
- let outputEl;
- let diffBuilder;
-
- setup(()=> {
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- outputEl = document.createElement('div');
- diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
- });
-
- suite('buildSectionElement for BOTH group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.BOTH, 1, 2),
- new GrDiffLine(GrDiffLineType.BOTH, 2, 3),
- new GrDiffLine(GrDiffLineType.BOTH, 3, 4),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World";';
- lines[2].text = ' return True';
-
- group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('both'));
- });
-
- test('creates each unchanged row once', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 3);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[0].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[1].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.equal(
- rowEls[2].querySelector('.lineNum.left').textContent,
- lines[2].beforeNumber);
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
- });
- });
-
- suite('buildSectionElement for moved chunks', () => {
- test('creates a moved out group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 15),
- new GrDiffLine(GrDiffLineType.REMOVE, 16),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- group.moveDetails = {changed: false};
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedOut'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved out');
- });
-
- test('creates a moved in group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.ADD, 37),
- new GrDiffLine(GrDiffLineType.ADD, 38),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- group.moveDetails = {changed: false};
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedIn'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved in');
- });
- });
-
- suite('buildSectionElement for DELTA group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 1),
- new GrDiffLine(GrDiffLineType.REMOVE, 2),
- new GrDiffLine(GrDiffLineType.ADD, 2),
- new GrDiffLine(GrDiffLineType.ADD, 3),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- lines[2].text = 'def hello_universe()';
- lines[3].text = ' print "Hello Universe"';
-
- group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('delta'));
- });
-
- test('creates the section with class if ignoredWhitespaceOnly', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
- });
-
- test('creates the section with class if dueToRebase', () => {
- group.dueToRebase = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('dueToRebase'));
- });
-
- test('creates first the removed and then the added rows', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 4);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[3].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[3].querySelector('.content').textContent, lines[3].text);
- });
-
- test('creates only the added rows if only ignored whitespace', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 2);
-
- assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[3].text);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
new file mode 100644
index 0000000..7a9d06d
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
@@ -0,0 +1,282 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff-group';
+import './gr-diff-builder';
+import './gr-diff-builder-unified';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
+import {DiffPreferencesInfo} from '../../../api/diff';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {createDiff} from '../../../test/test-data-generators';
+import {queryAndAssert} from '../../../utils/common-util';
+
+suite('GrDiffBuilderUnified tests', () => {
+ let prefs: DiffPreferencesInfo;
+ let outputEl: HTMLElement;
+ let diffBuilder: GrDiffBuilderUnified;
+
+ setup(() => {
+ prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ outputEl = document.createElement('div');
+ diffBuilder = new GrDiffBuilderUnified(createDiff(), prefs, outputEl, []);
+ });
+
+ suite('buildSectionElement for BOTH group', () => {
+ let lines: GrDiffLine[];
+ let group: GrDiffGroup;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 2),
+ new GrDiffLine(GrDiffLineType.BOTH, 2, 3),
+ new GrDiffLine(GrDiffLineType.BOTH, 3, 4),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World";';
+ lines[2].text = ' return True';
+
+ group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
+ });
+
+ test('creates the section', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('both'));
+ });
+
+ test('creates each unchanged row once', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 3);
+
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.left').textContent,
+ lines[0].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.right').textContent,
+ lines[0].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[0].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.left').textContent,
+ lines[1].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.right').textContent,
+ lines[1].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[1].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.left').textContent,
+ lines[2].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.content').textContent,
+ lines[2].text
+ );
+ });
+ });
+
+ suite('buildSectionElement for moved chunks', () => {
+ test('creates a moved out group', () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.REMOVE, 15),
+ new GrDiffLine(GrDiffLineType.REMOVE, 16),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ const group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ moveDetails: {changed: false},
+ });
+
+ const sectionEl = diffBuilder.buildSectionElement(group);
+
+ const rowEls = sectionEl.querySelectorAll('tr');
+ const moveControlsRow = rowEls[0];
+ const cells = moveControlsRow.querySelectorAll('td');
+ assert.isTrue(sectionEl.classList.contains('dueToMove'));
+ assert.equal(rowEls.length, 3);
+ assert.isTrue(moveControlsRow.classList.contains('movedOut'));
+ assert.equal(cells.length, 3);
+ assert.isTrue(cells[2].classList.contains('moveHeader'));
+ assert.equal(cells[2].textContent, 'Moved out');
+ });
+
+ test('creates a moved in group', () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.ADD, 37),
+ new GrDiffLine(GrDiffLineType.ADD, 38),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ const group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ moveDetails: {changed: false},
+ });
+
+ const sectionEl = diffBuilder.buildSectionElement(group);
+
+ const rowEls = sectionEl.querySelectorAll('tr');
+ const moveControlsRow = rowEls[0];
+ const cells = moveControlsRow.querySelectorAll('td');
+ assert.isTrue(sectionEl.classList.contains('dueToMove'));
+ assert.equal(rowEls.length, 3);
+ assert.isTrue(moveControlsRow.classList.contains('movedIn'));
+ assert.equal(cells.length, 3);
+ assert.isTrue(cells[2].classList.contains('moveHeader'));
+ assert.equal(cells[2].textContent, 'Moved in');
+ });
+ });
+
+ suite('buildSectionElement for DELTA group', () => {
+ let lines: GrDiffLine[];
+ let group: GrDiffGroup;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLineType.REMOVE, 1),
+ new GrDiffLine(GrDiffLineType.REMOVE, 2),
+ new GrDiffLine(GrDiffLineType.ADD, 2),
+ new GrDiffLine(GrDiffLineType.ADD, 3),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ lines[2].text = 'def hello_universe()';
+ lines[3].text = ' print "Hello Universe"';
+ });
+
+ test('creates the section', () => {
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('delta'));
+ });
+
+ test('creates the section with class if ignoredWhitespaceOnly', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ ignoredWhitespaceOnly: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
+ });
+
+ test('creates the section with class if dueToRebase', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ dueToRebase: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('dueToRebase'));
+ });
+
+ test('creates first the removed and then the added rows', () => {
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 4);
+
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.left').textContent,
+ lines[0].beforeNumber.toString()
+ );
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[0].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.left').textContent,
+ lines[1].beforeNumber.toString()
+ );
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[1].text
+ );
+
+ assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.content').textContent,
+ lines[2].text
+ );
+
+ assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[3], '.lineNum.right').textContent,
+ lines[3].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[3], '.content').textContent,
+ lines[3].text
+ );
+ });
+
+ test('creates only the added rows if only ignored whitespace', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ ignoredWhitespaceOnly: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 2);
+
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[2].text
+ );
+
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.right').textContent,
+ lines[3].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[3].text
+ );
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
index add7ffa..4b664e2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
@@ -89,7 +89,8 @@
protected readonly numLinesLeft: number;
- protected readonly _prefs: DiffPreferencesInfo;
+ // visible for testing
+ readonly _prefs: DiffPreferencesInfo;
protected readonly renderPrefs?: RenderPreferences;
@@ -194,7 +195,8 @@
group.element = element;
}
- private getGroupsByLineRange(
+ // visible for testing
+ getGroupsByLineRange(
startLine: LineNumber,
endLine: LineNumber,
side: Side
@@ -257,7 +259,8 @@
* TODO: Change `null` to `undefined` in paramete type. Also: Do we
* really need to support null/undefined? Also change to camelCase.
*/
- protected findLinesByRange(
+ // visible for testing
+ findLinesByRange(
start: LineNumber,
end: LineNumber,
side: Side,
@@ -352,9 +355,8 @@
*
* @return The commit information.
*/
- protected getBlameCommitForBaseLine(
- lineNum: LineNumber
- ): BlameInfo | undefined {
+ // visible for testing
+ getBlameCommitForBaseLine(lineNum: LineNumber): BlameInfo | undefined {
for (const blameCommit of this.blameInfo) {
for (const range of blameCommit.ranges) {
if (range.start <= lineNum && range.end >= lineNum) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
index dfe8a15..e80d86b 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -109,7 +109,8 @@
*/
initialLineNumber: number | null = null;
- private cursorManager = new GrCursorManager();
+ // visible for testing
+ cursorManager = new GrCursorManager();
private targetSubscription?: Subscription;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
deleted file mode 100644
index f48d673..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ /dev/null
@@ -1,681 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../gr-diff/gr-diff.js';
-import './gr-diff-cursor.js';
-import {fixture, html} from '@open-wc/testing-helpers';
-import {listenOnce, mockPromise} from '../../../test/test-utils.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import {createDefaultDiffPrefs} from '../../../constants/constants.js';
-import {GrDiffCursor} from './gr-diff-cursor.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-
-suite('gr-diff-cursor tests', () => {
- let cursor;
- let diffElement;
- let diff;
-
- setup(async () => {
- diffElement = await fixture(html`<gr-diff></gr-diff>`);
- cursor = new GrDiffCursor();
-
- // Register the diff with the cursor.
- cursor.replaceDiffs([diffElement]);
-
- diffElement.loggedIn = false;
- diffElement.comments = {
- left: [],
- right: [],
- meta: {},
- };
- diffElement.path = 'some/path.ts';
- const promise = mockPromise();
- const setupDone = () => {
- cursor._updateStops();
- cursor.moveToFirstChunk();
- diffElement.removeEventListener('render', setupDone);
- promise.resolve();
- };
- diffElement.addEventListener('render', setupDone);
-
- diff = createDiff();
- diffElement.prefs = createDefaultDiffPrefs();
- diffElement.diff = diff;
- await promise;
- });
-
- test('diff cursor functionality (side-by-side)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursor.diffRow);
-
- const firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- cursor.moveDown();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
-
- cursor.moveUp();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursor.diffRow, firstDeltaRow);
- });
-
- test('moveToFirstChunk', async () => {
- const diff = {
- meta_a: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- meta_b: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
- 'index b2adcf4..554ae49 100644',
- '--- a/lorem-ipsum.txt',
- '+++ b/lorem-ipsum.txt',
- ],
- content: [
- {b: ['new line 1']},
- {ab: ['unchanged line']},
- {a: ['old line 2']},
- {ab: ['more unchanged lines']},
- ],
- };
-
- diffElement.diff = diff;
- // The file comment button, if present, is a cursor stop. Ensure
- // moveToFirstChunk() works correctly even if the button is not shown.
- diffElement.prefs.show_file_comment_button = false;
- await flush();
- cursor._updateStops();
-
- const chunks = Array.from(diffElement.root.querySelectorAll(
- '.section.delta'));
- assert.equal(chunks.length, 2);
-
- // Verify it works on fresh diff.
- cursor.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'right');
-
- // Verify it works from other cursor positions.
- cursor.moveToNextChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'left');
- cursor.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'right');
- });
-
- test('moveToLastChunk', async () => {
- const diff = {
- meta_a: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- meta_b: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
- 'index b2adcf4..554ae49 100644',
- '--- a/lorem-ipsum.txt',
- '+++ b/lorem-ipsum.txt',
- ],
- content: [
- {ab: ['unchanged line']},
- {a: ['old line 2']},
- {ab: ['more unchanged lines']},
- {b: ['new line 3']},
- ],
- };
-
- diffElement.diff = diff;
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor._updateStops();
-
- const chunks = Array.from(diffElement.root.querySelectorAll(
- '.section.delta'));
- assert.equal(chunks.length, 2);
-
- // Verify it works on fresh diff.
- cursor.moveToLastChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'right');
-
- // Verify it works from other cursor positions.
- cursor.moveToPreviousChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'left');
- cursor.moveToLastChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'right');
- });
-
- test('cursor scroll behavior', () => {
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
-
- diffElement.dispatchEvent(new Event('render-start'));
- assert.isTrue(cursor.cursorManager.focusOnMove);
-
- window.dispatchEvent(new Event('scroll'));
- assert.equal(cursor.cursorManager.scrollMode, 'never');
- assert.isFalse(cursor.cursorManager.focusOnMove);
-
- diffElement.dispatchEvent(new Event('render-content'));
- assert.isTrue(cursor.cursorManager.focusOnMove);
-
- cursor.reInitCursor();
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('moves to selected line', () => {
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
-
- diffElement.dispatchEvent(
- new CustomEvent('line-selected', {
- detail: {number: '123', side: 'right', path: 'some/file'},
- }));
-
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], '123');
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(moveToNumStub.lastCall.args[2], 'some/file');
- });
-
- suite('unified diff', () => {
- setup(async () => {
- diffElement.viewMode = 'UNIFIED_DIFF';
- // We must allow the diff to re-render after setting the viewMode.
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- });
-
- test('diff cursor functionality (unified)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursor.diffRow);
-
- let firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- cursor.moveDown();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
-
- cursor.moveUp();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursor.diffRow, firstDeltaRow);
- });
- });
-
- test('cursor side functionality', () => {
- // The side only applies to side-by-side mode, which should be the default
- // mode.
- assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
-
- const firstDeltaSection = diffElement.shadowRoot
- .querySelector('.section.delta');
- const firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
-
- // Because the first delta in this diff is on the right, it should be set
- // to the right side.
- assert.equal(cursor.side, 'right');
- assert.equal(cursor.diffRow, firstDeltaRow);
- const firstIndex = cursor.cursorManager.index;
-
- // Move the side to the left. Because this delta only has a right side, we
- // should be moved up to the previous line where there is content on the
- // right. The previous row is part of the previous section.
- cursor.moveLeft();
-
- assert.equal(cursor.side, 'left');
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.cursorManager.index, firstIndex - 1);
- assert.equal(cursor.diffRow.parentElement,
- firstDeltaSection.previousSibling);
-
- // If we move down, we should skip everything in the first delta because
- // we are on the left side and the first delta has no content on the left.
- cursor.moveDown();
-
- assert.equal(cursor.side, 'left');
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.isTrue(cursor.cursorManager.index > firstIndex);
- assert.equal(cursor.diffRow.parentElement,
- firstDeltaSection.nextSibling);
- });
-
- test('chunk skip functionality', () => {
- const chunks = diffElement.root.querySelectorAll(
- '.section.delta');
- const indexOfChunk = function(chunk) {
- return Array.prototype.indexOf.call(chunks, chunk);
- };
-
- // We should be initialized to the first chunk. Since this chunk only has
- // content on the right side, our side should be right.
- let currentIndex = indexOfChunk(cursor.diffRow.parentElement);
- assert.equal(currentIndex, 0);
- assert.equal(cursor.side, 'right');
-
- // Move to the next chunk.
- cursor.moveToNextChunk();
-
- // Since this chunk only has content on the left side. we should have been
- // automatically moved over.
- const previousIndex = currentIndex;
- currentIndex = indexOfChunk(cursor.diffRow.parentElement);
- assert.equal(currentIndex, previousIndex + 1);
- assert.equal(cursor.side, 'left');
- });
-
- suite('moved chunks without line range)', () => {
- setup(async () => {
- const promise = mockPromise();
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursor.reInitCursor();
- promise.resolve();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.diff = {...diff, content: [
- {
- ab: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ',
- ],
- },
- {
- b: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false},
- },
- {
- ab: [
- 'Sem nascetur, erat ut, non in.',
- ],
- },
- {
- a: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false},
- },
- {
- ab: [
- 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
- ],
- },
- ]};
- await promise;
- });
-
- test('renders moveControls with simple descriptions', () => {
- const [movedIn, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- assert.equal(movedIn.textContent, 'Moved in');
- assert.equal(movedOut.textContent, 'Moved out');
- });
- });
-
- suite('moved chunks (moveDetails)', () => {
- setup(async () => {
- const promise = mockPromise();
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursor.reInitCursor();
- promise.resolve();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.diff = {...diff, content: [
- {
- ab: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ',
- ],
- },
- {
- b: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false, range: {start: 4, end: 6}},
- },
- {
- ab: [
- 'Sem nascetur, erat ut, non in.',
- ],
- },
- {
- a: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false, range: {start: 2, end: 4}},
- },
- {
- ab: [
- 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
- ],
- },
- ]};
- await promise;
- });
-
- test('renders moveControls with simple descriptions', () => {
- const [movedIn, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- assert.equal(movedIn.textContent, 'Moved from lines 4 - 6');
- assert.equal(movedOut.textContent, 'Moved to lines 2 - 4');
- });
-
- test('startLineAnchor of movedIn chunk fires events', async () => {
- const [movedIn] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- const [startLineAnchor] = movedIn.querySelectorAll('a');
-
- const promise = mockPromise();
- const onMovedLinkClicked = e => {
- assert.deepEqual(e.detail, {lineNum: 4, side: 'left'});
- promise.resolve();
- };
- assert.equal(startLineAnchor.textContent, '4');
- startLineAnchor
- .addEventListener('moved-link-clicked', onMovedLinkClicked);
- MockInteractions.click(startLineAnchor);
- await promise;
- });
-
- test('endLineAnchor of movedOut fires events', async () => {
- const [, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- const [, endLineAnchor] = movedOut.querySelectorAll('a');
-
- const promise = mockPromise();
- const onMovedLinkClicked = e => {
- assert.deepEqual(e.detail, {lineNum: 4, side: 'right'});
- promise.resolve();
- };
- assert.equal(endLineAnchor.textContent, '4');
- endLineAnchor.addEventListener('moved-link-clicked', onMovedLinkClicked);
- MockInteractions.click(endLineAnchor);
- await promise;
- });
- });
-
- test('initialLineNumber not provided', async () => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
- const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk')
- .callsFake(() => {
- scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
- });
-
- diffElement._diffChanged(createDiff());
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- assert.isFalse(moveToNumStub.called);
- assert.isTrue(moveToChunkStub.called);
- assert.equal(scrollBehaviorDuringMove, 'never');
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('initialLineNumber provided', async () => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber')
- .callsFake(() => {
- scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
- });
- const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
- cursor.initialLineNumber = 10;
- cursor.side = 'right';
-
- diffElement._diffChanged(createDiff());
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- assert.isFalse(moveToChunkStub.called);
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], 10);
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(scrollBehaviorDuringMove, 'keep-visible');
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('getTargetDiffElement', () => {
- cursor.initialLineNumber = 1;
- assert.isTrue(!!cursor.diffRow);
- assert.equal(
- cursor.getTargetDiffElement(),
- diffElement
- );
- });
-
- suite('createCommentInPlace', () => {
- setup(() => {
- diffElement.loggedIn = true;
- });
-
- test('adds new draft for selected line on the left', async () => {
- cursor.moveToLineNumber(2, 'left');
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 2);
- assert.equal(range, undefined);
- assert.equal(side, 'left');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('adds draft for selected line on the right', async () => {
- cursor.moveToLineNumber(4, 'right');
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 4);
- assert.equal(range, undefined);
- assert.equal(side, 'right');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('creates comment for range if selected', async () => {
- const someRange = {
- start_line: 2,
- start_character: 3,
- end_line: 6,
- end_character: 1,
- };
- diffElement.highlights.selectedRange = {
- side: 'right',
- range: someRange,
- };
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 6);
- assert.equal(range, someRange);
- assert.equal(side, 'right');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('ignores call if nothing is selected', () => {
- const createRangeCommentStub = sinon.stub(diffElement,
- 'createRangeComment');
- const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
- cursor.diffRow = undefined;
- cursor.createCommentInPlace();
- assert.isFalse(createRangeCommentStub.called);
- assert.isFalse(addDraftAtLineStub.called);
- });
- });
-
- test('getAddress', () => {
- // It should initialize to the first chunk: line 5 of the revision.
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 5});
-
- // Revision line 4 is up.
- cursor.moveUp();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 4});
-
- // Base line 4 is left.
- cursor.moveLeft();
- assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 4});
-
- // Moving to the next chunk takes it back to the start.
- cursor.moveToNextChunk();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 5});
-
- // The following chunk is a removal starting on line 10 of the base.
- cursor.moveToNextChunk();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: true, number: 10});
-
- // Should be null if there is no selection.
- cursor.cursorManager.unsetCursor();
- assert.isNotOk(cursor.getAddress());
- });
-
- test('_findRowByNumberAndFile', () => {
- // Get the first ab row after the first chunk.
- const row = diffElement.root.querySelectorAll('tr')[9];
-
- // It should be line 8 on the right, but line 5 on the left.
- assert.equal(cursor._findRowByNumberAndFile(8, 'right'), row);
- assert.equal(cursor._findRowByNumberAndFile(5, 'left'), row);
- });
-
- test('expand context updates stops', async () => {
- sinon.spy(cursor, '_updateStops');
- MockInteractions.tap(diffElement.shadowRoot
- .querySelector('gr-context-controls').shadowRoot
- .querySelector('.showContext'));
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- assert.isTrue(cursor._updateStops.called);
- });
-
- test('updates stops when loading changes', () => {
- sinon.spy(cursor, '_updateStops');
- diffElement.dispatchEvent(new Event('loading-changed'));
- assert.isTrue(cursor._updateStops.called);
- });
-
- suite('multi diff', () => {
- let diffElements;
-
- setup(async () => {
- diffElements = [
- await fixture(html`<gr-diff></gr-diff>`),
- await fixture(html`<gr-diff></gr-diff>`),
- await fixture(html`<gr-diff></gr-diff>`),
- ];
- cursor = new GrDiffCursor();
-
- // Register the diff with the cursor.
- cursor.replaceDiffs(diffElements);
-
- for (const el of diffElements) {
- el.prefs = createDefaultDiffPrefs();
- }
- });
-
- function getTargetDiffIndex() {
- // Mocha has a bug where when `assert.equals` fails, it will try to
- // JSON.stringify the operands, which fails when they are cyclic structures
- // like GrDiffElement. The failure is difficult to attribute to a specific
- // assertion because of the async nature assertion errors are handled and
- // can cause the test simply timing out, causing a lot of debugging headache.
- // Working with indices circumvents the problem.
- return diffElements.indexOf(cursor.getTargetDiffElement());
- }
-
- test('do not skip loading diffs', async () => {
- const diffRenderedPromises =
- diffElements.map(diffEl => listenOnce(diffEl, 'render'));
-
- diffElements[0].diff = createDiff();
- diffElements[2].diff = createDiff();
- await Promise.all([diffRenderedPromises[0], diffRenderedPromises[2]]);
- await new Promise(resolve => afterNextRender(diffElements[0], resolve));
-
- const lastLine = diffElements[0].diff.meta_b.lines;
-
- // Goto second last line of the first diff
- cursor.moveToLineNumber(lastLine - 1, 'right');
- assert.equal(
- cursor.getTargetLineElement().textContent, lastLine - 1);
-
- // Can move down until we reach the loading file
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursor.getTargetLineElement().textContent, lastLine);
-
- // Cannot move down while still loading the diff we would switch to
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursor.getTargetLineElement().textContent, lastLine);
-
- // Diff 1 finishing to load
- diffElements[1].diff = createDiff();
- await diffRenderedPromises[1];
- await new Promise(resolve => afterNextRender(diffElements[0], resolve));
-
- // Now we can go down
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 1);
- assert.equal(cursor.getTargetLineElement().textContent, 'File');
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
new file mode 100644
index 0000000..ac9b407
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
@@ -0,0 +1,693 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff';
+import './gr-diff-cursor';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {mockPromise, queryAll, queryAndAssert} from '../../../test/test-utils';
+import {createDiff} from '../../../test/test-data-generators';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {GrDiffCursor} from './gr-diff-cursor';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {DiffInfo, DiffViewMode, Side} from '../../../api/diff';
+import {GrDiff} from '../gr-diff/gr-diff';
+import {assertIsDefined} from '../../../utils/common-util';
+
+suite('gr-diff-cursor tests', () => {
+ let cursor: GrDiffCursor;
+ let diffElement: GrDiff;
+ let diff: DiffInfo;
+
+ setup(async () => {
+ diffElement = await fixture(html`<gr-diff></gr-diff>`);
+ cursor = new GrDiffCursor();
+
+ // Register the diff with the cursor.
+ cursor.replaceDiffs([diffElement]);
+
+ diffElement.loggedIn = false;
+ diffElement.path = 'some/path.ts';
+ const promise = mockPromise();
+ const setupDone = () => {
+ cursor._updateStops();
+ cursor.moveToFirstChunk();
+ diffElement.removeEventListener('render', setupDone);
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', setupDone);
+
+ diff = createDiff();
+ diffElement.prefs = createDefaultDiffPrefs();
+ diffElement.diff = diff;
+ await promise;
+ });
+
+ test('diff cursor functionality (side-by-side)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursor.diffRow);
+
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta .diff-row'
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+
+ cursor.moveDown();
+
+ assert.isOk(firstDeltaRow.nextElementSibling);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+
+ cursor.moveUp();
+
+ assert.isOk(firstDeltaRow.nextElementSibling);
+ assert.notEqual(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ });
+
+ test('moveToFirstChunk', async () => {
+ const diff: DiffInfo = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {b: ['new line 1']},
+ {ab: ['unchanged line']},
+ {a: ['old line 2']},
+ {ab: ['more unchanged lines']},
+ ],
+ };
+
+ diffElement.diff = diff;
+ // The file comment button, if present, is a cursor stop. Ensure
+ // moveToFirstChunk() works correctly even if the button is not shown.
+ diffElement.prefs!.show_file_comment_button = false;
+ await waitForEventOnce(diffElement, 'render');
+
+ cursor._updateStops();
+
+ const chunks = [
+ ...queryAll(diffElement, '.section.delta'),
+ ] as HTMLElement[];
+ assert.equal(chunks.length, 2);
+
+ // Verify it works on fresh diff.
+ cursor.moveToFirstChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Verify it works from other cursor positions.
+ cursor.moveToNextChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.LEFT);
+ cursor.moveToFirstChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.RIGHT);
+ });
+
+ test('moveToLastChunk', async () => {
+ const diff: DiffInfo = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {ab: ['unchanged line']},
+ {a: ['old line 2']},
+ {ab: ['more unchanged lines']},
+ {b: ['new line 3']},
+ ],
+ };
+
+ diffElement.diff = diff;
+ await waitForEventOnce(diffElement, 'render');
+ cursor._updateStops();
+
+ const chunks = [...queryAll(diffElement, '.section.delta')];
+ assert.equal(chunks.length, 2);
+
+ // Verify it works on fresh diff.
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Verify it works from other cursor positions.
+ cursor.moveToPreviousChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.LEFT);
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.RIGHT);
+ });
+
+ test('cursor scroll behavior', () => {
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+
+ diffElement.dispatchEvent(new Event('render-start'));
+ assert.isTrue(cursor.cursorManager.focusOnMove);
+
+ window.dispatchEvent(new Event('scroll'));
+ assert.equal(cursor.cursorManager.scrollMode, 'never');
+ assert.isFalse(cursor.cursorManager.focusOnMove);
+
+ diffElement.dispatchEvent(new Event('render-content'));
+ assert.isTrue(cursor.cursorManager.focusOnMove);
+
+ cursor.reInitCursor();
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('moves to selected line', () => {
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
+
+ diffElement.dispatchEvent(
+ new CustomEvent('line-selected', {
+ detail: {number: '123', side: Side.RIGHT, path: 'some/file'},
+ })
+ );
+
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 123);
+ assert.equal(moveToNumStub.lastCall.args[1], Side.RIGHT);
+ assert.equal(moveToNumStub.lastCall.args[2], 'some/file');
+ });
+
+ suite('unified diff', () => {
+ setup(async () => {
+ diffElement.viewMode = DiffViewMode.UNIFIED;
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ });
+
+ test('diff cursor functionality (unified)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursor.diffRow);
+
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta .diff-row'
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+
+ cursor.moveDown();
+
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+
+ cursor.moveUp();
+
+ assert.notEqual(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ });
+ });
+
+ test('cursor side functionality', () => {
+ // The side only applies to side-by-side mode, which should be the default
+ // mode.
+ assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
+
+ const firstDeltaSection = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta'
+ );
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ firstDeltaSection,
+ '.diff-row'
+ );
+
+ // Because the first delta in this diff is on the right, it should be set
+ // to the right side.
+ assert.equal(cursor.side, Side.RIGHT);
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ const firstIndex = cursor.cursorManager.index;
+
+ // Move the side to the left. Because this delta only has a right side, we
+ // should be moved up to the previous line where there is content on the
+ // right. The previous row is part of the previous section.
+ cursor.moveLeft();
+
+ assert.equal(cursor.side, Side.LEFT);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(cursor.cursorManager.index, firstIndex - 1);
+ assert.equal(
+ cursor.diffRow!.parentElement,
+ firstDeltaSection.previousSibling
+ );
+
+ // If we move down, we should skip everything in the first delta because
+ // we are on the left side and the first delta has no content on the left.
+ cursor.moveDown();
+
+ assert.equal(cursor.side, Side.LEFT);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.isTrue(cursor.cursorManager.index > firstIndex);
+ assert.equal(cursor.diffRow!.parentElement, firstDeltaSection.nextSibling);
+ });
+
+ test('chunk skip functionality', () => {
+ const chunks = [...queryAll(diffElement, '.section.delta')];
+ const indexOfChunk = function (chunk: HTMLElement) {
+ return Array.prototype.indexOf.call(chunks, chunk);
+ };
+
+ // We should be initialized to the first chunk. Since this chunk only has
+ // content on the right side, our side should be right.
+ let currentIndex = indexOfChunk(cursor.diffRow!.parentElement!);
+ assert.equal(currentIndex, 0);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Move to the next chunk.
+ cursor.moveToNextChunk();
+
+ // Since this chunk only has content on the left side. we should have been
+ // automatically moved over.
+ const previousIndex = currentIndex;
+ currentIndex = indexOfChunk(cursor.diffRow!.parentElement!);
+ assert.equal(currentIndex, previousIndex + 1);
+ assert.equal(cursor.side, Side.LEFT);
+ });
+
+ suite('moved chunks without line range)', () => {
+ setup(async () => {
+ const promise = mockPromise();
+ const renderHandler = function () {
+ diffElement.removeEventListener('render', renderHandler);
+ cursor.reInitCursor();
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.diff = {
+ ...diff,
+ content: [
+ {
+ ab: ['Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, '],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false},
+ },
+ {
+ ab: ['Sem nascetur, erat ut, non in.'],
+ },
+ {
+ a: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false},
+ },
+ {
+ ab: ['Arcu eget, rhoncus amet cursus, ipsum elementum.'],
+ },
+ ],
+ };
+ await promise;
+ });
+
+ test('renders moveControls with simple descriptions', () => {
+ const [movedIn, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ assert.equal(movedIn.textContent, 'Moved in');
+ assert.equal(movedOut.textContent, 'Moved out');
+ });
+ });
+
+ suite('moved chunks (moveDetails)', () => {
+ setup(async () => {
+ const promise = mockPromise();
+ const renderHandler = function () {
+ diffElement.removeEventListener('render', renderHandler);
+ cursor.reInitCursor();
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.diff = {
+ ...diff,
+ content: [
+ {
+ ab: ['Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, '],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false, range: {start: 4, end: 6}},
+ },
+ {
+ ab: ['Sem nascetur, erat ut, non in.'],
+ },
+ {
+ a: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false, range: {start: 2, end: 4}},
+ },
+ {
+ ab: ['Arcu eget, rhoncus amet cursus, ipsum elementum.'],
+ },
+ ],
+ };
+ await promise;
+ });
+
+ test('renders moveControls with simple descriptions', () => {
+ const [movedIn, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ assert.equal(movedIn.textContent, 'Moved from lines 4 - 6');
+ assert.equal(movedOut.textContent, 'Moved to lines 2 - 4');
+ });
+
+ test('startLineAnchor of movedIn chunk fires events', async () => {
+ const [movedIn] = [...queryAll(diffElement, '.dueToMove .moveControls')];
+ const [startLineAnchor] = movedIn.querySelectorAll('a');
+
+ const promise = mockPromise();
+ const onMovedLinkClicked = (e: CustomEvent) => {
+ assert.deepEqual(e.detail, {lineNum: 4, side: Side.LEFT});
+ promise.resolve();
+ };
+ assert.equal(startLineAnchor.textContent, '4');
+ startLineAnchor.addEventListener(
+ 'moved-link-clicked',
+ onMovedLinkClicked
+ );
+ startLineAnchor.click();
+ await promise;
+ });
+
+ test('endLineAnchor of movedOut fires events', async () => {
+ const [, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ const [, endLineAnchor] = movedOut.querySelectorAll('a');
+
+ const promise = mockPromise();
+ const onMovedLinkClicked = (e: CustomEvent) => {
+ assert.deepEqual(e.detail, {lineNum: 4, side: Side.RIGHT});
+ promise.resolve();
+ };
+ assert.equal(endLineAnchor.textContent, '4');
+ endLineAnchor.addEventListener('moved-link-clicked', onMovedLinkClicked);
+ endLineAnchor.click();
+ await promise;
+ });
+ });
+
+ test('initialLineNumber not provided', async () => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
+ const moveToChunkStub = sinon
+ .stub(cursor, 'moveToFirstChunk')
+ .callsFake(() => {
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
+ });
+
+ diffElement._diffChanged(createDiff());
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ assert.isFalse(moveToNumStub.called);
+ assert.isTrue(moveToChunkStub.called);
+ assert.equal(scrollBehaviorDuringMove, 'never');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('initialLineNumber provided', async () => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sinon
+ .stub(cursor, 'moveToLineNumber')
+ .callsFake(() => {
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
+ });
+ const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
+ cursor.initialLineNumber = 10;
+ cursor.side = Side.RIGHT;
+
+ diffElement._diffChanged(createDiff());
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ assert.isFalse(moveToChunkStub.called);
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 10);
+ assert.equal(moveToNumStub.lastCall.args[1], Side.RIGHT);
+ assert.equal(scrollBehaviorDuringMove, 'keep-visible');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('getTargetDiffElement', () => {
+ cursor.initialLineNumber = 1;
+ assert.isTrue(!!cursor.diffRow);
+ assert.equal(cursor.getTargetDiffElement(), diffElement);
+ });
+
+ suite('createCommentInPlace', () => {
+ setup(() => {
+ diffElement.loggedIn = true;
+ });
+
+ test('adds new draft for selected line on the left', async () => {
+ cursor.moveToLineNumber(2, Side.LEFT);
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 2);
+ assert.equal(range, undefined);
+ assert.equal(side, Side.LEFT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('adds draft for selected line on the right', async () => {
+ cursor.moveToLineNumber(4, Side.RIGHT);
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 4);
+ assert.equal(range, undefined);
+ assert.equal(side, Side.RIGHT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('creates comment for range if selected', async () => {
+ const someRange = {
+ start_line: 2,
+ start_character: 3,
+ end_line: 6,
+ end_character: 1,
+ };
+ diffElement.highlights.selectedRange = {
+ side: Side.RIGHT,
+ range: someRange,
+ };
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 6);
+ assert.equal(range, someRange);
+ assert.equal(side, Side.RIGHT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('ignores call if nothing is selected', () => {
+ const createRangeCommentStub = sinon.stub(
+ diffElement,
+ 'createRangeComment'
+ );
+ const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
+ cursor.diffRow = undefined;
+ cursor.createCommentInPlace();
+ assert.isFalse(createRangeCommentStub.called);
+ assert.isFalse(addDraftAtLineStub.called);
+ });
+ });
+
+ test('getAddress', () => {
+ // It should initialize to the first chunk: line 5 of the revision.
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 5});
+
+ // Revision line 4 is up.
+ cursor.moveUp();
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 4});
+
+ // Base line 4 is left.
+ cursor.moveLeft();
+ assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 4});
+
+ // Moving to the next chunk takes it back to the start.
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 5});
+
+ // The following chunk is a removal starting on line 10 of the base.
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 10});
+
+ // Should be null if there is no selection.
+ cursor.cursorManager.unsetCursor();
+ assert.isNotOk(cursor.getAddress());
+ });
+
+ test('_findRowByNumberAndFile', () => {
+ // Get the first ab row after the first chunk.
+ const rows = [...queryAll<HTMLTableRowElement>(diffElement, 'tr')];
+ const row = rows[9];
+ assert.ok(row);
+
+ // It should be line 8 on the right, but line 5 on the left.
+ assert.equal(cursor._findRowByNumberAndFile(8, Side.RIGHT), row);
+ assert.equal(cursor._findRowByNumberAndFile(5, Side.LEFT), row);
+ });
+
+ test('expand context updates stops', async () => {
+ const spy = sinon.spy(cursor, '_updateStops');
+ const controls = queryAndAssert(diffElement, 'gr-context-controls');
+ const showContext = queryAndAssert<HTMLElement>(controls, '.showContext');
+ showContext.click();
+ await waitForEventOnce(diffElement, 'render');
+ assert.isTrue(spy.called);
+ });
+
+ test('updates stops when loading changes', () => {
+ const spy = sinon.spy(cursor, '_updateStops');
+ diffElement.dispatchEvent(new Event('loading-changed'));
+ assert.isTrue(spy.called);
+ });
+
+ suite('multi diff', () => {
+ let diffElements: GrDiff[];
+
+ setup(async () => {
+ diffElements = [
+ await fixture(html`<gr-diff></gr-diff>`),
+ await fixture(html`<gr-diff></gr-diff>`),
+ await fixture(html`<gr-diff></gr-diff>`),
+ ];
+ cursor = new GrDiffCursor();
+
+ // Register the diff with the cursor.
+ cursor.replaceDiffs(diffElements);
+
+ for (const el of diffElements) {
+ el.prefs = createDefaultDiffPrefs();
+ }
+ });
+
+ function getTargetDiffIndex() {
+ // Mocha has a bug where when `assert.equals` fails, it will try to
+ // JSON.stringify the operands, which fails when they are cyclic structures
+ // like GrDiffElement. The failure is difficult to attribute to a specific
+ // assertion because of the async nature assertion errors are handled and
+ // can cause the test simply timing out, causing a lot of debugging headache.
+ // Working with indices circumvents the problem.
+ const target = cursor.getTargetDiffElement();
+ assertIsDefined(target);
+ return diffElements.indexOf(target);
+ }
+
+ test('do not skip loading diffs', async () => {
+ diffElements[0].diff = createDiff();
+ diffElements[2].diff = createDiff();
+ await waitForEventOnce(diffElements[0], 'render');
+ await waitForEventOnce(diffElements[2], 'render');
+
+ const lastLine = diffElements[0].diff.meta_b?.lines;
+ assertIsDefined(lastLine);
+
+ // Goto second last line of the first diff
+ cursor.moveToLineNumber(lastLine - 1, Side.RIGHT);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ `${lastLine - 1}`
+ );
+
+ // Can move down until we reach the loading file
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 0);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ lastLine.toString()
+ );
+
+ // Cannot move down while still loading the diff we would switch to
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 0);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ lastLine.toString()
+ );
+
+ // Diff 1 finishing to load
+ diffElements[1].diff = createDiff();
+ await waitForEventOnce(diffElements[1], 'render');
+
+ // Now we can go down
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 1);
+ assert.equal(cursor.getTargetLineElement()!.textContent, 'File');
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
similarity index 62%
rename from polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js
rename to polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
index 321086c..43a56d1 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
@@ -1,32 +1,25 @@
/**
* @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-
-import '../../../test/common-test-setup-karma.js';
-import {GrDiffLine, GrDiffLineType, BLANK_LINE} from './gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType, hideInContextControl} from './gr-diff-group.js';
+import '../../../test/common-test-setup-karma';
+import {GrDiffLine, GrDiffLineType, BLANK_LINE} from './gr-diff-line';
+import {
+ GrDiffGroup,
+ GrDiffGroupType,
+ hideInContextControl,
+} from './gr-diff-group';
suite('gr-diff-group tests', () => {
test('delta line pairs', () => {
const l1 = new GrDiffLine(GrDiffLineType.ADD, 0, 128);
const l2 = new GrDiffLine(GrDiffLineType.ADD, 0, 129);
const l3 = new GrDiffLine(GrDiffLineType.REMOVE, 64, 0);
- let group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [
- l1, l2, l3,
- ]});
+ let group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines: [l1, l2, l3],
+ });
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, [l1, l2]);
assert.deepEqual(group.removes, [l3]);
@@ -59,7 +52,9 @@
const l3 = new GrDiffLine(GrDiffLineType.BOTH, 66, 130);
const group = new GrDiffGroup({
- type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]});
+ type: GrDiffGroupType.BOTH,
+ lines: [l1, l2, l3],
+ });
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, []);
@@ -83,34 +78,44 @@
const l2 = new GrDiffLine(GrDiffLineType.REMOVE);
const l3 = new GrDiffLine(GrDiffLineType.BOTH);
- assert.throws(() =>
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]}));
+ assert.throws(
+ () => new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]})
+ );
});
suite('hideInContextControl', () => {
- let groups;
+ let groups: GrDiffGroup[];
setup(() => {
groups = [
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
- new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
- new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
- ]}),
- new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [
- new GrDiffLine(GrDiffLineType.REMOVE, 8),
- new GrDiffLine(GrDiffLineType.ADD, 0, 10),
- new GrDiffLine(GrDiffLineType.REMOVE, 9),
- new GrDiffLine(GrDiffLineType.ADD, 0, 11),
- new GrDiffLine(GrDiffLineType.REMOVE, 10),
- new GrDiffLine(GrDiffLineType.ADD, 0, 12),
- new GrDiffLine(GrDiffLineType.REMOVE, 11),
- new GrDiffLine(GrDiffLineType.ADD, 0, 13),
- ]}),
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
- new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
- new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
- ]}),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
+ new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
+ new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
+ ],
+ }),
+ new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines: [
+ new GrDiffLine(GrDiffLineType.REMOVE, 8),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 10),
+ new GrDiffLine(GrDiffLineType.REMOVE, 9),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 11),
+ new GrDiffLine(GrDiffLineType.REMOVE, 10),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 12),
+ new GrDiffLine(GrDiffLineType.REMOVE, 11),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 13),
+ ],
+ }),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
+ new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
+ new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
+ ],
+ }),
];
});
@@ -140,21 +145,25 @@
assert.equal(collapsedGroups[2].contextGroups.length, 2);
assert.equal(
- collapsedGroups[2].contextGroups[0].type,
- GrDiffGroupType.DELTA);
+ collapsedGroups[2].contextGroups[0].type,
+ GrDiffGroupType.DELTA
+ );
assert.deepEqual(
- collapsedGroups[2].contextGroups[0].adds,
- groups[1].adds.slice(1));
+ collapsedGroups[2].contextGroups[0].adds,
+ groups[1].adds.slice(1)
+ );
assert.deepEqual(
- collapsedGroups[2].contextGroups[0].removes,
- groups[1].removes.slice(1));
+ collapsedGroups[2].contextGroups[0].removes,
+ groups[1].removes.slice(1)
+ );
assert.equal(
- collapsedGroups[2].contextGroups[1].type,
- GrDiffGroupType.BOTH);
- assert.deepEqual(
- collapsedGroups[2].contextGroups[1].lines,
- [groups[2].lines[0]]);
+ collapsedGroups[2].contextGroups[1].type,
+ GrDiffGroupType.BOTH
+ );
+ assert.deepEqual(collapsedGroups[2].contextGroups[1].lines, [
+ groups[2].lines[0],
+ ]);
assert.equal(collapsedGroups[3].type, GrDiffGroupType.BOTH);
assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
@@ -166,19 +175,26 @@
type: GrDiffGroupType.BOTH,
skip: 60,
offsetLeft: 8,
- offsetRight: 10});
+ offsetRight: 10,
+ });
groups = [
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
- new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
- new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
- ]}),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
+ new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
+ new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
+ ],
+ }),
skipGroup,
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 68, 70),
- new GrDiffLine(GrDiffLineType.BOTH, 69, 71),
- new GrDiffLine(GrDiffLineType.BOTH, 70, 72),
- ]}),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 68, 70),
+ new GrDiffLine(GrDiffLineType.BOTH, 69, 71),
+ new GrDiffLine(GrDiffLineType.BOTH, 70, 72),
+ ],
+ }),
];
});
@@ -189,13 +205,11 @@
});
test('groups unchanged if the hidden range is empty', () => {
- assert.deepEqual(
- hideInContextControl(groups, 0, 0), groups);
+ assert.deepEqual(hideInContextControl(groups, 0, 0), groups);
});
test('groups unchanged if there is only 1 line to hide', () => {
- assert.deepEqual(
- hideInContextControl(groups, 3, 4), groups);
+ assert.deepEqual(hideInContextControl(groups, 3, 4), groups);
});
});
@@ -206,7 +220,7 @@
lines.push(new GrDiffLine(GrDiffLineType.ADD));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isTrue(group.isTotal(group));
+ assert.isTrue(group.isTotal());
});
test('is total for remove', () => {
@@ -215,12 +229,12 @@
lines.push(new GrDiffLine(GrDiffLineType.REMOVE));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isTrue(group.isTotal(group));
+ assert.isTrue(group.isTotal());
});
test('not total for empty', () => {
const group = new GrDiffGroup({type: GrDiffGroupType.BOTH});
- assert.isFalse(group.isTotal(group));
+ assert.isFalse(group.isTotal());
});
test('not total for non-delta', () => {
@@ -229,8 +243,7 @@
lines.push(new GrDiffLine(GrDiffLineType.BOTH));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isFalse(group.isTotal(group));
+ assert.isFalse(group.isTotal());
});
});
});
-
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 e5f9de0..27952d3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -43,11 +43,7 @@
import {getHiddenScroll} from '../../../scripts/hiddenscroll';
import {customElement, observe, property} from '@polymer/decorators';
import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common';
-import {
- DiffInfo,
- DiffPreferencesInfo,
- DiffPreferencesInfoKey,
-} from '../../../types/diff';
+import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
import {
GrDiffBuilderElement,
@@ -81,6 +77,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {GrDiffSelection} from '../gr-diff-selection/gr-diff-selection';
+import {deepEqual} from '../../../utils/deep-util';
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -100,7 +97,6 @@
export interface GrDiff {
$: {
- diffBuilder: GrDiffBuilderElement;
diffTable: HTMLTableElement;
};
}
@@ -179,7 +175,7 @@
@property({type: Object})
highlightRange?: CommentRange;
- @property({type: Array})
+ @property({type: Array, observer: '_coverageRangesObserver'})
coverageRanges: CoverageRange[] = [];
@property({type: Boolean, observer: '_lineWrappingObserver'})
@@ -248,9 +244,6 @@
@property({type: Object, observer: '_blameChanged'})
blame: BlameInfo[] | null = null;
- @property({type: Number})
- parentIndex?: number;
-
@property({type: Boolean})
showNewlineWarningLeft = false;
@@ -292,11 +285,16 @@
@property({type: Boolean})
isAttached = false;
- private renderDiffTableTask?: DelayedTask;
+ // visible for testing
+ renderDiffTableTask?: DelayedTask;
private diffSelection = new GrDiffSelection();
- private highlights = new GrDiffHighlight();
+ // visible for testing
+ highlights = new GrDiffHighlight();
+
+ // visible for testing
+ diffBuilder = new GrDiffBuilderElement();
constructor() {
super();
@@ -321,11 +319,12 @@
this._unobserveNodes();
this.diffSelection.cleanup();
this.highlights.cleanup();
+ this.diffBuilder.cancel();
super.disconnectedCallback();
}
getLineNumEls(side: Side): HTMLElement[] {
- return this.$.diffBuilder.getLineNumEls(side);
+ return this.diffBuilder.getLineNumEls(side);
}
showNoChangeMessage(
@@ -426,19 +425,25 @@
cr.side === removedCommentRange.side &&
rangesEqual(cr.range, removedCommentRange.range)
);
- this.splice('_commentRanges', i, 1);
+ this._commentRanges.splice(i, 1);
}
- if (addedCommentRanges && addedCommentRanges.length) {
- this.push('_commentRanges', ...addedCommentRanges);
+ if (addedCommentRanges?.length) {
+ this._commentRanges.push(...addedCommentRanges);
}
if (this.highlightRange) {
- this.push('_commentRanges', {
+ this._commentRanges.push({
side: Side.RIGHT,
range: this.highlightRange,
rootId: '',
});
}
+
+ this.diffBuilder.updateCommentRanges(this._commentRanges);
+ }
+
+ _coverageRangesObserver() {
+ this.diffBuilder.updateCoverageRanges(this.coverageRanges);
}
/**
@@ -483,7 +488,7 @@
/** Cancel any remaining diff builder rendering work. */
cancel() {
- this.$.diffBuilder.cancel();
+ this.diffBuilder.cancel();
this.renderDiffTableTask?.cancel();
}
@@ -492,7 +497,7 @@
// Get rendered stops.
const stops: Array<HTMLElement | AbortStop> =
- this.$.diffBuilder.getLineNumberRows();
+ this.diffBuilder.getLineNumberRows();
// If we are still loading this diff, abort after the rendered stops to
// avoid skipping over to e.g. the next file.
@@ -512,7 +517,7 @@
_blameChanged(newValue?: BlameInfo[] | null) {
if (newValue === undefined) return;
- this.$.diffBuilder.setBlame(newValue);
+ this.diffBuilder.setBlame(newValue);
if (newValue) {
this.classList.add('showBlame');
} else {
@@ -534,7 +539,7 @@
return classes.join(' ');
}
- _handleTap(e: CustomEvent) {
+ _handleTap(e: Event) {
const el = (dom(e) as EventApi).localTarget as Element;
if (
@@ -603,7 +608,7 @@
_createCommentForSelection(side: Side, range: CommentRange) {
const lineNum = range.end_line;
- const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
+ const lineEl = this.diffBuilder.getLineElByNumber(lineNum, side);
if (lineEl) {
this._createComment(lineEl, lineNum, side, range);
}
@@ -621,7 +626,7 @@
side?: Side,
range?: CommentRange
) {
- const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
+ const contentEl = this.diffBuilder.getContentTdByLineEl(lineEl);
if (!contentEl) throw new Error('content el not found for line el');
side = side ?? this._getCommentSideByLineAndContent(lineEl, contentEl);
assertIsDefined(this.path, 'path');
@@ -663,28 +668,11 @@
}
_prefsObserver(newPrefs: DiffPreferencesInfo, oldPrefs: DiffPreferencesInfo) {
- if (!this._prefsEqual(newPrefs, oldPrefs)) {
+ if (!deepEqual(newPrefs, oldPrefs)) {
this._prefsChanged(newPrefs);
}
}
- _prefsEqual(prefs1: DiffPreferencesInfo, prefs2: DiffPreferencesInfo) {
- if (prefs1 === prefs2) {
- return true;
- }
- if (!prefs1 || !prefs2) {
- return false;
- }
- // Scan the preference objects one level deep to see if they differ.
- const keys1 = Object.keys(prefs1) as DiffPreferencesInfoKey[];
- const keys2 = Object.keys(prefs2) as DiffPreferencesInfoKey[];
- return (
- keys1.length === keys2.length &&
- keys1.every(key => prefs1[key] === prefs2[key]) &&
- keys2.every(key => prefs1[key] === prefs2[key])
- );
- }
-
_pathObserver() {
// Call _prefsChanged(), because line-limit style value depends on path.
this._prefsChanged(this.prefs);
@@ -699,7 +687,7 @@
if (!this.lineOfInterest) return;
const lineNum = this.lineOfInterest.lineNum;
if (typeof lineNum !== 'number') return;
- this.$.diffBuilder.unhideLine(lineNum, this.lineOfInterest.side);
+ this.diffBuilder.unhideLine(lineNum, this.lineOfInterest.side);
}
_cleanup() {
@@ -808,7 +796,7 @@
if (this.prefs) {
this._updatePreferenceStyles(this.prefs, renderPrefs);
}
- this.$.diffBuilder.updateRenderPrefs(renderPrefs);
+ this.diffBuilder.updateRenderPrefs(renderPrefs);
}
_diffChanged(newValue?: DiffInfo) {
@@ -820,7 +808,7 @@
}
if (this.diff) {
this.diffSelection.init(this.diff, this.$.diffTable);
- this.highlights.init(this.$.diffTable, this.$.diffBuilder);
+ this.highlights.init(this.$.diffTable, this.diffBuilder);
}
}
@@ -866,9 +854,24 @@
this._showWarning = false;
const keyLocations = this._computeKeyLocations();
- this.$.diffBuilder.prefs = this._getBypassPrefs(this.prefs);
- this.$.diffBuilder.renderPrefs = this.renderPrefs;
- this.$.diffBuilder.render(keyLocations);
+
+ // TODO: Setting tons of public properties like this is obviously a code
+ // smell. We are planning to introduce a diff model for managing all this
+ // data. Then diff builder will only need access to that model.
+ this.diffBuilder.prefs = this._getBypassPrefs(this.prefs);
+ this.diffBuilder.renderPrefs = this.renderPrefs;
+ this.diffBuilder.diff = this.diff;
+ this.diffBuilder.path = this.path;
+ this.diffBuilder.viewMode = this.viewMode;
+ this.diffBuilder.layers = this.layers ?? [];
+ this.diffBuilder.isImageDiff = this.isImageDiff;
+ this.diffBuilder.baseImage = this.baseImage ?? null;
+ this.diffBuilder.revisionImage = this.revisionImage ?? null;
+ this.diffBuilder.useNewImageDiffUi = this.useNewImageDiffUi;
+ this.diffBuilder.diffElement = this.$.diffTable;
+ this.diffBuilder.updateCommentRanges(this._commentRanges);
+ this.diffBuilder.updateCoverageRanges(this.coverageRanges);
+ this.diffBuilder.render(keyLocations);
}
_handleRenderContent() {
@@ -895,10 +898,7 @@
const commentSide = getSide(threadEl);
const range = getRange(threadEl);
if (!commentSide) continue;
- const lineEl = this.$.diffBuilder.getLineElByNumber(
- lineNum,
- commentSide
- );
+ const lineEl = this.diffBuilder.getLineElByNumber(lineNum, commentSide);
// When the line the comment refers to does not exist, log an error
// but don't crash. This can happen e.g. if the API does not fully
// validate e.g. (robot) comments
@@ -911,7 +911,7 @@
);
continue;
}
- const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
+ const contentEl = this.diffBuilder.getContentTdByLineEl(lineEl);
if (!contentEl) continue;
if (lineNum === 'LOST' && !contentEl.hasChildNodes()) {
contentEl.appendChild(this._portedCommentsWithoutRangeMessage());
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
index 6d36b89..40d4e7f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
@@ -698,36 +698,22 @@
class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
on-click="_handleTap"
>
- <gr-diff-builder
- id="diffBuilder"
- comment-ranges="[[_commentRanges]]"
- coverage-ranges="[[coverageRanges]]"
- diff="[[diff]]"
- path="[[path]]"
- view-mode="[[viewMode]]"
- is-image-diff="[[isImageDiff]]"
- base-image="[[baseImage]]"
- layers="[[layers]]"
- revision-image="[[revisionImage]]"
- use-new-image-diff-ui="[[useNewImageDiffUi]]"
- >
- <table
- id="diffTable"
- class$="[[_diffTableClass]]"
- role="presentation"
- contenteditable$="[[isContentEditable]]"
- ></table>
+ <table
+ id="diffTable"
+ class$="[[_diffTableClass]]"
+ role="presentation"
+ contenteditable$="[[isContentEditable]]"
+ ></table>
- <template
- is="dom-if"
- if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
- >
- <div class="whitespace-change-only-message">
- This file only contains whitespace changes. Modify the whitespace
- setting to see the changes.
- </div>
- </template>
- </gr-diff-builder>
+ <template
+ is="dom-if"
+ if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
+ >
+ <div class="whitespace-change-only-message">
+ This file only contains whitespace changes. Modify the whitespace
+ setting to see the changes.
+ </div>
+ </template>
</div>
<div class$="[[_computeNewlineWarningClass(_newlineWarning, _loading)]]">
[[_newlineWarning]]
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
similarity index 62%
rename from polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
rename to polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index c8b643d..183cdfb 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -1,32 +1,37 @@
/**
* @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Copyright 2015 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-import '../../../test/common-test-setup-karma.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import './gr-diff.js';
-import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
-import {getComputedStyleValue} from '../../../utils/dom-util.js';
-import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
-import {runA11yAudit} from '../../../test/a11y-test-utils.js';
-import '@polymer/paper-button/paper-button.js';
-import {Side} from '../../../api/diff.js';
-import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
-import {AbortStop} from '../../../api/core.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-import {waitForEventOnce} from '../../../utils/event-util.js';
+import '../../../test/common-test-setup-karma';
+import {createDiff} from '../../../test/test-data-generators';
+import './gr-diff';
+import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image';
+import {getComputedStyleValue} from '../../../utils/dom-util';
+import {_setHiddenScroll} from '../../../scripts/hiddenscroll';
+import {runA11yAudit} from '../../../test/a11y-test-utils';
+import '@polymer/paper-button/paper-button';
+import {
+ DiffContent,
+ DiffInfo,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ IgnoreWhitespaceType,
+ Side,
+} from '../../../api/diff';
+import {
+ mockPromise,
+ mouseDown,
+ query,
+ queryAll,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {AbortStop} from '../../../api/core';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiff} from './gr-diff';
+import {ImageInfo} from '../../../types/common';
+import {GrRangedCommentHint} from '../gr-ranged-comment-hint/gr-ranged-comment-hint';
const basicFixture = fixtureFromElement('gr-diff');
@@ -37,42 +42,51 @@
});
suite('gr-diff tests', () => {
- let element;
+ let element: GrDiff;
- const MINIMAL_PREFS = {tab_size: 2, line_length: 80, font_size: 12};
+ const MINIMAL_PREFS: DiffPreferencesInfo = {
+ tab_size: 2,
+ line_length: 80,
+ font_size: 12,
+ context: 3,
+ ignore_whitespace: 'IGNORE_NONE',
+ };
- setup(() => {
-
- });
+ setup(() => {});
suite('selectionchange event handling', () => {
- const emulateSelection = function() {
+ let handleSelectionChangeStub: sinon.SinonSpy;
+
+ const emulateSelection = function () {
document.dispatchEvent(new CustomEvent('selectionchange'));
};
setup(() => {
element = basicFixture.instantiate();
- sinon.stub(element.highlights, 'handleSelectionChange');
+ handleSelectionChangeStub = sinon.spy(
+ element.highlights,
+ 'handleSelectionChange'
+ );
});
test('enabled if logged in', async () => {
element.loggedIn = true;
emulateSelection();
await flush();
- assert.isTrue(element.highlights.handleSelectionChange.called);
+ assert.isTrue(handleSelectionChangeStub.called);
});
test('ignored if logged out', async () => {
element.loggedIn = false;
emulateSelection();
await flush();
- assert.isFalse(element.highlights.handleSelectionChange.called);
+ assert.isFalse(handleSelectionChangeStub.called);
});
});
test('cancel', () => {
element = basicFixture.instantiate();
- const cancelStub = sinon.stub(element.$.diffBuilder, 'cancel');
+ const cancelStub = sinon.stub(element.diffBuilder, 'cancel');
element.cancel();
assert.isTrue(cancelStub.calledOnce);
});
@@ -98,10 +112,12 @@
});
test('line limit is based on line_length', () => {
- element.prefs = {...element.prefs, line_length: 100};
+ element.prefs = {...element.prefs!, line_length: 100};
flush();
- assert.equal(getComputedStyleValue('--line-limit-marker', element),
- '100ch');
+ assert.equal(
+ getComputedStyleValue('--line-limit-marker', element),
+ '100ch'
+ );
});
test('content-width should not be defined', () => {
@@ -123,32 +139,40 @@
});
test('max-width considers two content columns in side-by-side', () => {
- element.viewMode = 'SIDE_BY_SIDE';
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px + 0ch + 1px + 2px)'
+ );
});
test('max-width considers one content column in unified', () => {
- element.viewMode = 'UNIFIED_DIFF';
+ element.viewMode = DiffViewMode.UNIFIED;
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(1 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(1 * 80ch + 2 * 48px + 0ch + 1px + 2px)'
+ );
});
test('max-width considers font-size', () => {
- element.prefs = {...element.prefs, font_size: 13};
+ element.prefs = {...element.prefs!, font_size: 13};
flush();
// Each line number column: 4 * 13 = 52px
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 52px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 52px + 0ch + 1px + 2px)'
+ );
});
test('sign cols are considered if show_sign_col is true', () => {
element.renderPrefs = {...element.renderPrefs, show_sign_col: true};
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 48px + 2ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px + 2ch + 1px + 2px)'
+ );
});
});
@@ -168,39 +192,31 @@
});
test('view does not start with displayLine classList', () => {
- assert.isFalse(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ const container = queryAndAssert(element, '.diffContainer');
+ assert.isFalse(container.classList.contains('displayLine'));
});
test('displayLine class added called when displayLine is true', () => {
const spy = sinon.spy(element, '_computeContainerClass');
element.displayLine = true;
+ const container = queryAndAssert(element, '.diffContainer');
assert.isTrue(spy.called);
- assert.isTrue(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ assert.isTrue(container.classList.contains('displayLine'));
});
test('thread groups', () => {
const contentEl = document.createElement('div');
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
- element.$.diffBuilder.diff = createDiff();
- element.$.diffBuilder.prefs = {...MINIMAL_PREFS};
- element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder();
// No thread groups.
assert.equal(contentEl.querySelectorAll('.thread-group').length, 0);
// A thread group gets created.
- const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
+ const threadGroupEl = element._getOrCreateThreadGroup(
+ contentEl,
+ Side.LEFT
+ );
assert.isOk(threadGroupEl);
// The new thread group can be fetched.
@@ -208,17 +224,19 @@
});
suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
+ let mockFile1: ImageInfo;
+ let mockFile2: ImageInfo;
setup(() => {
mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
type: 'image/bmp',
};
mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
type: 'image/bmp',
};
@@ -235,7 +253,6 @@
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
- theme: 'DEFAULT',
};
});
@@ -244,8 +261,7 @@
element.revisionImage = mockFile2;
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
@@ -262,42 +278,40 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
+ const diffTable = element.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = queryAndAssert(leftLabel, '.label');
+ const leftLabelName = query(leftLabel, '.name');
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = queryAndAssert(rightLabel, '.label');
+ const rightLabelName = query(rightLabel, '.name');
assert.isNotOk(rightLabelName);
assert.isNotOk(leftLabelName);
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
});
test('renders image diffs with a different file name', async () => {
- const mockDiff = {
+ const mockDiff: DiffInfo = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
@@ -312,51 +326,51 @@
};
element.baseImage = mockFile1;
- element.baseImage._name = mockDiff.meta_a.name;
+ element.baseImage._name = mockDiff.meta_a!.name;
element.revisionImage = mockFile2;
- element.revisionImage._name = mockDiff.meta_b.name;
+ element.revisionImage._name = mockDiff.meta_b!.name;
element.diff = mockDiff;
await waitForEventOnce(element, 'render');
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
+ const diffTable = element.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = queryAndAssert(leftLabel, '.label');
+ const leftLabelName = queryAndAssert(leftLabel, '.name');
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = queryAndAssert(rightLabel, '.label');
+ const rightLabelName = queryAndAssert(rightLabel, '.name');
assert.isOk(rightLabelName);
assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a?.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b?.name);
assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp'); // \u00d7 - '×'
});
test('renders added image', async () => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'ADDED',
diff_header: [
@@ -371,7 +385,9 @@
};
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.revisionImage = mockFile2;
@@ -380,20 +396,17 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
+ const diffTable = element.$.diffTable;
+ const leftImage = query(diffTable, 'td.left img');
assert.isNotOk(leftImage);
- assert.isOk(rightImage);
+ queryAndAssert(diffTable, 'td.right img');
});
test('renders removed image', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'DELETED',
diff_header: [
@@ -407,7 +420,9 @@
binary: true,
};
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.baseImage = mockFile1;
@@ -416,20 +431,21 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
- assert.isOk(leftImage);
+ const diffTable = element.$.diffTable;
+ queryAndAssert(diffTable, 'td.left img');
+ const rightImage = query(diffTable, 'td.right img');
assert.isNotOk(rightImage);
});
test('does not render disallowed image type', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_a: {
+ name: 'carrot.jpg',
+ content_type: 'image/jpeg-evil',
+ lines: 560,
+ },
intraline_status: 'OK',
change_type: 'DELETED',
diff_header: [
@@ -445,7 +461,9 @@
mockFile1.type = 'image/jpeg-evil';
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.baseImage = mockFile1;
@@ -454,9 +472,9 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
+ const diffTable = element.$.diffTable;
+ const leftImage = query(diffTable, 'td.left img');
assert.isNotOk(leftImage);
});
});
@@ -513,7 +531,6 @@
show_tabs: true,
show_whitespace_errors: true,
syntax_highlighting: true,
- theme: 'DEFAULT',
ignore_whitespace: 'IGNORE_NONE',
};
@@ -548,20 +565,20 @@
const FILE_ROW = 1;
const actual = element.getCursorStops();
assert.equal(actual.length, ROWS + FILE_ROW + 1);
- assert.isTrue(actual[actual.length -1] instanceof AbortStop);
+ assert.isTrue(actual[actual.length - 1] instanceof AbortStop);
});
});
test('adds .hiddenscroll', () => {
_setHiddenScroll(true);
element.displayLine = true;
- assert.include(element.shadowRoot
- .querySelector('.diffContainer').className, 'hiddenscroll');
+ const container = queryAndAssert(element, '.diffContainer');
+ assert.include(container.className, 'hiddenscroll');
});
});
suite('logged in', () => {
- let fakeLineEl;
+ let fakeLineEl: HTMLElement;
setup(() => {
element = basicFixture.instantiate();
element.loggedIn = true;
@@ -571,15 +588,14 @@
classList: {
contains: sinon.stub().returns(true),
},
- };
+ } as unknown as HTMLElement;
});
test('addDraftAtLine', () => {
sinon.stub(element, '_selectLine');
- sinon.stub(element, '_createComment');
+ const createCommentStub = sinon.stub(element, '_createComment');
element.addDraftAtLine(fakeLineEl);
- assert.isTrue(element._createComment
- .calledWithExactly(fakeLineEl, 42));
+ assert.isTrue(createCommentStub.calledWithExactly(fakeLineEl, 42));
});
test('adds long range comment hint', async () => {
@@ -592,23 +608,29 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
+ threadEl.setAttribute('line-num', '1');
threadEl.setAttribute('range', JSON.stringify(range));
threadEl.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(13).fill('text'),
- }];
+ const content = [
+ {
+ a: [],
+ b: [],
+ },
+ {
+ ab: Array(13).fill('text'),
+ },
+ ];
setupSampleDiff({content});
- await new Promise(resolve => afterNextRender(element, resolve));
+ await waitForEventOnce(element, 'render');
element.appendChild(threadEl);
await flush();
- assert.deepEqual(
- element.querySelector('gr-ranged-comment-hint').range, range);
+ const hint = queryAndAssert<GrRangedCommentHint>(
+ element,
+ 'gr-ranged-comment-hint'
+ );
+ assert.deepEqual(hint.range, range);
});
test('no duplicate range hint for same thread', async () => {
@@ -621,19 +643,21 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
+ threadEl.setAttribute('line-num', '1');
threadEl.setAttribute('range', JSON.stringify(range));
threadEl.setAttribute('slot', 'right-1');
const firstHint = document.createElement('gr-ranged-comment-hint');
firstHint.range = range;
- firstHint.setAttribute('threadElRootId', threadEl.rootId);
firstHint.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(13).fill('text'),
- }];
+ const content = [
+ {
+ a: [],
+ b: [],
+ },
+ {
+ ab: Array(13).fill('text'),
+ },
+ ];
setupSampleDiff({content});
element.appendChild(firstHint);
@@ -644,86 +668,97 @@
await flush();
assert.equal(
- element.querySelectorAll('gr-ranged-comment-hint').length, 1);
+ element.querySelectorAll('gr-ranged-comment-hint').length,
+ 1
+ );
});
- test('removes long range comment hint when comment is discarded',
- async () => {
- const range = {
- start_line: 1,
- end_line: 7,
- start_character: 0,
- end_character: 0,
- };
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
- threadEl.setAttribute('range', JSON.stringify(range));
- threadEl.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(8).fill('text'),
- }];
- setupSampleDiff({content});
- element.appendChild(threadEl);
- await flush();
+ test('removes long range comment hint when comment is discarded', async () => {
+ const range = {
+ start_line: 1,
+ end_line: 7,
+ start_character: 0,
+ end_character: 0,
+ };
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('diff-side', 'right');
+ threadEl.setAttribute('line-num', '1');
+ threadEl.setAttribute('range', JSON.stringify(range));
+ threadEl.setAttribute('slot', 'right-1');
+ const content = [
+ {
+ a: [],
+ b: [],
+ },
+ {
+ ab: Array(8).fill('text'),
+ },
+ ];
+ setupSampleDiff({content});
+ element.appendChild(threadEl);
+ await flush();
- threadEl.remove();
- await flush();
+ threadEl.remove();
+ await flush();
- assert.isEmpty(element.querySelectorAll('gr-ranged-comment-hint'));
- });
+ assert.isEmpty(element.querySelectorAll('gr-ranged-comment-hint'));
+ });
suite('change in preferences', () => {
setup(() => {
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
diff_header: [],
intraline_status: 'OK',
change_type: 'MODIFIED',
content: [{skip: 66}],
};
- element.renderDiffTableTask.flush();
+ element.renderDiffTableTask?.flush();
});
test('change in preferences re-renders diff', () => {
- sinon.stub(element, '_renderDiffTable');
+ const stub = sinon.stub(element, '_renderDiffTable');
element.prefs = {
- ...MINIMAL_PREFS, time_format: 'HHMM_12'};
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ ...MINIMAL_PREFS,
+ };
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
});
test('adding/removing property in preferences re-renders diff', () => {
const stub = sinon.stub(element, '_renderDiffTable');
- const newPrefs1 = {...MINIMAL_PREFS,
- line_wrapping: true};
+ const newPrefs1: DiffPreferencesInfo = {
+ ...MINIMAL_PREFS,
+ line_wrapping: true,
+ };
element.prefs = newPrefs1;
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
stub.reset();
const newPrefs2 = {...newPrefs1};
delete newPrefs2.line_wrapping;
element.prefs = newPrefs2;
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
});
- test('change in preferences does not re-renders diff with ' +
- 'noRenderOnPrefsChange', () => {
- sinon.stub(element, '_renderDiffTable');
- element.noRenderOnPrefsChange = true;
- element.prefs = {
- ...MINIMAL_PREFS, time_format: 'HHMM_12'};
- element.renderDiffTableTask.flush();
- assert.isFalse(element._renderDiffTable.called);
- });
+ test(
+ 'change in preferences does not re-renders diff with ' +
+ 'noRenderOnPrefsChange',
+ () => {
+ const stub = sinon.stub(element, '_renderDiffTable');
+ element.noRenderOnPrefsChange = true;
+ element.prefs = {
+ ...MINIMAL_PREFS,
+ context: 12,
+ };
+ element.renderDiffTableTask?.flush();
+ assert.isFalse(stub.called);
+ }
+ );
});
});
@@ -732,8 +767,7 @@
element = basicFixture.instantiate();
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
diff_header: [],
intraline_status: 'OK',
change_type: 'MODIFIED',
@@ -755,11 +789,12 @@
assert.equal(element._diffHeaderItems.length, 1);
flush();
- assert.equal(element.$.diffHeader.textContent.trim(), 'test');
+ const header = queryAndAssert(element, '#diffHeader');
+ assert.equal(header.textContent?.trim(), 'test');
});
test('binary files', () => {
- element.diff.binary = true;
+ element.diff!.binary = true;
assert.equal(element._diffHeaderItems.length, 0);
element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
assert.equal(element._diffHeaderItems.length, 0);
@@ -771,16 +806,17 @@
});
suite('safety and bypass', () => {
- let renderStub;
+ let renderStub: sinon.SinonStub;
setup(() => {
element = basicFixture.instantiate();
- renderStub = sinon.stub(element.$.diffBuilder, 'render').callsFake(
- () => {
- element.$.diffBuilder.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return Promise.resolve({});
- });
+ renderStub = sinon.stub(element.diffBuilder, 'render').callsFake(() => {
+ const diffTable = element.$.diffTable;
+ diffTable.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true})
+ );
+ return Promise.resolve({});
+ });
sinon.stub(element, 'getDiffLength').returns(10000);
element.diff = createDiff();
element.noRenderOnPrefsChange = true;
@@ -838,7 +874,7 @@
assert.equal(element.prefs.context, 3);
assert.equal(element._safetyBypass, -1);
- assert.equal(element.$.diffBuilder.prefs.context, -1);
+ assert.equal(element.diffBuilder.prefs.context, -1);
});
test('toggles collapse context from bypass', async () => {
@@ -851,7 +887,7 @@
assert.equal(element.prefs.context, 3);
assert.isNull(element._safetyBypass);
- assert.equal(element.$.diffBuilder.prefs.context, 3);
+ assert.equal(element.diffBuilder.prefs.context, 3);
});
test('toggles collapse context from pref using default', async () => {
@@ -863,7 +899,7 @@
assert.equal(element.prefs.context, -1);
assert.equal(element._safetyBypass, 10);
- assert.equal(element.$.diffBuilder.prefs.context, 10);
+ assert.equal(element.diffBuilder.prefs.context, 10);
});
});
@@ -874,7 +910,7 @@
test('unsetting', () => {
element.blame = [];
- const setBlameSpy = sinon.spy(element.$.diffBuilder, 'setBlame');
+ const setBlameSpy = sinon.spy(element.diffBuilder, 'setBlame');
element.classList.add('showBlame');
element.blame = null;
assert.isTrue(setBlameSpy.calledWithExactly(null));
@@ -882,7 +918,15 @@
});
test('setting', () => {
- element.blame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ element.blame = [
+ {
+ author: 'test-author',
+ time: 12345,
+ commit_msg: '',
+ id: 'commit id',
+ ranges: [{start: 1, end: 2}],
+ },
+ ];
assert.isTrue(element.classList.contains('showBlame'));
});
});
@@ -891,8 +935,10 @@
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
- const getWarning = element =>
- element.shadowRoot.querySelector('.newlineWarning').textContent;
+ const getWarning = (element: GrDiff) => {
+ const warningElement = queryAndAssert(element, '.newlineWarning');
+ return warningElement.textContent;
+ };
setup(() => {
element = basicFixture.instantiate();
@@ -904,8 +950,9 @@
element.showNewlineWarningLeft = true;
element.showNewlineWarningRight = true;
assert.include(
- getWarning(element),
- NO_NEWLINE_LEFT + ' \u2014 ' + NO_NEWLINE_RIGHT);// \u2014 - '—'
+ getWarning(element),
+ NO_NEWLINE_LEFT + ' \u2014 ' + NO_NEWLINE_RIGHT
+ ); // \u2014 - '—'
});
suite('showNewlineWarningLeft', () => {
@@ -918,11 +965,6 @@
element.showNewlineWarningLeft = false;
assert.notInclude(getWarning(element), NO_NEWLINE_LEFT);
});
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningLeft = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_LEFT);
- });
});
suite('showNewlineWarningRight', () => {
@@ -935,49 +977,25 @@
element.showNewlineWarningRight = false;
assert.notInclude(getWarning(element), NO_NEWLINE_RIGHT);
});
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningRight = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_RIGHT);
- });
});
test('_computeNewlineWarningClass', () => {
const hidden = 'newlineWarning hidden';
const shown = 'newlineWarning';
- assert.equal(element._computeNewlineWarningClass(null, true), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', true), hidden);
- assert.equal(element._computeNewlineWarningClass(null, false), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', false), shown);
- });
-
- test('_prefsEqual', () => {
- element = basicFixture.instantiate();
- assert.isTrue(element._prefsEqual(null, null));
- assert.isTrue(element._prefsEqual({}, {}));
- assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
- assert.isTrue(
- element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
- const somePref = {abc: 'def', p: true};
- assert.isTrue(element._prefsEqual(somePref, somePref));
-
- assert.isFalse(element._prefsEqual({}, null));
- assert.isFalse(element._prefsEqual(null, {}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
+ assert.equal(element._computeNewlineWarningClass(false, true), hidden);
+ assert.equal(element._computeNewlineWarningClass(true, true), hidden);
+ assert.equal(element._computeNewlineWarningClass(false, false), hidden);
+ assert.equal(element._computeNewlineWarningClass(true, false), shown);
});
});
suite('key locations', () => {
- let renderStub;
+ let renderStub: sinon.SinonStub;
setup(() => {
element = basicFixture.instantiate();
- element.prefs = {};
- renderStub = sinon.stub(element.$.diffBuilder, 'render')
- .returns(new Promise(() => {}));
+ element.prefs = {...MINIMAL_PREFS};
+ renderStub = sinon.stub(element.diffBuilder, 'render');
});
test('lineOfInterest is a key location', () => {
@@ -994,7 +1012,7 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
+ threadEl.setAttribute('line-num', '3');
element.appendChild(threadEl);
flush();
@@ -1021,7 +1039,11 @@
});
});
});
- const setupSampleDiff = function(params) {
+ const setupSampleDiff = function (params: {
+ content: DiffContent[];
+ ignore_whitespace?: IgnoreWhitespaceType;
+ binary?: boolean;
+ }) {
const {ignore_whitespace, content} = params;
// binary can't be undefined, use false if not set
const binary = params.binary || false;
@@ -1039,7 +1061,6 @@
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
- theme: 'DEFAULT',
};
element.diff = {
intraline_status: 'OK',
@@ -1059,21 +1080,24 @@
};
test('clear diff table content as soon as diff changes', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- }, {
- b: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ },
+ {
+ b: ['Non eram nescius, Brute, cum, quae summis ingeniis '],
+ },
+ ];
function assertDiffTableWithContent() {
- assert.isTrue(element.$.diffTable.innerText.includes(content[0].a));
+ const diffTable = element.$.diffTable;
+ assert.isTrue(diffTable.innerText.includes(content[0].a?.[0] ?? ''));
}
setupSampleDiff({content});
assertDiffTableWithContent();
- element.diff = {...element.diff};
+ element.diff = {...element.diff!};
// immediately cleaned up
- assert.equal(element.$.diffTable.innerHTML, '');
+ const diffTable = element.$.diffTable;
+ assert.equal(diffTable.innerHTML, '');
element._renderDiffTable();
flush();
// rendered again
@@ -1082,40 +1106,46 @@
suite('selection test', () => {
test('user-select set correctly on side-by-side view', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({content});
flush();
- const diffLine = element.shadowRoot.querySelectorAll('.contentText')[2];
+
+ const diffLine = queryAll<HTMLElement>(element, '.contentText')[2];
assert.equal(getComputedStyle(diffLine).userSelect, 'none');
- // click to mark it as selected
- MockInteractions.tap(diffLine);
+ mouseDown(diffLine);
assert.equal(getComputedStyle(diffLine).userSelect, 'text');
});
test('user-select set correctly on unified view', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({content});
- element.viewMode = 'UNIFIED_DIFF';
+ element.viewMode = DiffViewMode.UNIFIED;
flush();
- const diffLine = element.shadowRoot.querySelectorAll('.contentText')[2];
+ const diffLine = queryAll<HTMLElement>(element, '.contentText')[2];
assert.equal(getComputedStyle(diffLine).userSelect, 'none');
- MockInteractions.tap(diffLine);
+ mouseDown(diffLine);
assert.equal(getComputedStyle(diffLine).userSelect, 'text');
});
});
@@ -1123,71 +1153,87 @@
suite('whitespace changes only message', () => {
test('show the message if ignore_whitespace is criteria matches', () => {
setupSampleDiff({content: [{skip: 100}]});
- assert.isTrue(element.showNoChangeMessage(
+ assert.isTrue(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message for binary files', () => {
setupSampleDiff({content: [{skip: 100}], binary: true});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message if still loading', () => {
setupSampleDiff({content: [{skip: 100}]});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ true,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message if contains valid changes', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({content});
assert.equal(element._diffLength, 3);
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show message if ignore whitespace is disabled', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({ignore_whitespace: 'IGNORE_NONE', content});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
});
@@ -1195,21 +1241,4 @@
const diff = createDiff();
assert.equal(element.getDiffLength(diff), 52);
});
-
- test('_prefsEqual', () => {
- element = basicFixture.instantiate();
- assert.isTrue(element._prefsEqual(null, null));
- assert.isTrue(element._prefsEqual({}, {}));
- assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
- assert.isTrue(element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
- const somePref = {abc: 'def', p: true};
- assert.isTrue(element._prefsEqual(somePref, somePref));
-
- assert.isFalse(element._prefsEqual({}, null));
- assert.isFalse(element._prefsEqual(null, {}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
- });
});
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
deleted file mode 100644
index bb46484..0000000
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../test/common-test-setup-karma.js';
-import {createDiffAppContext} from './gr-diff-app-context-init.js';
-
-suite('gr diff app context initializer tests', () => {
- test('all services initialized and are singletons', () => {
- const appContext = createDiffAppContext();
- Object.keys(appContext).forEach(serviceName => {
- const service = appContext[serviceName];
- assert.isNotNull(service);
- const service2 = appContext[serviceName];
- assert.strictEqual(service, service2);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts
new file mode 100644
index 0000000..84fd859
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2020 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {AppContext} from '../services/app-context';
+import '../test/common-test-setup-karma';
+import {createDiffAppContext} from './gr-diff-app-context-init';
+
+suite('gr diff app context initializer tests', () => {
+ test('all services initialized and are singletons', () => {
+ const appContext: AppContext = createDiffAppContext();
+ for (const serviceName of Object.keys(appContext) as Array<
+ keyof AppContext
+ >) {
+ const service = appContext[serviceName];
+ assert.isNotNull(service);
+ const service2 = appContext[serviceName];
+ assert.strictEqual(service, service2);
+ }
+ });
+});
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index c276f79..a34f880 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -9,14 +9,21 @@
NumericChangeId,
ChangeStatus,
ReviewerState,
+ AccountId,
AccountInfo,
+ GroupInfo,
} from '../../api/rest-api';
import {Model} from '../model';
import {Finalizable} from '../../services/registry';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {define} from '../dependency';
import {select} from '../../utils/observable-util';
-import {ReviewInput, ReviewerInput} from '../../types/common';
+import {
+ ReviewInput,
+ ReviewerInput,
+ AttentionSetInput,
+} from '../../types/common';
+import {accountOrGroupKey} from '../../utils/account-util';
export const bulkActionsModelToken =
define<BulkActionsModel>('bulk-actions-model');
@@ -154,14 +161,15 @@
}
addReviewers(
- changedReviewers: Map<ReviewerState, AccountInfo[]>
+ changedReviewers: Map<ReviewerState, (AccountInfo | GroupInfo)[]>,
+ reason: string
): Promise<Response>[] {
const current = this.subject$.getValue();
const changes = current.selectedChangeNums.map(
changeNum => current.allChanges.get(changeNum)!
);
return changes.map(change => {
- const reviewersNewToChange = [
+ const reviewersNewToChange: ReviewerInput[] = [
ReviewerState.REVIEWER,
ReviewerState.CC,
].flatMap(state =>
@@ -170,8 +178,20 @@
if (reviewersNewToChange.length === 0) {
return Promise.resolve(new Response());
}
+ const attentionSetUpdates: AttentionSetInput[] = reviewersNewToChange
+ .filter(reviewerInput => reviewerInput.state === ReviewerState.REVIEWER)
+ .map(reviewerInput => {
+ return {
+ // TODO: Once Groups are supported, filter them out and only add
+ // Accounts to the attention set, just like gr-reply-dialog.
+ user: reviewerInput.reviewer as AccountId,
+ reason,
+ };
+ });
const reviewInput: ReviewInput = {
reviewers: reviewersNewToChange,
+ ignore_automatic_attention_set_rules: true,
+ add_to_attention_set: attentionSetUpdates,
};
return this.restApiService.saveChangeReview(
change._number,
@@ -242,14 +262,14 @@
private getNewReviewersToChange(
change: ChangeInfo,
state: ReviewerState,
- changedReviewers: Map<ReviewerState, AccountInfo[]>
+ changedReviewers: Map<ReviewerState, (AccountInfo | GroupInfo)[]>
): ReviewerInput[] {
return (
changedReviewers
.get(state)
?.filter(account => !change.reviewers[state]?.includes(account))
.map(account => {
- return {state, reviewer: account._account_id!};
+ return {state, reviewer: accountOrGroupKey(account)};
}) ?? []
);
}
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index 5347b41..84d5c4e 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -7,6 +7,7 @@
import {
createAccountWithIdNameAndEmail,
createChange,
+ createGroupInfo,
createRevisions,
} from '../../test/test-data-generators';
import {
@@ -18,6 +19,7 @@
AccountInfo,
ReviewerState,
AccountId,
+ GroupInfo,
} from '../../api/rest-api';
import {BulkActionsModel, LoadingState} from './bulk-actions-model';
import {getAppContext} from '../../services/app-context';
@@ -200,6 +202,7 @@
createAccountWithIdNameAndEmail(0),
createAccountWithIdNameAndEmail(1),
];
+ const groups: GroupInfo[] = [createGroupInfo('groupId')];
const changes: ChangeInfo[] = [
{
...createChange(),
@@ -234,21 +237,49 @@
test('adds reviewers/cc only to changes that need it', async () => {
bulkActionsModel.addReviewers(
new Map([
- [ReviewerState.REVIEWER, [accounts[0]]],
+ [ReviewerState.REVIEWER, [accounts[0], groups[0]]],
[ReviewerState.CC, [accounts[1]]],
- ])
+ ]),
+ '<GERRIT_ACCOUNT_12345> replied on the change'
);
- // changes[0] is not updated since it already has the reviewer & CC
- assert.isTrue(saveChangeReviewStub.calledOnce);
+ assert.isTrue(saveChangeReviewStub.calledTwice);
+ // changes[0] only adds the group since it already has the other
+ // reviewer/CCs
assert.sameDeepOrderedMembers(saveChangeReviewStub.firstCall.args, [
+ changes[0]._number,
+ 'current',
+ {
+ reviewers: [{reviewer: groups[0].id, state: ReviewerState.REVIEWER}],
+ ignore_automatic_attention_set_rules: true,
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_12345> replied on the change',
+ user: groups[0].id,
+ },
+ ],
+ },
+ ]);
+ assert.sameDeepOrderedMembers(saveChangeReviewStub.secondCall.args, [
changes[1]._number,
'current',
{
reviewers: [
{reviewer: accounts[0]._account_id, state: ReviewerState.REVIEWER},
+ {reviewer: groups[0].id, state: ReviewerState.REVIEWER},
{reviewer: accounts[1]._account_id, state: ReviewerState.CC},
],
+ ignore_automatic_attention_set_rules: true,
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_12345> replied on the change',
+ user: accounts[0]._account_id,
+ },
+ {
+ reason: '<GERRIT_ACCOUNT_12345> replied on the change',
+ user: groups[0].id,
+ },
+ ],
},
]);
});
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index a1b732f..44d63d4b 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -28,7 +28,6 @@
export enum KnownExperimentId {
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
- SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
BULK_ACTIONS = 'UiFeature__bulk_actions_dashboard',
DIFF_RENDERING_LIT = 'UiFeature__diff_rendering_lit',
}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
index 5f77e8a..028b2af 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -62,7 +62,7 @@
static CREDS_EXPIRED_MSG = 'Credentials expired.';
- private authCheckPromise?: Promise<Response>;
+ private authCheckPromise?: Promise<boolean>;
private _last_auth_check_time: number = Date.now();
@@ -100,37 +100,37 @@
Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS
) {
// Refetch after last check expired
- this.authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
+ this.authCheckPromise = fetch(`${this.baseUrl}/auth-check`)
+ .then(res => {
+ // Make a call that requires loading the body of the request. This makes it so that the browser
+ // can close the request even though callers of this method might only ever read headers.
+ // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
+ try {
+ res.clone().text();
+ } catch {
+ // Ignore error
+ }
+
+ // auth-check will return 204 if authed
+ // treat the rest as unauthed
+ if (res.status === 204) {
+ this._setStatus(Auth.STATUS.AUTHED);
+ return true;
+ } else {
+ this._setStatus(Auth.STATUS.NOT_AUTHED);
+ return false;
+ }
+ })
+ .catch(() => {
+ this._setStatus(AuthStatus.ERROR);
+ // Reset authCheckPromise to avoid caching the failed promise
+ this.authCheckPromise = undefined;
+ return false;
+ });
this._last_auth_check_time = Date.now();
}
- return this.authCheckPromise
- .then(res => {
- // Make a call that requires loading the body of the request. This makes it so that the browser
- // can close the request even though callers of this method might only ever read headers.
- // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
- try {
- res.clone().text();
- } catch {
- // Ignore error
- }
-
- // auth-check will return 204 if authed
- // treat the rest as unauthed
- if (res.status === 204) {
- this._setStatus(Auth.STATUS.AUTHED);
- return true;
- } else {
- this._setStatus(Auth.STATUS.NOT_AUTHED);
- return false;
- }
- })
- .catch(() => {
- this._setStatus(AuthStatus.ERROR);
- // Reset authCheckPromise to avoid caching the failed promise
- this.authCheckPromise = undefined;
- return false;
- });
+ return this.authCheckPromise;
}
clearCache() {
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 2b4fc60..3193833 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -2702,20 +2702,22 @@
return Promise.all([promiseA, promiseB]).then(results => {
// Sometimes the server doesn't send back the content type.
- const baseImage: Base64ImageFile | null = results[0]
- ? {
- ...results[0],
- _expectedType: diff.meta_a.content_type,
- _name: diff.meta_a.name,
- }
- : null;
- const revisionImage: Base64ImageFile | null = results[1]
- ? {
- ...results[1],
- _expectedType: diff.meta_b.content_type,
- _name: diff.meta_b.name,
- }
- : null;
+ const baseImage: Base64ImageFile | null =
+ results[0] && diff.meta_a
+ ? {
+ ...results[0],
+ _expectedType: diff.meta_a.content_type,
+ _name: diff.meta_a.name,
+ }
+ : null;
+ const revisionImage: Base64ImageFile | null =
+ results[1] && diff.meta_b
+ ? {
+ ...results[1],
+ _expectedType: diff.meta_b.content_type,
+ _name: diff.meta_b.name,
+ }
+ : null;
const imagesForDiff: ImagesForDiff = {baseImage, revisionImage};
return imagesForDiff;
});
diff --git a/polygerrit-ui/app/services/registry.ts b/polygerrit-ui/app/services/registry.ts
index e7de1ef..74b6997 100644
--- a/polygerrit-ui/app/services/registry.ts
+++ b/polygerrit-ui/app/services/registry.ts
@@ -73,7 +73,7 @@
initializing = true;
initialized.set(name, factory(context));
} catch (e) {
- console.error(`Failed to initialize ${name}`, e);
+ console.error(`Failed to initialize ${String(name)}`, e);
} finally {
initializing = false;
}
diff --git a/polygerrit-ui/app/test/mocks/comment-api.js b/polygerrit-ui/app/test/mocks/comment-api.js
deleted file mode 100644
index fc4599d..0000000
--- a/polygerrit-ui/app/test/mocks/comment-api.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-
-/**
- * This is an "abstract" class for tests. The descendant must define a template
- * for this element and a tagName - see createCommentApiMockWithTemplateElement below
- */
-class CommentApiMock extends LegacyElementMixin(PolymerElement) {
- static get properties() {
- return {
- _changeComments: Object,
- };
- }
-}
-
-/**
- * Creates a new element which is descendant of CommentApiMock with specified
- * template. Additionally, the method registers a tagName for this element.
- *
- * Each tagName must be a unique accross all tests.
- */
-export function createCommentApiMockWithTemplateElement(tagName, template) {
- const elementClass = class extends CommentApiMock {
- static get is() { return tagName; }
-
- static get template() { return template; }
- };
- customElements.define(tagName, elementClass);
- return elementClass;
-}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 829a36b..8e8fe42 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -469,6 +469,24 @@
};
}
+export function createEmptyDiff(): DiffInfo {
+ return {
+ meta_a: {
+ name: 'empty-left.txt',
+ content_type: 'text/plain',
+ lines: 1,
+ },
+ meta_b: {
+ name: 'empty-right.txt',
+ content_type: 'text/plain',
+ lines: 1,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [],
+ };
+}
+
export function createDiff(): DiffInfo {
return {
meta_a: {
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 985bec1..ea7865e 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -27,7 +27,7 @@
import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
import {queryAndAssert, query} from '../utils/common-util';
import {FlagsService} from '../services/flags/flags';
-import {Key, Modifier} from '../utils/dom-util';
+import {afterNextRender, Key, Modifier} from '../utils/dom-util';
import {Observable} from 'rxjs';
import {filter, take, timeout} from 'rxjs/operators';
import {HighlightService} from '../services/highlight/highlight-service';
@@ -224,6 +224,10 @@
return waitUntil(() => stub.called, `${name} was not called`);
}
+export async function nextRender() {
+ return new Promise(resolve => afterNextRender(resolve));
+}
+
/**
* Subscribes to the observable and resolves once it emits a matching value.
* Usage:
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 562d47f..7ad656d 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -48,9 +48,9 @@
export interface DiffInfo extends DiffInfoApi {
/** Meta information about the file on side A as a DiffFileMetaInfo entity. */
- meta_a: DiffFileMetaInfo;
+ meta_a?: DiffFileMetaInfo;
/** Meta information about the file on side B as a DiffFileMetaInfo entity. */
- meta_b: DiffFileMetaInfo;
+ meta_b?: DiffFileMetaInfo;
/**
* Links to the file diff in external sites as a list of DiffWebLinkInfo
diff --git a/polygerrit-ui/app/utils/account-util.ts b/polygerrit-ui/app/utils/account-util.ts
index b7cc77b..b6018ba 100644
--- a/polygerrit-ui/app/utils/account-util.ts
+++ b/polygerrit-ui/app/utils/account-util.ts
@@ -35,7 +35,7 @@
export const ACCOUNT_TEMPLATE_REGEX = '<GERRIT_ACCOUNT_(\\d+)>';
export function accountKey(account: AccountInfo): AccountId | EmailAddress {
- if (account._account_id) return account._account_id;
+ if (account._account_id !== undefined) return account._account_id;
if (account.email) return account.email;
throw new Error('Account has neither _account_id nor email.');
}
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 6ccf770..95b753c 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -159,7 +159,8 @@
/**
* Returns the elements that are present in every sub-array. If a compareBy
- * predicate is passed in, it will be used instead of strict equality.
+ * predicate is passed in, it will be used instead of strict equality. A new
+ * array is always returned even if there is already just a single array.
*/
export function intersection<T>(
arrays: T[][],
@@ -171,6 +172,9 @@
if (arrays.length === 0) {
return [];
}
+ if (arrays.length === 1) {
+ return [...arrays[0]];
+ }
return arrays.reduce((result, array) =>
result.filter(t => array.find(u => compareBy(t, u)))
);
diff --git a/polygerrit-ui/app/utils/common-util_test.ts b/polygerrit-ui/app/utils/common-util_test.ts
index 8cc523a..0adfaa6 100644
--- a/polygerrit-ui/app/utils/common-util_test.ts
+++ b/polygerrit-ui/app/utils/common-util_test.ts
@@ -75,8 +75,11 @@
});
test('intersections', () => {
+ const arrayWithValues = [1, 2, 3];
assert.sameDeepMembers(intersection([]), []);
- assert.sameDeepMembers(intersection([[1, 2, 3]]), [1, 2, 3]);
+ assert.sameDeepMembers(intersection([arrayWithValues]), arrayWithValues);
+ // a new array is returned even if a single array is provided.
+ assert.notStrictEqual(intersection([arrayWithValues]), arrayWithValues);
assert.sameDeepMembers(
intersection([
[1, 2, 3],
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 16e0586..f2e0994 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -505,3 +505,14 @@
});
obs.observe(el);
}
+
+/**
+ * Mimics a Polymer utility. `requestAnimationFrame` is called before the next
+ * browser paint. An additional `setTimeout` ensures that the paint has
+ * actually happened.
+ */
+export function afterNextRender(callback: (value?: unknown) => void) {
+ requestAnimationFrame(() => {
+ setTimeout(callback);
+ });
+}
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 418adbd..e624cef 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -32,7 +32,7 @@
);
}
-type HTMLElementEventDetailType<K extends keyof HTMLElementEventMap> =
+export type HTMLElementEventDetailType<K extends keyof HTMLElementEventMap> =
HTMLElementEventMap[K] extends CustomEvent<infer DT>
? unknown extends DT
? never
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index f5703f4..2b6f700 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -21,7 +21,6 @@
SubmitRequirementStatus,
LabelNameToValuesMap,
} from '../api/rest-api';
-import {FlagsService, KnownExperimentId} from '../services/flags/flags';
import {
AccountInfo,
ApprovalInfo,
@@ -421,16 +420,3 @@
label => !labelAssociatedWithSubmitReqs.includes(label)
);
}
-
-export function showNewSubmitRequirements(
- flagsService: FlagsService,
- change?: ParsedChangeInfo | ChangeInfo
-) {
- const isSubmitRequirementsUiEnabled = flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
- if (!isSubmitRequirementsUiEnabled) return false;
- if ((getRequirements(change) ?? []).length === 0) return false;
-
- return true;
-}
diff --git a/tools/js/template_checker.bzl b/tools/js/template_checker.bzl
index da77234..6c645f4 100644
--- a/tools/js/template_checker.bzl
+++ b/tools/js/template_checker.bzl
@@ -123,9 +123,7 @@
)
# Pack all transformed files. Later files can be materialized in the
- # WORKSPACE/polygerrit-ui/app/tmpl_out dir. The following command do it
- # automatically
- # npm run polytest:dev
+ # WORKSPACE/polygerrit-ui/app/tmpl_out dir.
pkg_tar(
name = name + "_tar",
srcs = generated_dev_files,