Merge "Add Optional type JSON (de)serialization unit tests"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 05e7341..da15775 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6641,7 +6641,8 @@
by one of the following REST endpoints: link:#create-change[Create
Change], link:#create-merge-patch-set-for-change[Create Merge Patch Set
For Change], link:#cherry-pick[Cherry Pick Revision],
-link:rest-api-project.html#cherry-pick-commit[Cherry Pick Commit]
+link:rest-api-project.html#cherry-pick-commit[Cherry Pick Commit],
+link:#rebase-change[Rebase Change]
|==================================
[[change-input]]
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 91c3f70..4235821 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -36,7 +36,6 @@
public static class Builder<T> {
private final List<FieldDef<T, ?>> fields = new ArrayList<>();
- private boolean useLegacyNumericFields;
public Builder<T> add(Schema<T> schema) {
this.fields.addAll(schema.getFields().values());
@@ -55,13 +54,8 @@
return this;
}
- public Builder<T> legacyNumericFields(boolean useLegacyNumericFields) {
- this.useLegacyNumericFields = useLegacyNumericFields;
- return this;
- }
-
public Schema<T> build() {
- return new Schema<>(useLegacyNumericFields, ImmutableList.copyOf(fields));
+ return new Schema<>(ImmutableList.copyOf(fields));
}
}
@@ -90,15 +84,14 @@
private final ImmutableMap<String, FieldDef<T, ?>> fields;
private final ImmutableMap<String, FieldDef<T, ?>> storedFields;
- private final boolean useLegacyNumericFields;
private int version;
- public Schema(boolean useLegacyNumericFields, Iterable<FieldDef<T, ?>> fields) {
- this(0, useLegacyNumericFields, fields);
+ public Schema(Iterable<FieldDef<T, ?>> fields) {
+ this(0, fields);
}
- public Schema(int version, boolean useLegacyNumericFields, Iterable<FieldDef<T, ?>> fields) {
+ public Schema(int version, Iterable<FieldDef<T, ?>> fields) {
this.version = version;
ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
ImmutableMap.Builder<String, FieldDef<T, ?>> sb = ImmutableMap.builder();
@@ -110,17 +103,12 @@
}
this.fields = b.build();
this.storedFields = sb.build();
- this.useLegacyNumericFields = useLegacyNumericFields;
}
public final int getVersion() {
return version;
}
- public final boolean useLegacyNumericFields() {
- return useLegacyNumericFields;
- }
-
/**
* Get all fields in this schema.
*
diff --git a/java/com/google/gerrit/index/SchemaUtil.java b/java/com/google/gerrit/index/SchemaUtil.java
index 9599d6a..96fe4fc 100644
--- a/java/com/google/gerrit/index/SchemaUtil.java
+++ b/java/com/google/gerrit/index/SchemaUtil.java
@@ -67,30 +67,23 @@
}
public static <V> Schema<V> schema(Collection<FieldDef<V, ?>> fields) {
- return new Schema<>(true, ImmutableList.copyOf(fields));
+ return new Schema<>(ImmutableList.copyOf(fields));
}
- public static <V> Schema<V> schema(Schema<V> schema, boolean useLegacyNumericFields) {
- return new Schema<>(
- useLegacyNumericFields,
- new ImmutableList.Builder<FieldDef<V, ?>>().addAll(schema.getFields().values()).build());
+ @SafeVarargs
+ public static <V> Schema<V> schema(FieldDef<V, ?>... fields) {
+ return schema(ImmutableList.copyOf(fields));
}
@SafeVarargs
public static <V> Schema<V> schema(Schema<V> schema, FieldDef<V, ?>... moreFields) {
return new Schema<>(
- true,
new ImmutableList.Builder<FieldDef<V, ?>>()
.addAll(schema.getFields().values())
.addAll(ImmutableList.copyOf(moreFields))
.build());
}
- @SafeVarargs
- public static <V> Schema<V> schema(FieldDef<V, ?>... fields) {
- return new Schema<>(true, ImmutableList.copyOf(fields));
- }
-
public static Set<String> getPersonParts(PersonIdent person) {
if (person == null) {
return ImmutableSet.of();
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 988d6fb..f8256bb 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -66,8 +66,6 @@
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.IntPoint;
-import org.apache.lucene.document.LegacyIntField;
-import org.apache.lucene.document.LegacyLongField;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
@@ -88,7 +86,6 @@
import org.apache.lucene.store.Directory;
/** Basic Lucene index implementation. */
-@SuppressWarnings("deprecation")
public abstract class AbstractLuceneIndex<K, V> implements Index<K, V> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -348,13 +345,9 @@
if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
for (Object value : values.getValues()) {
Integer intValue = (Integer) value;
- if (schema.useLegacyNumericFields()) {
- doc.add(new LegacyIntField(name, intValue, store));
- } else {
- doc.add(new IntPoint(name, intValue));
- if (store == Store.YES) {
- doc.add(new StoredField(name, intValue));
- }
+ doc.add(new IntPoint(name, intValue));
+ if (store == Store.YES) {
+ doc.add(new StoredField(name, intValue));
}
}
} else if (type == FieldType.LONG) {
@@ -383,13 +376,9 @@
}
private void addLongField(Document doc, String name, Store store, Long longValue) {
- if (schema.useLegacyNumericFields()) {
- doc.add(new LegacyLongField(name, longValue, store));
- } else {
- doc.add(new LongPoint(name, longValue));
- if (store == Store.YES) {
- doc.add(new StoredField(name, longValue));
- }
+ doc.add(new LongPoint(name, longValue));
+ if (store == Store.YES) {
+ doc.add(new StoredField(name, longValue));
}
}
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 475dac4..661c0b0 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -15,8 +15,7 @@
package com.google.gerrit.lucene;
import static com.google.common.collect.Iterables.getOnlyElement;
-import static com.google.gerrit.lucene.LuceneChangeIndex.ID2_SORT_FIELD;
-import static com.google.gerrit.lucene.LuceneChangeIndex.ID_SORT_FIELD;
+import static com.google.gerrit.lucene.LuceneChangeIndex.ID_STR_SORT_FIELD;
import static com.google.gerrit.lucene.LuceneChangeIndex.MERGED_ON_SORT_FIELD;
import static com.google.gerrit.lucene.LuceneChangeIndex.UPDATED_SORT_FIELD;
import static com.google.gerrit.server.index.change.ChangeSchemaDefinitions.NAME;
@@ -120,12 +119,9 @@
void add(Document doc, Values<ChangeData> values) {
// Add separate DocValues fields for those fields needed for sorting.
FieldDef<ChangeData, ?> f = values.getField();
- if (f == ChangeField.LEGACY_ID) {
- int v = (Integer) getOnlyElement(values.getValues());
- doc.add(new NumericDocValuesField(ID_SORT_FIELD, v));
- } else if (f == ChangeField.LEGACY_ID_STR) {
+ if (f == ChangeField.LEGACY_ID_STR) {
String v = (String) getOnlyElement(values.getValues());
- doc.add(new NumericDocValuesField(ID2_SORT_FIELD, Integer.valueOf(v)));
+ doc.add(new NumericDocValuesField(ID_STR_SORT_FIELD, Integer.valueOf(v)));
} else if (f == ChangeField.UPDATED) {
long t = ((Timestamp) getOnlyElement(values.getValues())).getTime();
doc.add(new NumericDocValuesField(UPDATED_SORT_FIELD, t));
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index c4a5240..934b27f 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -137,7 +137,7 @@
@Override
public void replace(AccountState as) {
try {
- replace(idTerm(getSchema().useLegacyNumericFields(), as), toDocument(as)).get();
+ replace(idTerm(getSchema().hasField(ID), as), toDocument(as)).get();
} catch (ExecutionException | InterruptedException e) {
throw new StorageException(e);
}
@@ -155,7 +155,7 @@
@Override
public void delete(Account.Id key) {
try {
- delete(idTerm(getSchema().useLegacyNumericFields(), key)).get();
+ delete(idTerm(getSchema().hasField(ID), key)).get();
} catch (ExecutionException | InterruptedException e) {
throw new StorageException(e);
}
@@ -164,15 +164,14 @@
@Override
public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
throws QueryParseException {
- queryBuilder.getSchema().useLegacyNumericFields();
return new LuceneQuerySource(
- opts.filterFields(o -> IndexUtils.accountFields(o, getSchema().useLegacyNumericFields())),
+ opts.filterFields(o -> IndexUtils.accountFields(o, getSchema().hasField(ID))),
queryBuilder.toQuery(p),
getSort());
}
private Sort getSort() {
- String idSortField = getSchema().useLegacyNumericFields() ? ID_SORT_FIELD : ID2_SORT_FIELD;
+ String idSortField = getSchema().hasField(ID) ? ID_SORT_FIELD : ID2_SORT_FIELD;
return new Sort(
new SortField(FULL_NAME_SORT_FIELD, SortField.Type.STRING, false),
new SortField(EMAIL_SORT_FIELD, SortField.Type.STRING, false),
@@ -181,10 +180,10 @@
@Override
protected AccountState fromDocument(Document doc) {
- FieldDef<AccountState, ?> idField = getSchema().useLegacyNumericFields() ? ID : ID_STR;
+ FieldDef<AccountState, ?> idField = getSchema().hasField(ID) ? ID : ID_STR;
Account.Id id =
Account.id(
- getSchema().useLegacyNumericFields()
+ getSchema().hasField(ID)
? doc.getField(idField.getName()).numericValue().intValue()
: Integer.valueOf(doc.getField(idField.getName()).stringValue()));
// Use the AccountCache rather than depending on any stored fields in the document (of which
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 9ea9d2e..cf176ee 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -17,7 +17,6 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
-import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
@@ -100,30 +99,39 @@
static final String UPDATED_SORT_FIELD = sortFieldName(ChangeField.UPDATED);
static final String MERGED_ON_SORT_FIELD = sortFieldName(ChangeField.MERGED_ON);
- static final String ID_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID);
- static final String ID2_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID_STR);
+ static final String ID_STR_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID_STR);
private static final String CHANGES = "changes";
private static final String CHANGES_OPEN = "open";
private static final String CHANGES_CLOSED = "closed";
private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
- @FunctionalInterface
- interface IdTerm {
- Term get(String name, int id);
+ /*
+ @FunctionalInterface
+ interface IdTerm {
+ Term get(String name, int id);
+ }
+
+ static Term idTerm(IdTerm idTerm, FieldDef<ChangeData, ?> idField, ChangeData cd) {
+ return idTerm(idTerm, idField, cd.getId());
+ }
+
+ static Term idTerm(IdTerm idTerm, FieldDef<ChangeData, ?> idField, Change.Id id) {
+ return idTerm.get(idField.getName(), id.get());
+ }
+
+ @FunctionalInterface
+ interface ChangeIdExtractor {
+ Change.Id extract(IndexableField f);
+ }
+ */
+
+ static Term idTerm(ChangeData cd) {
+ return idTerm(cd.getId());
}
- static Term idTerm(IdTerm idTerm, FieldDef<ChangeData, ?> idField, ChangeData cd) {
- return idTerm(idTerm, idField, cd.getId());
- }
-
- static Term idTerm(IdTerm idTerm, FieldDef<ChangeData, ?> idField, Change.Id id) {
- return idTerm.get(idField.getName(), id.get());
- }
-
- @FunctionalInterface
- interface ChangeIdExtractor {
- Change.Id extract(IndexableField f);
+ static Term idTerm(Change.Id id) {
+ return QueryBuilder.stringTerm(LEGACY_ID_STR.getName(), Integer.toString(id.get()));
}
private final ListeningExecutorService executor;
@@ -132,12 +140,6 @@
private final QueryBuilder<ChangeData> queryBuilder;
private final ChangeSubIndex openIndex;
private final ChangeSubIndex closedIndex;
-
- // TODO(davido): Remove the below fields when support for legacy numeric fields is removed.
- private final FieldDef<ChangeData, ?> idField;
- private final String idSortFieldName;
- private final IdTerm idTerm;
- private final ChangeIdExtractor extractor;
private final ImmutableSet<String> skipFields;
@Inject
@@ -205,20 +207,6 @@
searcherFactory,
autoFlush);
}
-
- idField = this.schema.useLegacyNumericFields() ? LEGACY_ID : LEGACY_ID_STR;
- idSortFieldName = schema.useLegacyNumericFields() ? ID_SORT_FIELD : ID2_SORT_FIELD;
- idTerm =
- (name, id) ->
- this.schema.useLegacyNumericFields()
- ? QueryBuilder.intTerm(name, id)
- : QueryBuilder.stringTerm(name, Integer.toString(id));
- extractor =
- (f) ->
- Change.id(
- this.schema.useLegacyNumericFields()
- ? f.numericValue().intValue()
- : Integer.valueOf(f.stringValue()));
}
@Override
@@ -237,7 +225,7 @@
@Override
public void replace(ChangeData cd) {
- Term id = LuceneChangeIndex.idTerm(idTerm, idField, cd);
+ Term id = LuceneChangeIndex.idTerm(cd);
// toDocument is essentially static and doesn't depend on the specific
// sub-index, so just pick one.
Document doc = openIndex.toDocument(cd);
@@ -270,9 +258,9 @@
@Override
public void delete(Change.Id changeId) {
- Term id = LuceneChangeIndex.idTerm(idTerm, idField, changeId);
+ Term idTerm = LuceneChangeIndex.idTerm(changeId);
try {
- Futures.allAsList(openIndex.delete(id), closedIndex.delete(id)).get();
+ Futures.allAsList(openIndex.delete(idTerm), closedIndex.delete(idTerm)).get();
} catch (ExecutionException | InterruptedException e) {
throw new StorageException(e);
}
@@ -309,7 +297,7 @@
return new Sort(
new SortField(UPDATED_SORT_FIELD, SortField.Type.LONG, true),
new SortField(MERGED_ON_SORT_FIELD, SortField.Type.LONG, true),
- new SortField(idSortFieldName, SortField.Type.LONG, true));
+ new SortField(ID_STR_SORT_FIELD, SortField.Type.LONG, true));
}
private class QuerySource implements ChangeDataSource {
@@ -357,7 +345,7 @@
throw new StorageException("interrupted");
}
- final Set<String> fields = IndexUtils.changeFields(opts, schema.useLegacyNumericFields());
+ final Set<String> fields = IndexUtils.changeFields(opts);
return new ChangeDataResults(
executor.submit(
new Callable<List<Document>>() {
@@ -378,7 +366,7 @@
public ResultSet<FieldBundle> readRaw() {
List<Document> documents;
try {
- documents = doRead(IndexUtils.changeFields(opts, schema.useLegacyNumericFields()));
+ documents = doRead(IndexUtils.changeFields(opts));
} catch (IOException e) {
throw new StorageException(e);
}
@@ -457,7 +445,7 @@
ImmutableList.Builder<ChangeData> result =
ImmutableList.builderWithExpectedSize(docs.size());
for (Document doc : docs) {
- result.add(toChangeData(fields(doc, fields), fields, idField.getName()));
+ result.add(toChangeData(fields(doc, fields), fields, LEGACY_ID_STR.getName()));
}
return result.build();
} catch (InterruptedException e) {
@@ -499,9 +487,10 @@
} else {
IndexableField f = Iterables.getFirst(doc.get(idFieldName), null);
+ Change.Id id = Change.id(Integer.valueOf(f.stringValue()));
// IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
IndexableField project = doc.get(PROJECT.getName()).iterator().next();
- cd = changeDataFactory.create(Project.nameKey(project.stringValue()), extractor.extract(f));
+ cd = changeDataFactory.create(Project.nameKey(project.stringValue()), id);
}
for (FieldDef<ChangeData, ?> field : getSchema().getFields().values()) {
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index e1b56c6..bd34743 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -15,6 +15,7 @@
package com.google.gerrit.lucene;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.lucene.search.BooleanClause.Occur.MUST;
import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
@@ -39,36 +40,18 @@
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.LegacyNumericRangeQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.RegexpQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.BytesRefBuilder;
-import org.apache.lucene.util.LegacyNumericUtils;
-@SuppressWarnings("deprecation")
public class QueryBuilder<V> {
- @FunctionalInterface
- static interface IntTermQuery {
- Query get(String name, int value);
- }
-
- @FunctionalInterface
- static interface IntRangeQuery {
- Query get(String name, int min, int max);
- }
-
- @FunctionalInterface
- static interface LongRangeQuery {
- Query get(String name, long min, long max);
- }
-
- static Term intTerm(String name, int value) {
- BytesRefBuilder builder = new BytesRefBuilder();
- LegacyNumericUtils.intToPrefixCoded(value, 0, builder);
- return new Term(name, builder.get());
+ /** @param name field name qparam i key value */
+ static Term intTerm(String name, int i) {
+ checkState(false, "Lucene index implementation removed legacy numeric type");
+ return null;
}
static Term stringTerm(String name, String value) {
@@ -84,29 +67,9 @@
private final Schema<V> schema;
private final org.apache.lucene.util.QueryBuilder queryBuilder;
- // TODO(davido): Remove the below fields when support for legacy numeric fields is removed.
- private final IntTermQuery intTermQuery;
- private final IntRangeQuery intRangeTermQuery;
- private final LongRangeQuery longRangeQuery;
-
public QueryBuilder(Schema<V> schema, Analyzer analyzer) {
this.schema = schema;
queryBuilder = new org.apache.lucene.util.QueryBuilder(analyzer);
- intTermQuery =
- (name, value) ->
- this.schema.useLegacyNumericFields()
- ? new TermQuery(intTerm(name, value))
- : intPoint(name, value);
- intRangeTermQuery =
- (name, min, max) ->
- this.schema.useLegacyNumericFields()
- ? LegacyNumericRangeQuery.newIntRange(name, min, max, true, true)
- : IntPoint.newRangeQuery(name, min, max);
- longRangeQuery =
- (name, min, max) ->
- this.schema.useLegacyNumericFields()
- ? LegacyNumericRangeQuery.newLongRange(name, min, max, true, true)
- : LongPoint.newRangeQuery(name, min, max);
}
public Query toQuery(Predicate<V> p) throws QueryParseException {
@@ -209,7 +172,7 @@
} catch (NumberFormatException e) {
throw new QueryParseException("not an integer: " + p.getValue(), e);
}
- return intTermQuery.get(p.getField().getName(), value);
+ return intPoint(p.getField().getName(), value);
}
private Query intRangeQuery(IndexPredicate<V> p) throws QueryParseException {
@@ -220,9 +183,9 @@
int maximum = r.getMaximumValue();
if (minimum == maximum) {
// Just fall back to a standard integer query.
- return intTermQuery.get(name, minimum);
+ return intPoint(name, minimum);
}
- return intRangeTermQuery.get(name, minimum, maximum);
+ return IntPoint.newRangeQuery(name, minimum, maximum);
}
throw new QueryParseException("not an integer range: " + p);
}
@@ -230,7 +193,7 @@
private Query timestampQuery(IndexPredicate<V> p) throws QueryParseException {
if (p instanceof TimestampRangePredicate) {
TimestampRangePredicate<V> r = (TimestampRangePredicate<V>) p;
- return longRangeQuery.get(
+ return LongPoint.newRangeQuery(
r.getField().getName(),
r.getMinTimestamp().toEpochMilli(),
r.getMaxTimestamp().toEpochMilli());
@@ -240,7 +203,7 @@
private Query notTimestamp(TimestampRangePredicate<V> r) throws QueryParseException {
if (r.getMinTimestamp().toEpochMilli() == 0) {
- return longRangeQuery.get(
+ return LongPoint.newRangeQuery(
r.getField().getName(), r.getMaxTimestamp().toEpochMilli(), Long.MAX_VALUE);
}
throw new QueryParseException("cannot negate: " + r);
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index 29bd045..59b32c2 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -40,10 +40,7 @@
private final ChangeCleanupConfig cfg;
private final Provider<ChangeQueryProcessor> queryProvider;
- // Provider is needed, because AbandonUtil is singleton, but ChangeQueryBuilder accesses
- // index collection, that is only provided when multiversion index module is started.
- // TODO(davido); Remove provider again, when support for legacy numeric fields is removed.
- private final Provider<ChangeQueryBuilder> queryBuilderProvider;
+ private final ChangeQueryBuilder queryBuilder;
private final BatchAbandon batchAbandon;
private final InternalUser internalUser;
@@ -52,11 +49,11 @@
ChangeCleanupConfig cfg,
InternalUser.Factory internalUserFactory,
Provider<ChangeQueryProcessor> queryProvider,
- Provider<ChangeQueryBuilder> queryBuilderProvider,
+ ChangeQueryBuilder queryBuilder,
BatchAbandon batchAbandon) {
this.cfg = cfg;
this.queryProvider = queryProvider;
- this.queryBuilderProvider = queryBuilderProvider;
+ this.queryBuilder = queryBuilder;
this.batchAbandon = batchAbandon;
internalUser = internalUserFactory.create();
}
@@ -74,11 +71,7 @@
}
List<ChangeData> changesToAbandon =
- queryProvider
- .get()
- .enforceVisibility(false)
- .query(queryBuilderProvider.get().parse(query))
- .entities();
+ queryProvider.get().enforceVisibility(false).query(queryBuilder.parse(query)).entities();
ImmutableListMultimap.Builder<Project.NameKey, ChangeData> builder =
ImmutableListMultimap.builder();
for (ChangeData cd : changesToAbandon) {
@@ -118,7 +111,7 @@
queryProvider
.get()
.enforceVisibility(false)
- .query(queryBuilderProvider.get().parse(newQuery))
+ .query(queryBuilder.parse(newQuery))
.entities();
if (!changesToAbandon.isEmpty()) {
validChanges.add(cd);
diff --git a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
index a980c32..1cf31c1 100644
--- a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
@@ -16,13 +16,11 @@
import static java.util.Objects.requireNonNull;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.mail.send.AddToAttentionSetSender;
-import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -31,18 +29,14 @@
import com.google.gerrit.server.util.AttentionSetEmail;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
/** Add a specified user to the attention set. */
public class AddToAttentionSetOp implements BatchUpdateOp {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
public interface Factory {
AddToAttentionSetOp create(Account.Id attentionUserId, String reason, boolean notify);
}
private final ChangeData.Factory changeDataFactory;
- private final MessageIdGenerator messageIdGenerator;
private final AddToAttentionSetSender.Factory addToAttentionSetSender;
private final AttentionSetEmail.Factory attentionSetEmailFactory;
@@ -63,14 +57,12 @@
AddToAttentionSetOp(
ChangeData.Factory changeDataFactory,
AddToAttentionSetSender.Factory addToAttentionSetSender,
- MessageIdGenerator messageIdGenerator,
AttentionSetEmail.Factory attentionSetEmailFactory,
@Assisted Account.Id attentionUserId,
@Assisted String reason,
@Assisted boolean notify) {
this.changeDataFactory = changeDataFactory;
this.addToAttentionSetSender = addToAttentionSetSender;
- this.messageIdGenerator = messageIdGenerator;
this.attentionSetEmailFactory = attentionSetEmailFactory;
this.attentionUserId = requireNonNull(attentionUserId, "user");
@@ -105,18 +97,13 @@
if (!notify) {
return;
}
- try {
- attentionSetEmailFactory
- .create(
- addToAttentionSetSender.create(ctx.getProject(), change.getId()),
- ctx,
- change,
- reason,
- messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()),
- attentionUserId)
- .sendAsync();
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(e.getMessage(), change.getId());
- }
+ attentionSetEmailFactory
+ .create(
+ addToAttentionSetSender.create(ctx.getProject(), change.getId()),
+ ctx,
+ change,
+ reason,
+ attentionUserId)
+ .sendAsync();
}
}
diff --git a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
index 50ee9d4..9fb4fc4 100644
--- a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
@@ -16,13 +16,11 @@
import static java.util.Objects.requireNonNull;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.AttentionSetUpdate.Operation;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.RemoveFromAttentionSetSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.query.change.ChangeData;
@@ -32,19 +30,15 @@
import com.google.gerrit.server.util.AttentionSetEmail;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
import java.util.Optional;
/** Remove a specified user from the attention set. */
public class RemoveFromAttentionSetOp implements BatchUpdateOp {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
public interface Factory {
RemoveFromAttentionSetOp create(Account.Id attentionUserId, String reason, boolean notify);
}
private final ChangeData.Factory changeDataFactory;
- private final MessageIdGenerator messageIdGenerator;
private final RemoveFromAttentionSetSender.Factory removeFromAttentionSetSender;
private final AttentionSetEmail.Factory attentionSetEmailFactory;
@@ -64,14 +58,12 @@
@Inject
RemoveFromAttentionSetOp(
ChangeData.Factory changeDataFactory,
- MessageIdGenerator messageIdGenerator,
RemoveFromAttentionSetSender.Factory removeFromAttentionSetSenderFactory,
AttentionSetEmail.Factory attentionSetEmailFactory,
@Assisted Account.Id attentionUserId,
@Assisted String reason,
@Assisted boolean notify) {
this.changeDataFactory = changeDataFactory;
- this.messageIdGenerator = messageIdGenerator;
this.removeFromAttentionSetSender = removeFromAttentionSetSenderFactory;
this.attentionSetEmailFactory = attentionSetEmailFactory;
this.attentionUserId = requireNonNull(attentionUserId, "user");
@@ -105,18 +97,13 @@
if (!notify) {
return;
}
- try {
- attentionSetEmailFactory
- .create(
- removeFromAttentionSetSender.create(ctx.getProject(), change.getId()),
- ctx,
- change,
- reason,
- messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()),
- attentionUserId)
- .sendAsync();
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(e.getMessage(), change.getId());
- }
+ attentionSetEmailFactory
+ .create(
+ removeFromAttentionSetSender.create(ctx.getProject(), change.getId()),
+ ctx,
+ change,
+ reason,
+ attentionUserId)
+ .sendAsync();
}
}
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index ee8dfc8..fced578 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -15,21 +15,18 @@
package com.google.gerrit.server.index;
import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
-import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID_STR;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.project.ProjectField;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.group.GroupField;
-import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.GroupBackedUser;
import java.io.IOException;
import java.util.Set;
@@ -71,11 +68,9 @@
/**
* Returns a sanitized set of fields for change index queries by removing fields that the current
- * index version doesn't support and accounting for numeric vs. string primary keys. The primary
- * key situation is temporary and should be removed after the migration is done.
+ * index version doesn't support.
*/
- public static Set<String> changeFields(QueryOptions opts, boolean useLegacyNumericFields) {
- FieldDef<ChangeData, ?> idField = useLegacyNumericFields ? LEGACY_ID : LEGACY_ID_STR;
+ public static Set<String> changeFields(QueryOptions opts) {
// Ensure we request enough fields to construct a ChangeData. We need both
// change ID and project, which can either come via the Change field or
// separate fields.
@@ -84,10 +79,10 @@
// A Change is always sufficient.
return fs;
}
- if (fs.contains(PROJECT.getName()) && fs.contains(idField.getName())) {
+ if (fs.contains(PROJECT.getName()) && fs.contains(LEGACY_ID_STR.getName())) {
return fs;
}
- return Sets.union(fs, ImmutableSet.of(idField.getName(), PROJECT.getName()));
+ return Sets.union(fs, ImmutableSet.of(LEGACY_ID_STR.getName(), PROJECT.getName()));
}
/**
diff --git a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
index 5de3ba4..7029d10 100644
--- a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -22,30 +22,25 @@
/** Definition of account index versions (schemata). See {@link SchemaDefinitions}. */
public class AccountSchemaDefinitions extends SchemaDefinitions<AccountState> {
+
@Deprecated
- static final Schema<AccountState> V4 =
+ static final Schema<AccountState> V8 =
schema(
AccountField.ACTIVE,
AccountField.EMAIL,
AccountField.EXTERNAL_ID,
+ AccountField.EXTERNAL_ID_STATE,
AccountField.FULL_NAME,
AccountField.ID,
AccountField.NAME_PART,
+ AccountField.NAME_PART_NO_SECONDARY_EMAIL,
+ AccountField.PREFERRED_EMAIL,
+ AccountField.PREFERRED_EMAIL_EXACT,
+ AccountField.REF_STATE,
AccountField.REGISTERED,
AccountField.USERNAME,
AccountField.WATCHED_PROJECT);
- @Deprecated static final Schema<AccountState> V5 = schema(V4, AccountField.PREFERRED_EMAIL);
-
- @Deprecated
- static final Schema<AccountState> V6 =
- schema(V5, AccountField.REF_STATE, AccountField.EXTERNAL_ID_STATE);
-
- @Deprecated static final Schema<AccountState> V7 = schema(V6, AccountField.PREFERRED_EMAIL_EXACT);
-
- @Deprecated
- static final Schema<AccountState> V8 = schema(V7, AccountField.NAME_PART_NO_SECONDARY_EMAIL);
-
// Bump Lucene version requires reindexing
@Deprecated static final Schema<AccountState> V9 = schema(V8);
@@ -55,14 +50,17 @@
// New numeric types: use dimensional points using the k-d tree geo-spatial data structure
// to offer fast single- and multi-dimensional numeric range. As the consequense, integer
// document id type is replaced with string document id type.
+ @Deprecated
static final Schema<AccountState> V11 =
new Schema.Builder<AccountState>()
.add(V10)
.remove(AccountField.ID)
.add(AccountField.ID_STR)
- .legacyNumericFields(false)
.build();
+ // Bump Lucene version requires reindexing
+ static final Schema<AccountState> V12 = schema(V11);
+
/**
* Name of the account index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java
index 50fdcde..81a4d1e 100644
--- a/java/com/google/gerrit/server/index/account/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -100,7 +100,7 @@
return StalenessCheckResult.notStale();
}
- boolean useLegacyNumericFields = i.getSchema().useLegacyNumericFields();
+ boolean useLegacyNumericFields = i.getSchema().hasField(AccountField.ID);
ImmutableSet<String> fields = useLegacyNumericFields ? FIELDS : FIELDS2;
Optional<FieldBundle> result =
i.getRaw(
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index c06347e..281bcb4 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -116,9 +116,6 @@
// TODO: Rename LEGACY_ID to NUMERIC_ID
/** Legacy change ID. */
- public static final FieldDef<ChangeData, Integer> LEGACY_ID =
- integer("legacy_id").stored().build(cd -> cd.getId().get());
-
public static final FieldDef<ChangeData, String> LEGACY_ID_STR =
exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.getId().get()));
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 05c5c77..6fc2665 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -30,8 +30,6 @@
@Override
default Predicate<ChangeData> keyPredicate(Change.Id id) {
- return getSchema().useLegacyNumericFields()
- ? ChangePredicates.id(id)
- : ChangePredicates.idStr(id);
+ return ChangePredicates.idStr(id);
}
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 0a06735..aa08069 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -23,18 +23,25 @@
/** Definition of change index versions (schemata). See {@link SchemaDefinitions}. */
public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
@Deprecated
- static final Schema<ChangeData> V55 =
+ /** Added new field {@link ChangeField#IS_SUBMITTABLE} based on submit requirements. */
+ static final Schema<ChangeData> V74 =
schema(
ChangeField.ADDED,
ChangeField.APPROVAL,
ChangeField.ASSIGNEE,
+ ChangeField.ATTENTION_SET_FULL,
+ ChangeField.ATTENTION_SET_USERS,
+ ChangeField.ATTENTION_SET_USERS_COUNT,
ChangeField.AUTHOR,
ChangeField.CHANGE,
+ ChangeField.CHERRY_PICK,
+ ChangeField.CHERRY_PICK_OF_CHANGE,
+ ChangeField.CHERRY_PICK_OF_PATCHSET,
ChangeField.COMMENT,
ChangeField.COMMENTBY,
ChangeField.COMMIT,
- ChangeField.COMMITTER,
ChangeField.COMMIT_MESSAGE,
+ ChangeField.COMMITTER,
ChangeField.DELETED,
ChangeField.DELTA,
ChangeField.DIRECTORY,
@@ -47,14 +54,19 @@
ChangeField.EXTENSION,
ChangeField.FILE_PART,
ChangeField.FOOTER,
+ ChangeField.FUZZY_HASHTAG,
ChangeField.FUZZY_TOPIC,
ChangeField.GROUP,
ChangeField.HASHTAG,
ChangeField.HASHTAG_CASE_AWARE,
ChangeField.ID,
+ ChangeField.IS_PURE_REVERT,
+ ChangeField.IS_SUBMITTABLE,
ChangeField.LABEL,
- ChangeField.LEGACY_ID,
+ ChangeField.LEGACY_ID_STR,
+ ChangeField.MERGE,
ChangeField.MERGEABLE,
+ ChangeField.MERGED_ON,
ChangeField.ONLY_EXTENSIONS,
ChangeField.OWNER,
ChangeField.PATCH_SET,
@@ -77,131 +89,18 @@
ChangeField.STATUS,
ChangeField.STORED_SUBMIT_RECORD_LENIENT,
ChangeField.STORED_SUBMIT_RECORD_STRICT,
+ ChangeField.STORED_SUBMIT_REQUIREMENTS,
ChangeField.SUBMISSIONID,
ChangeField.SUBMIT_RECORD,
+ ChangeField.SUBMIT_RULE_RESULT,
ChangeField.TOTAL_COMMENT_COUNT,
ChangeField.TR,
ChangeField.UNRESOLVED_COMMENT_COUNT,
ChangeField.UPDATED,
+ ChangeField.UPLOADER,
ChangeField.WIP);
/**
- * The computation of the {@link ChangeField#EXTENSION} field is changed, hence reindexing is
- * required.
- */
- @Deprecated static final Schema<ChangeData> V56 = schema(V55);
-
- /**
- * New numeric types: use dimensional points using the k-d tree geo-spatial data structure to
- * offer fast single- and multi-dimensional numeric range. As the consequense, {@link
- * ChangeField#LEGACY_ID} is replaced with {@link ChangeField#LEGACY_ID_STR}.
- */
- @Deprecated
- static final Schema<ChangeData> V57 =
- new Schema.Builder<ChangeData>()
- .add(V56)
- .remove(ChangeField.LEGACY_ID)
- .add(ChangeField.LEGACY_ID_STR)
- .legacyNumericFields(false)
- .build();
-
- /**
- * Added new fields {@link ChangeField#CHERRY_PICK_OF_CHANGE} and {@link
- * ChangeField#CHERRY_PICK_OF_PATCHSET}.
- */
- @Deprecated
- static final Schema<ChangeData> V58 =
- new Schema.Builder<ChangeData>()
- .add(V57)
- .add(ChangeField.CHERRY_PICK_OF_CHANGE)
- .add(ChangeField.CHERRY_PICK_OF_PATCHSET)
- .build();
-
- /**
- * Added new fields {@link ChangeField#ATTENTION_SET_USERS} and {@link
- * ChangeField#ATTENTION_SET_FULL}.
- */
- @Deprecated
- static final Schema<ChangeData> V59 =
- new Schema.Builder<ChangeData>()
- .add(V58)
- .add(ChangeField.ATTENTION_SET_USERS)
- .add(ChangeField.ATTENTION_SET_FULL)
- .build();
-
- /** Added new fields {@link ChangeField#MERGE} */
- @Deprecated
- static final Schema<ChangeData> V60 =
- new Schema.Builder<ChangeData>().add(V59).add(ChangeField.MERGE).build();
-
- /** Added new field {@link ChangeField#MERGED_ON} */
- @Deprecated
- static final Schema<ChangeData> V61 =
- new Schema.Builder<ChangeData>().add(V60).add(ChangeField.MERGED_ON).build();
-
- /** Added new field {@link ChangeField#FUZZY_HASHTAG} */
- @Deprecated
- static final Schema<ChangeData> V62 =
- new Schema.Builder<ChangeData>().add(V61).add(ChangeField.FUZZY_HASHTAG).build();
-
- /**
- * The computation of the {@link ChangeField#DIRECTORY} field is changed, hence reindexing is
- * required.
- */
- @Deprecated static final Schema<ChangeData> V63 = schema(V62, false);
-
- /** Added support for MIN/MAX/ANY for {@link ChangeField#LABEL} */
- @Deprecated static final Schema<ChangeData> V64 = schema(V63, false);
-
- /** Added new field for submit requirements. */
- @Deprecated
- static final Schema<ChangeData> V65 =
- new Schema.Builder<ChangeData>().add(V64).add(ChangeField.STORED_SUBMIT_REQUIREMENTS).build();
-
- /**
- * The computation of {@link ChangeField#LABEL} has changed: We added the non_uploader arg to the
- * label field.
- */
- @Deprecated static final Schema<ChangeData> V66 = schema(V65, false);
-
- /** Updated submit records: store the rule name that created the submit record. */
- @Deprecated static final Schema<ChangeData> V67 = schema(V66, false);
-
- /** Added new field {@link ChangeField#SUBMIT_RULE_RESULT}. */
- @Deprecated
- static final Schema<ChangeData> V68 =
- new Schema.Builder<ChangeData>().add(V67).add(ChangeField.SUBMIT_RULE_RESULT).build();
-
- /** Added new field {@link ChangeField#CHERRY_PICK}. */
- @Deprecated
- static final Schema<ChangeData> V69 =
- new Schema.Builder<ChangeData>().add(V68).add(ChangeField.CHERRY_PICK).build();
-
- /** Added new field {@link ChangeField#ATTENTION_SET_USERS_COUNT}. */
- @Deprecated
- static final Schema<ChangeData> V70 =
- new Schema.Builder<ChangeData>().add(V69).add(ChangeField.ATTENTION_SET_USERS_COUNT).build();
-
- /** Added new field {@link ChangeField#UPLOADER}. */
- @Deprecated
- static final Schema<ChangeData> V71 =
- new Schema.Builder<ChangeData>().add(V70).add(ChangeField.UPLOADER).build();
-
- /** Added new field {@link ChangeField#IS_PURE_REVERT}. */
- @Deprecated
- static final Schema<ChangeData> V72 =
- new Schema.Builder<ChangeData>().add(V71).add(ChangeField.IS_PURE_REVERT).build();
-
- @Deprecated
- /** Added new "count=$count" argument to the {@link ChangeField#LABEL} operator. */
- static final Schema<ChangeData> V73 = schema(V72, false);
-
- @Deprecated
- /** Added new field {@link ChangeField#IS_SUBMITTABLE} based on submit requirements. */
- static final Schema<ChangeData> V74 =
- new Schema.Builder<ChangeData>().add(V73).add(ChangeField.IS_SUBMITTABLE).build();
-
- /**
* Added new field {@link ChangeField#PREFIX_HASHTAG} and {@link ChangeField#PREFIX_TOPIC} to
* allow easier search for topics.
*/
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index c4d8952..773aa9a 100644
--- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -23,23 +23,20 @@
/** Definition of group index versions (schemata). See {@link SchemaDefinitions}. */
public class GroupSchemaDefinitions extends SchemaDefinitions<InternalGroup> {
@Deprecated
- static final Schema<InternalGroup> V2 =
+ static final Schema<InternalGroup> V5 =
schema(
+ GroupField.CREATED_ON,
GroupField.DESCRIPTION,
GroupField.ID,
GroupField.IS_VISIBLE_TO_ALL,
+ GroupField.MEMBER,
GroupField.NAME,
GroupField.NAME_PART,
GroupField.OWNER_UUID,
+ GroupField.REF_STATE,
+ GroupField.SUBGROUP,
GroupField.UUID);
- @Deprecated static final Schema<InternalGroup> V3 = schema(V2, GroupField.CREATED_ON);
-
- @Deprecated
- static final Schema<InternalGroup> V4 = schema(V3, GroupField.MEMBER, GroupField.SUBGROUP);
-
- @Deprecated static final Schema<InternalGroup> V5 = schema(V4, GroupField.REF_STATE);
-
// Bump Lucene version requires reindexing
@Deprecated static final Schema<InternalGroup> V6 = schema(V5);
@@ -48,7 +45,7 @@
// New numeric types: use dimensional points using the k-d tree geo-spatial data structure
// to offer fast single- and multi-dimensional numeric range.
- static final Schema<InternalGroup> V8 = schema(V7, false);
+ static final Schema<InternalGroup> V8 = schema(V7);
/** Singleton instance of the schema definitions. This is one per JVM. */
public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/mail/send/AddToAttentionSetSender.java b/java/com/google/gerrit/server/mail/send/AddToAttentionSetSender.java
index b13bcf6..f9ef199 100644
--- a/java/com/google/gerrit/server/mail/send/AddToAttentionSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/AddToAttentionSetSender.java
@@ -30,7 +30,7 @@
@Inject
public AddToAttentionSetSender(
EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
- super(args, project, changeId);
+ super(args, "addToAttentionSet", project, changeId);
}
@Override
diff --git a/java/com/google/gerrit/server/mail/send/AttentionSetSender.java b/java/com/google/gerrit/server/mail/send/AttentionSetSender.java
index 8f898a8..f5af783 100644
--- a/java/com/google/gerrit/server/mail/send/AttentionSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/AttentionSetSender.java
@@ -11,6 +11,7 @@
// WITHOUT 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.mail.send;
import com.google.gerrit.entities.Account;
@@ -23,8 +24,9 @@
private Account.Id attentionSetUser;
private String reason;
- public AttentionSetSender(EmailArguments args, Project.NameKey project, Change.Id changeId) {
- super(args, "addToAttentionSet", ChangeEmail.newChangeData(args, project, changeId));
+ public AttentionSetSender(
+ EmailArguments args, String messageClass, Project.NameKey project, Change.Id changeId) {
+ super(args, messageClass, ChangeEmail.newChangeData(args, project, changeId));
}
@Override
diff --git a/java/com/google/gerrit/server/mail/send/RemoveFromAttentionSetSender.java b/java/com/google/gerrit/server/mail/send/RemoveFromAttentionSetSender.java
index 6762b7d..5242bfb 100644
--- a/java/com/google/gerrit/server/mail/send/RemoveFromAttentionSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/RemoveFromAttentionSetSender.java
@@ -30,7 +30,7 @@
@Inject
public RemoveFromAttentionSetSender(
EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
- super(args, project, changeId);
+ super(args, "removeFromAttentionSet", project, changeId);
}
@Override
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index 8f94089..dd8d685 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -66,7 +66,7 @@
public static Predicate<AccountState> id(Schema<AccountState> schema, Account.Id accountId) {
return new AccountPredicate(
- schema.useLegacyNumericFields() ? AccountField.ID : AccountField.ID_STR,
+ schema.hasField(AccountField.ID) ? AccountField.ID : AccountField.ID_STR,
AccountQueryBuilder.FIELD_ACCOUNT,
accountId.toString());
}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index ce17b31..c874db7 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -136,15 +136,6 @@
* Returns a predicate that matches the change with the provided {@link
* com.google.gerrit.entities.Change.Id}.
*/
- public static Predicate<ChangeData> id(Change.Id id) {
- return new ChangeIndexPredicate(
- ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
- }
-
- /**
- * Returns a predicate that matches the change with the provided {@link
- * com.google.gerrit.entities.Change.Id}.
- */
public static Predicate<ChangeData> idStr(Change.Id id) {
return new ChangeIndexPredicate(
ChangeField.LEGACY_ID_STR, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 28ffef7..9e9a960 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -559,9 +559,7 @@
if (PAT_LEGACY_ID.matcher(query).matches()) {
Integer id = Ints.tryParse(query);
if (id != null) {
- return args.getSchema().useLegacyNumericFields()
- ? ChangePredicates.id(Change.id(id))
- : ChangePredicates.idStr(Change.id(id));
+ return ChangePredicates.idStr(Change.id(id));
}
} else if (PAT_CHANGE_ID.matcher(query).matches()) {
return ChangePredicates.idPrefix(parseChangeId(query));
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index f95dbb0..fc4c1d0 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -89,11 +89,7 @@
List<Predicate<ChangeData>> and = new ArrayList<>(5);
and.add(ChangePredicates.project(c.getProject()));
and.add(ChangePredicates.ref(c.getDest().branch()));
- and.add(
- Predicate.not(
- args.getSchema().useLegacyNumericFields()
- ? ChangePredicates.id(c.getId())
- : ChangePredicates.idStr(c.getId())));
+ and.add(Predicate.not(ChangePredicates.idStr(c.getId())));
and.add(Predicate.or(filePredicates));
ChangeDataCache changeDataCache = new ChangeDataCache(cd, args.projectCache);
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index e7b25fb..99c1ca1 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -56,11 +56,6 @@
* holding on to a single instance.
*/
public class InternalChangeQuery extends InternalQuery<ChangeData, InternalChangeQuery> {
- @FunctionalInterface
- static interface ChangeIdPredicateFactory {
- Predicate<ChangeData> create(Change.Id id);
- }
-
private static Predicate<ChangeData> ref(BranchNameKey branch) {
return ChangePredicates.ref(branch.branch());
}
@@ -84,9 +79,6 @@
private final ChangeData.Factory changeDataFactory;
private final ChangeNotes.Factory notesFactory;
- // TODO(davido): Remove the below fields when support for legacy numeric fields is removed.
- private final ChangeIdPredicateFactory predicateFactory;
-
@Inject
InternalChangeQuery(
ChangeQueryProcessor queryProcessor,
@@ -97,11 +89,6 @@
super(queryProcessor, indexes, indexConfig);
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
- predicateFactory =
- (id) ->
- schema().useLegacyNumericFields()
- ? ChangePredicates.id(id)
- : ChangePredicates.idStr(id);
}
public List<ChangeData> byKey(Change.Key key) {
@@ -113,13 +100,13 @@
}
public List<ChangeData> byLegacyChangeId(Change.Id id) {
- return query(predicateFactory.create(id));
+ return query(ChangePredicates.idStr(id));
}
public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) {
List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size());
for (Change.Id id : ids) {
- preds.add(predicateFactory.create(id));
+ preds.add(ChangePredicates.idStr(id));
}
return query(or(preds));
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index fbd99eb..fbb4fbd 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -70,7 +70,7 @@
private final Provider<CurrentUser> userProvider;
private final BatchUpdate.Factory batchUpdateFactory;
- private final Provider<ChangeQueryBuilder> queryBuilderProvider;
+ private final ChangeQueryBuilder queryBuilder;
private final Provider<InternalChangeQuery> queryProvider;
private final ChangeData.Factory changeDataFactory;
private final ChangeJson.Factory changeJsonFactory;
@@ -83,7 +83,7 @@
DeleteDraftComments(
Provider<CurrentUser> userProvider,
BatchUpdate.Factory batchUpdateFactory,
- Provider<ChangeQueryBuilder> queryBuilderProvider,
+ ChangeQueryBuilder queryBuilder,
Provider<InternalChangeQuery> queryProvider,
ChangeData.Factory changeDataFactory,
ChangeJson.Factory changeJsonFactory,
@@ -93,7 +93,7 @@
ExperimentFeatures experimentFeatures) {
this.userProvider = userProvider;
this.batchUpdateFactory = batchUpdateFactory;
- this.queryBuilderProvider = queryBuilderProvider;
+ this.queryBuilder = queryBuilder;
this.queryProvider = queryProvider;
this.changeDataFactory = changeDataFactory;
this.changeJsonFactory = changeJsonFactory;
@@ -161,7 +161,7 @@
return hasDraft;
}
try {
- return Predicate.and(hasDraft, queryBuilderProvider.get().parse(input.query));
+ return Predicate.and(hasDraft, queryBuilder.parse(input.query));
} catch (QueryParseException e) {
throw new BadRequestException("Invalid query: " + e.getMessage(), e);
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index e3cf4db..842f4b9 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -32,7 +32,6 @@
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.query.FieldBundle;
@@ -236,10 +235,7 @@
return suggestedReviewers;
}
- private static Account.Id fromIdField(FieldBundle f, boolean useLegacyNumericFields) {
- if (useLegacyNumericFields) {
- return Account.id(f.getValue(AccountField.ID).intValue());
- }
+ private static Account.Id fromIdField(FieldBundle f) {
return Account.id(Integer.valueOf(f.getValue(AccountField.ID_STR)));
}
@@ -255,10 +251,6 @@
accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
logger.atFine().log("accounts index query: %s", pred);
accountIndexRewriter.validateMaxTermsInQuery(pred);
- boolean useLegacyNumericFields =
- accountIndexes.getSearchIndex().getSchema().useLegacyNumericFields();
- FieldDef<AccountState, ?> idField =
- useLegacyNumericFields ? AccountField.ID : AccountField.ID_STR;
ResultSet<FieldBundle> result =
accountIndexes
.getSearchIndex()
@@ -268,12 +260,10 @@
indexConfig,
0,
suggestReviewers.getLimit(),
- ImmutableSet.of(idField.getName())))
+ ImmutableSet.of(AccountField.ID_STR.getName())))
.readRaw();
List<Account.Id> matches =
- result.toList().stream()
- .map(f -> fromIdField(f, useLegacyNumericFields))
- .collect(toList());
+ result.toList().stream().map(f -> fromIdField(f)).collect(toList());
logger.atFine().log("Matches: %s", matches);
return matches;
} catch (TooManyTermsInQueryException e) {
diff --git a/java/com/google/gerrit/server/util/AttentionSetEmail.java b/java/com/google/gerrit/server/util/AttentionSetEmail.java
index 48ddd31..1b36139 100644
--- a/java/com/google/gerrit/server/util/AttentionSetEmail.java
+++ b/java/com/google/gerrit/server/util/AttentionSetEmail.java
@@ -18,19 +18,24 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.mail.send.AddToAttentionSetSender;
import com.google.gerrit.server.mail.send.AttentionSetSender;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.mail.send.MessageIdGenerator.MessageId;
import com.google.gerrit.server.mail.send.RemoveFromAttentionSetSender;
import com.google.gerrit.server.update.Context;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
-public class AttentionSetEmail implements Runnable, RequestContext {
+public class AttentionSetEmail {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public interface Factory {
@@ -43,7 +48,6 @@
* @param ctx context for sending the email.
* @param change the change that the user was added/removed in.
* @param reason reason for adding/removing the user.
- * @param messageId messageId for tracking the email.
* @param attentionUserId the user added/removed.
*/
AttentionSetEmail create(
@@ -51,70 +55,117 @@
Context ctx,
Change change,
String reason,
- MessageIdGenerator.MessageId messageId,
Account.Id attentionUserId);
}
- private ExecutorService sendEmailsExecutor;
- private AccountTemplateUtil accountTemplateUtil;
- private AttentionSetSender sender;
- private Context ctx;
- private Change change;
- private String reason;
-
- private MessageIdGenerator.MessageId messageId;
- private Account.Id attentionUserId;
+ private final ExecutorService sendEmailsExecutor;
+ private final AsyncSender asyncSender;
@Inject
AttentionSetEmail(
@SendEmailExecutor ExecutorService executor,
+ ThreadLocalRequestContext requestContext,
+ MessageIdGenerator messageIdGenerator,
AccountTemplateUtil accountTemplateUtil,
@Assisted AttentionSetSender sender,
@Assisted Context ctx,
@Assisted Change change,
@Assisted String reason,
- @Assisted MessageIdGenerator.MessageId messageId,
@Assisted Account.Id attentionUserId) {
this.sendEmailsExecutor = executor;
- this.accountTemplateUtil = accountTemplateUtil;
- this.sender = sender;
- this.ctx = ctx;
- this.change = change;
- this.reason = reason;
- this.messageId = messageId;
- this.attentionUserId = attentionUserId;
+
+ MessageId messageId;
+ try {
+ messageId =
+ messageIdGenerator.fromChangeUpdateAndReason(
+ ctx.getRepoView(), change.currentPatchSetId(), "AttentionSetEmail");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ this.asyncSender =
+ new AsyncSender(
+ requestContext,
+ ctx.getIdentifiedUser(),
+ sender,
+ messageId,
+ ctx.getNotify(change.getId()),
+ attentionUserId,
+ accountTemplateUtil.replaceTemplates(reason),
+ change.getId());
}
public void sendAsync() {
@SuppressWarnings("unused")
- Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(this);
+ Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(asyncSender);
}
- @Override
- public void run() {
- try {
- AccountState accountState =
- ctx.getUser().isIdentifiedUser() ? ctx.getUser().asIdentifiedUser().state() : null;
- if (accountState != null) {
- sender.setFrom(accountState.account().id());
- }
- sender.setNotify(ctx.getNotify(change.getId()));
- sender.setAttentionSetUser(attentionUserId);
- sender.setReason(accountTemplateUtil.replaceTemplates(reason));
- sender.setMessageId(messageId);
- sender.send();
- } catch (Exception e) {
- logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
+ /**
+ * {@link Runnable} that sends the email asynchonously.
+ *
+ * <p>Only pass objects into this class that are thread-safe (e.g. immutable) so that they can be
+ * safely accessed from the background thread.
+ */
+ private static class AsyncSender implements Runnable, RequestContext {
+ private final ThreadLocalRequestContext requestContext;
+ private final IdentifiedUser user;
+ private final AttentionSetSender sender;
+ private final MessageIdGenerator.MessageId messageId;
+ private final NotifyResolver.Result notify;
+ private final Account.Id attentionUserId;
+ private final String reason;
+ private final Change.Id changeId;
+
+ AsyncSender(
+ ThreadLocalRequestContext requestContext,
+ IdentifiedUser user,
+ AttentionSetSender sender,
+ MessageIdGenerator.MessageId messageId,
+ NotifyResolver.Result notify,
+ Account.Id attentionUserId,
+ String reason,
+ Change.Id changeId) {
+ this.requestContext = requestContext;
+ this.user = user;
+ this.sender = sender;
+ this.messageId = messageId;
+ this.notify = notify;
+ this.attentionUserId = attentionUserId;
+ this.reason = reason;
+ this.changeId = changeId;
}
- }
- @Override
- public String toString() {
- return "send-email comments";
- }
+ @Override
+ public void run() {
+ RequestContext old = requestContext.setContext(this);
+ try {
+ Optional<Account.Id> accountId =
+ user.isIdentifiedUser()
+ ? Optional.of(user.asIdentifiedUser().getAccountId())
+ : Optional.empty();
+ if (accountId.isPresent()) {
+ sender.setFrom(accountId.get());
+ }
+ sender.setNotify(notify);
+ sender.setAttentionSetUser(attentionUserId);
+ sender.setReason(reason);
+ sender.setMessageId(messageId);
+ sender.send();
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log("Cannot email update for change %s", changeId);
+ } finally {
+ requestContext.setContext(old);
+ }
+ }
- @Override
- public CurrentUser getUser() {
- return ctx.getUser();
+ @Override
+ public String toString() {
+ return "send-email attention-set-update";
+ }
+
+ @Override
+ public CurrentUser getUser() {
+ return user;
+ }
}
}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
index cac376f..7386a03 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -32,10 +32,12 @@
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.index.Schema;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.server.index.GerritIndexStatus;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Injector;
import com.google.inject.Key;
@@ -48,6 +50,7 @@
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
+import org.junit.Assume;
import org.junit.Test;
@NoHttpd
@@ -175,7 +178,9 @@
@Test
public void onlineUpgradeChanges() throws Exception {
- int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
+ Schema<ChangeData> previous = ChangeSchemaDefinitions.INSTANCE.getPrevious();
+ Assume.assumeNotNull(previous);
+ int prevVersion = previous.getVersion();
int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
// Before storing any changes, switch back to the previous version.
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index fc56a3c..07abae9 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -28,11 +28,10 @@
@Ignore
public class FakeChangeIndex implements ChangeIndex {
- static final Schema<ChangeData> V1 = new Schema<>(1, false, ImmutableList.of(ChangeField.STATUS));
+ static final Schema<ChangeData> V1 = new Schema<>(1, ImmutableList.of(ChangeField.STATUS));
static final Schema<ChangeData> V2 =
- new Schema<>(
- 2, false, ImmutableList.of(ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED));
+ new Schema<>(2, ImmutableList.of(ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED));
private static class Source implements ChangeDataSource {
private final Predicate<ChangeData> p;
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 16f7199..9a2499a 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -45,7 +45,6 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
@@ -269,19 +268,7 @@
AccountInfo user2 = newAccount("user");
requestContext.setContext(newRequestContext(Account.id(user2._accountId)));
- if (getSchemaVersion() < 5) {
- assertMissingField(AccountField.PREFERRED_EMAIL);
- assertFailingQuery("email:foo", "'email' operator is not supported by account index version");
- return;
- }
-
- // This at least needs the PREFERRED_EMAIL field which is available from schema version 5.
- if (getSchemaVersion() >= 5) {
- assertQuery(preferredEmail, user1);
- } else {
- assertQuery(preferredEmail);
- }
-
+ assertQuery(preferredEmail, user1);
assertQuery(secondaryEmail);
assertQuery("email:" + preferredEmail, user1);
@@ -369,14 +356,6 @@
assertQuery("self", user3);
assertQuery("me", user3);
- if (getSchemaVersion() < 8) {
- assertMissingField(AccountField.NAME_PART_NO_SECONDARY_EMAIL);
-
- // prefix queries only work if the NAME_PART_NO_SECONDARY_EMAIL field is available
- assertQuery("john");
- return;
- }
-
assertQuery("John", user1);
assertQuery("john", user1);
assertQuery("Doe", user1);
@@ -649,18 +628,13 @@
IndexConfig.createDefault(), 0, 1, schema.getStoredFields().keySet()));
assertThat(rawFields).isPresent();
- if (schema.useLegacyNumericFields()) {
+ if (schema.hasField(AccountField.ID)) {
assertThat(rawFields.get().getValue(AccountField.ID)).isEqualTo(userInfo._accountId);
} else {
assertThat(Integer.valueOf(rawFields.get().getValue(AccountField.ID_STR)))
.isEqualTo(userInfo._accountId);
}
- // The field EXTERNAL_ID_STATE is only supported from schema version 6.
- if (getSchemaVersion() < 6) {
- return;
- }
-
List<AccountExternalIdInfo> externalIdInfos = gApi.accounts().self().getExternalIds();
List<ByteArrayWrapper> blobs = new ArrayList<>();
for (AccountExternalIdInfo info : externalIdInfos) {
@@ -876,12 +850,6 @@
return accounts.stream().map(a -> a._accountId).collect(toList());
}
- protected void assertMissingField(FieldDef<AccountState, ?> field) {
- assertWithMessage("schema %s has field %s", getSchemaVersion(), field.getName())
- .that(getSchema().hasField(field))
- .isFalse();
- }
-
protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
try {
assertQuery(query);
@@ -891,14 +859,6 @@
}
}
- protected int getSchemaVersion() {
- return getSchema().getVersion();
- }
-
- protected Schema<AccountState> getSchema() {
- return indexes.getSearchIndex().getSchema();
- }
-
/** Boiler plate code to check two byte arrays for equality */
private static class ByteArrayWrapper {
private byte[] arr;
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 6d9f916..baa4802 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -91,7 +91,6 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.IndexPredicate;
@@ -176,7 +175,7 @@
@Inject protected AllUsersName allUsersName;
@Inject protected BatchUpdate.Factory updateFactory;
@Inject protected ChangeInserter.Factory changeFactory;
- @Inject protected Provider<ChangeQueryBuilder> queryBuilderProvider;
+ @Inject protected ChangeQueryBuilder queryBuilder;
@Inject protected GerritApi gApi;
@Inject protected IdentifiedUser.GenericFactory userFactory;
@Inject protected ChangeIndexCollection indexes;
@@ -650,7 +649,6 @@
@Test
public void byAuthorExact() throws Exception {
- assume().that(getSchema().hasField(ChangeField.EXACT_AUTHOR)).isTrue();
byAuthorOrCommitterExact("author:");
}
@@ -661,7 +659,6 @@
@Test
public void byCommitterExact() throws Exception {
- assume().that(getSchema().hasField(ChangeField.EXACT_COMMITTER)).isTrue();
byAuthorOrCommitterExact("committer:");
}
@@ -1607,11 +1604,9 @@
assertQuery("ext:.jAvA", change4);
assertQuery("ext:cc", change3, change2, change1);
- if (getSchemaVersion() >= 56) {
- // matching changes with files that have no extension is possible
- assertQuery("ext:\"\"", change5, change4);
- assertFailingQuery("ext:");
- }
+ // matching changes with files that have no extension is possible
+ assertQuery("ext:\"\"", change5, change4);
+ assertFailingQuery("ext:");
}
@Test
@@ -1975,21 +1970,6 @@
}
@Test
- public void mergedOperatorSupportedByIndexVersion() throws Exception {
- if (getSchemaVersion() < 61) {
- assertMissingField(ChangeField.MERGED_ON);
- assertFailingQuery(
- "mergedbefore:2009-10-01",
- "'mergedbefore' operator is not supported by change index version");
- assertFailingQuery(
- "mergedafter:2009-10-01",
- "'mergedafter' operator is not supported by change index version");
- } else {
- assertThat(getSchema().hasField(ChangeField.MERGED_ON)).isTrue();
- }
- }
-
- @Test
public void byMergedBefore() throws Exception {
assume().that(getSchema().hasField(ChangeField.MERGED_ON)).isTrue();
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
@@ -2473,10 +2453,6 @@
@Test
public void bySubmitRuleResult() throws Exception {
- if (getSchemaVersion() < 68) {
- assertMissingField(ChangeField.SUBMIT_RULE_RESULT);
- return;
- }
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
TestRepository<Repo> repo = createProject("repo");
@@ -2497,13 +2473,6 @@
@Test
public void byNonExistingSubmitRule_returnsEmpty() throws Exception {
- // Some submit rules could be removed from the gerrit.config but there can be records for
- // merged changes in NoteDb for these rules. We allow querying for non-existent rules to handle
- // this case.
- if (getSchemaVersion() < 68) {
- assertMissingField(ChangeField.SUBMIT_RULE_RESULT);
- return;
- }
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
TestRepository<Repo> repo = createProject("repo");
@@ -4112,7 +4081,6 @@
assertQuery(ChangeIndexPredicate.none());
- ChangeQueryBuilder queryBuilder = queryBuilderProvider.get();
for (Predicate<ChangeData> matchingOneChange :
ImmutableList.of(
// One index query, one post-filtering query.
@@ -4482,12 +4450,6 @@
}
}
- protected void assertMissingField(FieldDef<ChangeData, ?> field) {
- assertWithMessage("schema %s has field %s", getSchemaVersion(), field.getName())
- .that(getSchema().hasField(field))
- .isFalse();
- }
-
protected void assertFailingQuery(String query) throws Exception {
assertFailingQuery(query, null);
}
@@ -4504,10 +4466,6 @@
}
}
- protected int getSchemaVersion() {
- return getSchema().getVersion();
- }
-
protected Schema<ChangeData> getSchema() {
return indexes.getSearchIndex().getSchema();
}
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/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index a4ef0c3..9b01735 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -84,16 +84,15 @@
constructor() {
super();
this.query = (input: string) => this.getRepoBranchesSuggestions(input);
- }
- override connectedCallback() {
- super.connectedCallback();
- if (!this.repoName) return;
-
- subscribe(this, this.configModel().serverConfig$, config => {
- this.privateChangesEnabled =
- config?.change?.disable_private_changes ?? false;
- });
+ subscribe(
+ this,
+ () => this.configModel().serverConfig$,
+ config => {
+ this.privateChangesEnabled =
+ config?.change?.disable_private_changes ?? false;
+ }
+ );
}
static override get styles() {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index 06691d7..fc2b789 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -127,12 +127,16 @@
constructor() {
super();
- subscribe(this, this.userModel.preferences$, prefs => {
- if (prefs?.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this.selectedScheme = prefs.download_scheme.toLowerCase();
+ subscribe(
+ this,
+ () => this.userModel.preferences$,
+ prefs => {
+ if (prefs?.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this.selectedScheme = prefs.download_scheme.toLowerCase();
+ }
}
- });
+ );
}
override connectedCallback() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
index 55fe277..596e850 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar.ts
@@ -57,16 +57,16 @@
private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getBulkActionsModel().selectedChangeNums$,
+ () => this.getBulkActionsModel().selectedChangeNums$,
selectedChangeNums => (this.numSelected = selectedChangeNums.length)
);
subscribe(
this,
- this.getBulkActionsModel().totalChangeCount$,
+ () => this.getBulkActionsModel().totalChangeCount$,
totalChangeCount => (this.totalChangeCount = totalChangeCount)
);
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
index 4fd65df..eb5e6a8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-abandon-flow/gr-change-list-bulk-abandon-flow.ts
@@ -35,11 +35,11 @@
];
}
- override connectedCallback() {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getBulkActionsModel().selectedChanges$,
+ () => this.getBulkActionsModel().selectedChanges$,
selectedChanges => (this.selectedChanges = selectedChanges)
);
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
index 07bc31d..77c9382 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow.ts
@@ -24,6 +24,7 @@
import {getAppContext} from '../../../services/app-context';
import {fontStyles} from '../../../styles/gr-font-styles';
import {queryAndAssert} from '../../../utils/common-util';
+import '@polymer/iron-icon/iron-icon';
import {
LabelNameToValuesMap,
ReviewInput,
@@ -36,6 +37,7 @@
import '../../change/gr-label-score-row/gr-label-score-row';
import {getOverallStatus} from '../../../utils/bulk-flow-util';
import {allSettled} from '../../../utils/async-util';
+import {pluralize} from '../../../utils/string-util';
@customElement('gr-change-list-bulk-vote-flow')
export class GrChangeListBulkVoteFlow extends LitElement {
@@ -77,24 +79,37 @@
margin-top: var(--spacing-m);
}
.vote-type {
- margin-bottom: var(--spacing-m);
+ margin-bottom: var(--spacing-s);
margin-top: 0;
display: table-caption;
- font-weight: 600; /* TODO: create css variable for it */
}
.main-heading {
margin-bottom: var(--spacing-m);
font-weight: var(--font-weight-h2);
}
+ .error-container {
+ background-color: var(--red-50);
+ margin-top: var(--spacing-l);
+ }
+ .error-container iron-icon {
+ padding: 10px var(--spacing-xl);
+ color: var(--red-700);
+ --iron-icon-height: 20px;
+ --iron-icon-width: 20px;
+ }
+ .error-container span {
+ position: relative;
+ top: 1px;
+ }
`,
];
}
- override connectedCallback() {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getBulkActionsModel().selectedChanges$,
+ () => this.getBulkActionsModel().selectedChanges$,
selectedChanges => {
this.selectedChanges = selectedChanges;
this.resetFlow();
@@ -102,7 +117,7 @@
);
subscribe(
this,
- this.userModel.account$,
+ () => this.userModel.account$,
account => (this.account = account)
);
}
@@ -146,20 +161,40 @@
'Trigger Votes',
permittedLabels
)}
+ ${this.renderErrors()}
</div>
- <!-- TODO: Add error handling status if something fails -->
</gr-dialog>
</gr-overlay>
`;
}
+ private renderErrors() {
+ if (getOverallStatus(this.progressByChange) !== ProgressStatus.FAILED) {
+ return nothing;
+ }
+ return html`
+ <div class="error-container">
+ <iron-icon icon="gr-icons:error"></iron-icon>
+ <span>
+ <!-- prettier-ignore -->
+ Failed to vote on ${pluralize(
+ Array.from(this.progressByChange.values()).filter(
+ status => status === ProgressStatus.FAILED
+ ).length,
+ 'change'
+ )}
+ </span>
+ </div>
+ `;
+ }
+
private renderLabels(
labels: Label[],
heading: string,
permittedLabels?: LabelNameToValuesMap
) {
return html` <div class="scoresTable newSubmitRequirements">
- <h3 class="vote-type">${labels.length ? heading : nothing}</h3>
+ <h3 class="heading-4 vote-type">${labels.length ? heading : nothing}</h3>
${labels
.filter(
label =>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index 0447713..e2cbaf0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -157,7 +157,7 @@
</div>
<div slot="main">
<div class="newSubmitRequirements scoresTable">
- <h3 class="vote-type">Submit requirements votes</h3>
+ <h3 class="heading-4 vote-type">Submit requirements votes</h3>
<gr-label-score-row name="A"> </gr-label-score-row>
<gr-label-score-row name="B"> </gr-label-score-row>
<gr-label-score-row name="C"> </gr-label-score-row>
@@ -165,7 +165,7 @@
</gr-label-score-row>
</div>
<div class="newSubmitRequirements scoresTable">
- <h3 class="vote-type">Trigger Votes</h3>
+ <h3 class="heading-4 vote-type">Trigger Votes</h3>
<gr-label-score-row name="change1OnlyTriggerLabelE">
</gr-label-score-row>
</div>
@@ -174,6 +174,75 @@
</gr-overlay> `);
});
+ test('renders with errors', async () => {
+ const changes: ChangeInfo[] = [change1];
+ getChangesStub.returns(Promise.resolve(changes));
+ model.sync(changes);
+ await waitUntilObserved(
+ model.loadingState$,
+ state => state === LoadingState.LOADED
+ );
+ stubRestApi('saveChangeReview').callsFake(
+ (_changeNum, _patchNum, _review, errFn) =>
+ Promise.resolve(new Response()).then(res => {
+ errFn && errFn();
+ return res;
+ })
+ );
+ await selectChange(change1);
+ await element.updateComplete;
+
+ queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').click();
+
+ await waitUntil(
+ () =>
+ element.progressByChange.get(1 as NumericChangeId) ===
+ ProgressStatus.FAILED
+ );
+
+ expect(element).shadowDom.to.equal(/* HTML */ `<gr-button
+ aria-disabled="false"
+ flatten=""
+ id="voteFlowButton"
+ role="button"
+ tabindex="0"
+ >
+ Vote
+ </gr-button>
+ <gr-overlay
+ aria-hidden="true"
+ id="actionOverlay"
+ style="outline: none; display: none;"
+ tabindex="-1"
+ with-backdrop=""
+ >
+ <gr-dialog role="dialog">
+ <div slot="header">
+ <span class="main-heading"> Vote on selected changes </span>
+ </div>
+ <div slot="main">
+ <div class="newSubmitRequirements scoresTable">
+ <h3 class="heading-4 vote-type">Submit requirements votes</h3>
+ <gr-label-score-row name="A"> </gr-label-score-row>
+ <gr-label-score-row name="B"> </gr-label-score-row>
+ <gr-label-score-row name="C"> </gr-label-score-row>
+ <gr-label-score-row name="change1OnlyLabelD">
+ </gr-label-score-row>
+ </div>
+ <div class="newSubmitRequirements scoresTable">
+ <h3 class="heading-4 vote-type">Trigger Votes</h3>
+ <gr-label-score-row name="change1OnlyTriggerLabelE">
+ </gr-label-score-row>
+ </div>
+ <div class="error-container">
+ <iron-icon icon="gr-icons:error"> </iron-icon>
+ <span> Failed to vote on 1 change </span>
+ </div>
+ </div>
+ </gr-dialog>
+ </gr-overlay> `);
+ });
+
test('button state updates as changes are updated', async () => {
const changes: ChangeInfo[] = [change1];
getChangesStub.returns(Promise.resolve(changes));
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
index fdeb512..d7ff864 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
@@ -110,11 +110,11 @@
];
}
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getBulkActionsModel().selectedChanges$,
+ () => this.getBulkActionsModel().selectedChanges$,
selectedChanges => {
this.selectedChanges = selectedChanges;
}
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 92f6f62..933b300 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
@@ -125,6 +125,18 @@
private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getBulkActionsModel().selectedChangeNums$,
+ selectedChangeNums => {
+ if (!this.change) return;
+ this.checked = selectedChangeNums.includes(this.change._number);
+ }
+ );
+ }
+
override connectedCallback() {
super.connectedCallback();
getPluginLoader()
@@ -134,14 +146,6 @@
'change-list-item-cell'
);
});
- subscribe(
- this,
- this.getBulkActionsModel().selectedChangeNums$,
- selectedChangeNums => {
- if (!this.change) return;
- this.checked = selectedChangeNums.includes(this.change._number);
- }
- );
}
static override get styles() {
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 95e9c87..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,
@@ -24,20 +24,20 @@
import {
GrReviewerSuggestionsProvider,
ReviewerSuggestionsProvider,
- SUGGESTIONS_PROVIDERS_USERS_TYPES,
} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import '../../shared/gr-account-list/gr-account-list';
import {getOverallStatus} from '../../../utils/bulk-flow-util';
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';
-
-const SUGGESTIONS_PROVIDERS_USERS_TYPES_BY_REVIEWER_STATE = {
- REVIEWER: SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER,
- CC: SUGGESTIONS_PROVIDERS_USERS_TYPES.CC,
-};
+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 {
@@ -46,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, []],
@@ -78,6 +78,8 @@
private isLoggedIn = false;
+ private account?: AccountDetailInfo;
+
static override get styles() {
return css`
gr-dialog {
@@ -114,23 +116,28 @@
`;
}
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getBulkActionsModel().selectedChanges$,
+ () => this.getBulkActionsModel().selectedChanges$,
selectedChanges => (this.selectedChanges = selectedChanges)
);
subscribe(
this,
- this.getConfigModel().serverConfig$,
+ () => this.getConfigModel().serverConfig$,
serverConfig => (this.serverConfig = serverConfig)
);
subscribe(
this,
- getAppContext().userModel.loggedIn$,
+ () => getAppContext().userModel.loggedIn$,
isLoggedIn => (this.isLoggedIn = isLoggedIn)
);
+ subscribe(
+ this,
+ () => getAppContext().userModel.account$,
+ account => (this.account = account)
+ );
}
override render() {
@@ -250,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));
}
@@ -298,7 +304,6 @@
reviewerState: ReviewerState,
event: CustomEvent<AccountInputDetail>
) {
- const account = event.detail.account as AccountInfo;
const oppositeReviewerState =
reviewerState === ReviewerState.CC
? ReviewerState.REVIEWER
@@ -307,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);
@@ -341,7 +346,8 @@
])
);
const inFlightActions = this.getBulkActionsModel().addReviewers(
- this.updatedAccountsByReviewerState
+ this.updatedAccountsByReviewerState,
+ getReplyByReason(this.account, this.serverConfig)
);
await allSettled(
@@ -389,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(
@@ -403,7 +403,7 @@
): ReviewerSuggestionsProvider {
const suggestionsProvider = new GrReviewerSuggestionsProvider(
this.restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES_BY_REVIEWER_STATE[state],
+ state,
this.serverConfig,
this.isLoggedIn,
...this.selectedChanges.map(change => change._number)
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-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index 2bf6446..330a93b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -132,13 +132,9 @@
constructor() {
super();
provide(this, bulkActionsModelToken, () => this.bulkActionsModel);
- }
-
- override connectedCallback() {
- super.connectedCallback();
subscribe(
this,
- this.bulkActionsModel.selectedChangeNums$,
+ () => this.bulkActionsModel.selectedChangeNums$,
selectedChanges =>
(this.showBulkActionsHeader = selectedChanges.length > 0)
);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
index 112e688..ffdc9f4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow.ts
@@ -110,11 +110,11 @@
];
}
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getBulkActionsModel().selectedChanges$,
+ () => this.getBulkActionsModel().selectedChanges$,
selectedChanges => {
this.selectedChanges = selectedChanges;
}
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 8161592..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ /dev/null
@@ -1,214 +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"
- limit="25"
- 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"
- limit="25"
- 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"
- limit="25"
- 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-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index f5893ac..6ec815c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -430,54 +430,58 @@
private readonly reporting = getAppContext().reportingService;
- override connectedCallback() {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getChecksModel().allRunsLatestPatchsetLatestAttempt$,
+ () => this.getChecksModel().allRunsLatestPatchsetLatestAttempt$,
x => (this.runs = x)
);
subscribe(
this,
- this.getChecksModel().aPluginHasRegistered$,
+ () => this.getChecksModel().aPluginHasRegistered$,
x => (this.showChecksSummary = x)
);
subscribe(
this,
- this.getChecksModel().someProvidersAreLoadingFirstTime$,
+ () => this.getChecksModel().someProvidersAreLoadingFirstTime$,
x => (this.someProvidersAreLoading = x)
);
subscribe(
this,
- this.getChecksModel().errorMessagesLatest$,
+ () => this.getChecksModel().errorMessagesLatest$,
x => (this.errorMessages = x)
);
subscribe(
this,
- this.getChecksModel().loginCallbackLatest$,
+ () => this.getChecksModel().loginCallbackLatest$,
x => (this.loginCallback = x)
);
subscribe(
this,
- this.getChecksModel().topLevelActionsLatest$,
+ () => this.getChecksModel().topLevelActionsLatest$,
x => (this.actions = x)
);
subscribe(
this,
- this.getChecksModel().topLevelMessagesLatest$,
+ () => this.getChecksModel().topLevelMessagesLatest$,
x => (this.messages = x)
);
subscribe(
this,
- this.getCommentsModel().changeComments$,
+ () => this.getCommentsModel().changeComments$,
x => (this.changeComments = x)
);
subscribe(
this,
- this.getCommentsModel().threads$,
+ () => this.getCommentsModel().threads$,
x => (this.commentThreads = x)
);
- subscribe(this, this.userModel.account$, x => (this.selfAccount = x));
+ subscribe(
+ this,
+ () => this.userModel.account$,
+ x => (this.selfAccount = x)
+ );
}
static override get styles() {
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 e6d89a1..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;
+ });
+ }
});
}
@@ -944,6 +961,8 @@
assertIsDefined(this._change, '_change');
if (!this._changeNum)
throw new Error('missing required changeNum property');
+ // to prevent 2 requests at the same time
+ if (this.$.commitMessageEditor.disabled) return;
// Trim trailing whitespace from each line.
const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
@@ -1284,6 +1303,10 @@
if (value.basePatchNum === undefined)
value.basePatchNum = ParentPatchSetNum;
+ if (value.patchNum === undefined) {
+ value.patchNum = computeLatestPatchNum(this._allPatchSets);
+ }
+
const patchChanged = this.hasPatchRangeChanged(value);
let patchNumChanged = this.hasPatchNumChanged(value);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 80cf8a4..45a6530 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -1529,7 +1529,7 @@
);
});
- test('_handleCommitMessageSave trims trailing whitespace', () => {
+ test('_handleCommitMessageSave trims trailing whitespace', async () => {
element._change = createChangeViewChange();
// Response code is 500, because we want to avoid window reloading
const putStub = stubRestApi('putChangeCommitMessage').returns(
@@ -1541,10 +1541,10 @@
element._handleCommitMessageSave(mockEvent('test \n test '));
assert.equal(putStub.lastCall.args[1], 'test\n test');
-
+ element.$.commitMessageEditor.disabled = false;
element._handleCommitMessageSave(mockEvent(' test\ntest'));
assert.equal(putStub.lastCall.args[1], ' test\ntest');
-
+ element.$.commitMessageEditor.disabled = false;
element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index de9395f..b8d4761 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -93,12 +93,16 @@
];
}
- override connectedCallback() {
- super.connectedCallback();
- subscribe(this, this.getChangeModel().change$, x => (this.change = x));
+ constructor() {
+ super();
subscribe(
this,
- this.getCommentsModel().threads$,
+ () => this.getChangeModel().change$,
+ x => (this.change = x)
+ );
+ subscribe(
+ this,
+ () => this.getCommentsModel().threads$,
x => (this.unresolvedThreads = x.filter(isUnresolved))
);
}
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-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/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 65e42a1..0902fe6 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -663,7 +663,8 @@
return;
}
e.stopPropagation();
- this.message = {...this.message, expanded: true};
+ this.message.expanded = true;
+ this.requestUpdate();
}
private handleAuthorClick(e: Event) {
@@ -671,7 +672,8 @@
return;
}
e.stopPropagation();
- this.message = {...this.message, expanded: false};
+ this.message.expanded = false;
+ this.requestUpdate();
}
// private but used in tests.
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index c03121d..f6b0ad4 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -324,7 +324,8 @@
return;
}
- el.message = {...el.message, expanded: true};
+ el.message.expanded = true;
+ el.requestUpdate();
await el.updateComplete;
let top = el.offsetTop;
for (
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 c35b6b3..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
@@ -27,10 +27,7 @@
import '../gr-label-scores/gr-label-scores';
import '../gr-thread-list/gr-thread-list';
import '../../../styles/shared-styles';
-import {
- GrReviewerSuggestionsProvider,
- SUGGESTIONS_PROVIDERS_USERS_TYPES,
-} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {GrReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import {getAppContext} from '../../../services/app-context';
import {
ChangeStatus,
@@ -264,7 +261,7 @@
account?: AccountInfo;
@state()
- ccs: (AccountInfoInput | GroupInfoInput)[] = [];
+ ccs: AccountInput[] = [];
@state()
attentionCcsCount = 0;
@@ -596,6 +593,60 @@
`,
];
+ constructor() {
+ super();
+ this.filterReviewerSuggestion =
+ this.filterReviewerSuggestionGenerator(false);
+ this.filterCCSuggestion = this.filterReviewerSuggestionGenerator(true);
+ this.jsAPI.addElement(TargetElement.REPLY_DIALOG, this);
+ subscribe(
+ this,
+ () => getAppContext().userModel.loggedIn$,
+ isLoggedIn => (this.isLoggedIn = isLoggedIn)
+ );
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ (
+ IronA11yAnnouncer as unknown as FixIronA11yAnnouncer
+ ).requestAvailability();
+ this.restApiService.getAccount().then(account => {
+ if (account) this.account = account;
+ });
+
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.CTRL_KEY]}, _ =>
+ this.submit()
+ )
+ );
+ this.cleanups.push(
+ addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.META_KEY]}, _ =>
+ this.submit()
+ )
+ );
+ this.cleanups.push(addShortcut(this, {key: Key.ESC}, _ => this.cancel()));
+ this.addEventListener('comment-editing-changed', e => {
+ this.commentEditing = (e as CustomEvent).detail;
+ });
+
+ // Plugins on reply-reviewers endpoint can take advantage of these
+ // events to add / remove reviewers
+
+ this.addEventListener('add-reviewer', e => {
+ // Only support account type, see more from:
+ // elements/shared/gr-account-list/gr-account-list.js#addAccountItem
+ this.reviewersList?.addAccountItem({
+ account: (e as CustomEvent).detail.reviewer,
+ count: 1,
+ });
+ });
+
+ this.addEventListener('remove-reviewer', e => {
+ this.reviewersList?.removeAccount((e as CustomEvent).detail.reviewer);
+ });
+ }
+
override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('draft')) {
this.draftChanged(changedProperties.get('draft') as string);
@@ -643,61 +694,6 @@
}
}
- constructor() {
- super();
- this.filterReviewerSuggestion =
- this.filterReviewerSuggestionGenerator(false);
- this.filterCCSuggestion = this.filterReviewerSuggestionGenerator(true);
- this.jsAPI.addElement(TargetElement.REPLY_DIALOG, this);
- }
-
- override connectedCallback() {
- super.connectedCallback();
- (
- IronA11yAnnouncer as unknown as FixIronA11yAnnouncer
- ).requestAvailability();
- this.restApiService.getAccount().then(account => {
- if (account) this.account = account;
- });
-
- this.cleanups.push(
- addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.CTRL_KEY]}, _ =>
- this.submit()
- )
- );
- this.cleanups.push(
- addShortcut(this, {key: Key.ENTER, modifiers: [Modifier.META_KEY]}, _ =>
- this.submit()
- )
- );
- this.cleanups.push(addShortcut(this, {key: Key.ESC}, _ => this.cancel()));
- this.addEventListener('comment-editing-changed', e => {
- this.commentEditing = (e as CustomEvent).detail;
- });
-
- // Plugins on reply-reviewers endpoint can take advantage of these
- // events to add / remove reviewers
-
- this.addEventListener('add-reviewer', e => {
- // Only support account type, see more from:
- // elements/shared/gr-account-list/gr-account-list.js#addAccountItem
- this.reviewersList?.addAccountItem({
- account: (e as CustomEvent).detail.reviewer,
- count: 1,
- });
- });
-
- this.addEventListener('remove-reviewer', e => {
- this.reviewersList?.removeAccount((e as CustomEvent).detail.reviewer);
- });
-
- subscribe(
- this,
- getAppContext().userModel.loggedIn$,
- isLoggedIn => (this.isLoggedIn = isLoggedIn)
- );
- }
-
override disconnectedCallback() {
this.storeTask?.cancel();
for (const cleanup of this.cleanups) cleanup();
@@ -1491,7 +1487,7 @@
const jsonPromise = this.restApiService.getResponseObject(response.clone());
return jsonPromise.then((parsed: ParsedJSON) => {
const result = parsed as ReviewResult;
- // Only perform custom error handling for 400s and a parseable
+ // Only perform custom error handling for 400s and a parsable
// ReviewResult response.
if (response.status === 400 && result && result.reviewers) {
const errors: string[] = [];
@@ -1531,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)) {
@@ -2090,7 +2086,7 @@
if (!change) return;
const provider = new GrReviewerSuggestionsProvider(
this.restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER,
+ ReviewerState.REVIEWER,
this.serverConfig,
this.isLoggedIn,
change._number
@@ -2102,7 +2098,7 @@
if (!change) return;
const provider = new GrReviewerSuggestionsProvider(
this.restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES.CC,
+ ReviewerState.CC,
this.serverConfig,
this.isLoggedIn,
change._number
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/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 3616f30..08e3ab4 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -144,11 +144,11 @@
private readonly getChecksModel = resolve(this, checksModelToken);
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getChecksModel().allRunsLatestPatchsetLatestAttempt$,
+ () => this.getChecksModel().allRunsLatestPatchsetLatestAttempt$,
x => (this.runs = x)
);
}
@@ -200,7 +200,6 @@
<td class="name">
<gr-limited-text
class="name"
- limit="25"
.text=${requirement.name}
></gr-limited-text>
</td>
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index 6918a1a..09e601c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -99,7 +99,7 @@
</iron-icon>
</td>
<td class="name">
- <gr-limited-text class="name" limit="25"></gr-limited-text>
+ <gr-limited-text class="name"></gr-limited-text>
</td>
<td>
<gr-endpoint-decorator
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index bf85d11..0ba2e89 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -205,15 +205,23 @@
private readonly userModel = getAppContext().userModel;
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getChangeModel().changeNum$,
+ () => this.getChangeModel().changeNum$,
x => (this.changeNum = x)
);
- subscribe(this, this.getChangeModel().change$, x => (this.change = x));
- subscribe(this, this.userModel.account$, x => (this.account = x));
+ subscribe(
+ this,
+ () => this.getChangeModel().change$,
+ x => (this.change = x)
+ );
+ subscribe(
+ this,
+ () => this.userModel.account$,
+ x => (this.account = x)
+ );
}
override willUpdate(changed: PropertyValues) {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 9ea29b0..b9beb1d 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -114,9 +114,13 @@
private readonly reporting = getAppContext().reportingService;
- override connectedCallback() {
- super.connectedCallback();
- subscribe(this, this.getChangeModel().labels$, x => (this.labels = x));
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getChangeModel().labels$,
+ x => (this.labels = x)
+ );
}
static override get styles() {
@@ -608,11 +612,11 @@
];
}
- override connectedCallback() {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getConfigModel().repoConfig$,
+ () => this.getConfigModel().repoConfig$,
x => (this.repoConfig = x)
);
}
@@ -787,31 +791,31 @@
private readonly reporting = getAppContext().reportingService;
- override connectedCallback() {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getChecksModel().topLevelActionsSelected$,
+ () => this.getChecksModel().topLevelActionsSelected$,
x => (this.actions = x)
);
subscribe(
this,
- this.getChecksModel().topLevelLinksSelected$,
+ () => this.getChecksModel().topLevelLinksSelected$,
x => (this.links = x)
);
subscribe(
this,
- this.getChecksModel().checksSelectedPatchsetNumber$,
+ () => this.getChecksModel().checksSelectedPatchsetNumber$,
x => (this.checksPatchsetNumber = x)
);
subscribe(
this,
- this.getChangeModel().latestPatchNum$,
+ () => this.getChangeModel().latestPatchNum$,
x => (this.latestPatchsetNumber = x)
);
subscribe(
this,
- this.getChecksModel().someProvidersAreLoadingSelected$,
+ () => this.getChecksModel().someProvidersAreLoadingSelected$,
x => (this.someProvidersAreLoading = x)
);
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index b18a5ff..414be21 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -429,21 +429,21 @@
private readonly reporting = getAppContext().reportingService;
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getChecksModel().allRunsSelectedPatchset$,
+ () => this.getChecksModel().allRunsSelectedPatchset$,
x => (this.runs = x)
);
subscribe(
this,
- this.getChecksModel().errorMessagesLatest$,
+ () => this.getChecksModel().errorMessagesLatest$,
x => (this.errorMessages = x)
);
subscribe(
this,
- this.getChecksModel().loginCallbackLatest$,
+ () => this.getChecksModel().loginCallbackLatest$,
x => (this.loginCallback = x)
);
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index d808d11..bb34dec 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -73,31 +73,31 @@
private readonly reporting = getAppContext().reportingService;
- override connectedCallback(): void {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getChecksModel().allRunsSelectedPatchset$,
+ () => this.getChecksModel().allRunsSelectedPatchset$,
x => (this.runs = x)
);
subscribe(
this,
- this.getChecksModel().allResultsSelected$,
+ () => this.getChecksModel().allResultsSelected$,
x => (this.results = x)
);
subscribe(
this,
- this.getChecksModel().checksSelectedPatchsetNumber$,
+ () => this.getChecksModel().checksSelectedPatchsetNumber$,
x => (this.checksPatchsetNumber = x)
);
subscribe(
this,
- this.getChangeModel().latestPatchNum$,
+ () => this.getChangeModel().latestPatchNum$,
x => (this.latestPatchsetNumber = x)
);
subscribe(
this,
- this.getChangeModel().changeNum$,
+ () => this.getChangeModel().changeNum$,
x => (this.changeNum = x)
);
}
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.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 39fc048..896a9b2 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -131,11 +131,11 @@
private readonly getCommentsModel = resolve(this, commentsModelToken);
- override connectedCallback() {
- super.connectedCallback();
+ constructor() {
+ super();
subscribe(
this,
- this.getCommentsModel().changeComments$,
+ () => this.getCommentsModel().changeComments$,
x => (this.changeComments = x)
);
}
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/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index dc8e7a6..48d50c7 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -122,13 +122,17 @@
this.addEventListener('content-change', e => {
this.handleContentChange(e as CustomEvent<{value: string}>);
});
+ subscribe(
+ this,
+ () => this.userModel.editPreferences$,
+ editPreferences => {
+ this.editPrefs = editPreferences;
+ }
+ );
}
override connectedCallback() {
super.connectedCallback();
- subscribe(this, this.userModel.editPreferences$, editPreferences => {
- this.editPrefs = editPreferences;
- });
this.cleanups.push(
addShortcut(this, {key: 's', modifiers: [Modifier.CTRL_KEY]}, () =>
this.handleSaveShortcut()
diff --git a/polygerrit-ui/app/elements/lit/subscription-controller.ts b/polygerrit-ui/app/elements/lit/subscription-controller.ts
index b37a978..fdd24cf 100644
--- a/polygerrit-ui/app/elements/lit/subscription-controller.ts
+++ b/polygerrit-ui/app/elements/lit/subscription-controller.ts
@@ -5,50 +5,46 @@
*/
import {ReactiveController, ReactiveControllerHost} from 'lit';
import {Observable, Subscription} from 'rxjs';
+import {Provider} from '../../models/dependency';
-const SUBSCRIPTION_SYMBOL = Symbol('subscriptions');
-
-// Checks whether a subscription can be added. Returns true if it can be added,
-// return false if it's already present.
-// Subscriptions are stored on the host so they have the same life-time as the
-// host.
-function checkSubscription<T>(
- host: ReactiveControllerHost,
- obs$: Observable<T>,
- setProp: (t: T) => void
-): boolean {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const hostSubscriptions = ((host as any)[SUBSCRIPTION_SYMBOL] ||= new Map());
- if (!hostSubscriptions.has(obs$)) hostSubscriptions.set(obs$, new Set());
- const obsSubscriptions = hostSubscriptions.get(obs$);
- if (obsSubscriptions.has(setProp)) return false;
- obsSubscriptions.add(setProp);
- return true;
+export class SubscriptionError extends Error {
+ constructor(message: string) {
+ super(message);
+ }
}
/**
* Enables components to simply hook up a property with an Observable like so:
*
- * subscribe(this, obs$, x => (this.prop = x));
+ * subscribe(this, () => obs$, x => (this.prop = x));
*/
export function subscribe<T>(
- host: ReactiveControllerHost,
- obs$: Observable<T>,
- setProp: (t: T) => void
+ host: ReactiveControllerHost & HTMLElement,
+ provider: Provider<Observable<T>>,
+ callback: (t: T) => void
) {
- if (!checkSubscription(host, obs$, setProp)) return;
- host.addController(new SubscriptionController(obs$, setProp));
+ if (host.isConnected)
+ throw new Error(
+ 'Subscriptions should happen before a component is connected'
+ );
+ const controller = new SubscriptionController(provider, callback);
+ host.addController(controller);
}
+
export class SubscriptionController<T> implements ReactiveController {
private sub?: Subscription;
constructor(
- private readonly obs$: Observable<T>,
- private readonly setProp: (t: T) => void
+ private readonly provider: Provider<Observable<T>>,
+ private readonly callback: (t: T) => void
) {}
hostConnected() {
- this.sub = this.obs$.subscribe(this.setProp);
+ this.sub = this.provider().subscribe(v => this.update(v));
+ }
+
+ update(value: T) {
+ this.callback(value);
}
hostDisconnected() {
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
index 0f7e065..94ccc16 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.ts
@@ -60,12 +60,16 @@
private readonly userModel = getAppContext().userModel;
- override connectedCallback() {
- super.connectedCallback();
- subscribe(this, this.userModel.editPreferences$, editPreferences => {
- this.originalEditPrefs = editPreferences;
- this.editPrefs = {...editPreferences};
- });
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.userModel.editPreferences$,
+ editPreferences => {
+ this.originalEditPrefs = editPreferences;
+ this.editPrefs = {...editPreferences};
+ }
+ );
}
static override get styles() {
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index 845b30c..46c2956 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -35,12 +35,16 @@
private readonly userModel = getAppContext().userModel;
- override connectedCallback() {
- super.connectedCallback();
- subscribe(this, this.userModel.preferences$, prefs => {
- this.originalPrefs = prefs;
- this.menuItems = [...prefs.my];
- });
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.userModel.preferences$,
+ prefs => {
+ this.originalPrefs = prefs;
+ this.menuItems = [...prefs.my];
+ }
+ );
}
static override styles = [
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-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-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 471ebd6..f5f9584 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -264,35 +264,49 @@
super();
this.shortcuts.addGlobal({key: 'e'}, () => this.handleExpandShortcut());
this.shortcuts.addGlobal({key: 'E'}, () => this.handleCollapseShortcut());
- }
-
- override connectedCallback(): void {
- super.connectedCallback();
subscribe(
this,
- this.getChangeModel().changeNum$,
+ () => this.getChangeModel().changeNum$,
x => (this.changeNum = x)
);
- subscribe(this, this.userModel.account$, x => (this.account = x));
- subscribe(this, this.getChangeModel().repo$, x => (this.repoName = x));
- subscribe(this, this.userModel.diffPreferences$, x =>
- this.syntaxLayer.setEnabled(!!x.syntax_highlighting)
+ subscribe(
+ this,
+ () => this.userModel.account$,
+ x => (this.account = x)
);
- subscribe(this, this.userModel.preferences$, prefs => {
- const layers: DiffLayer[] = [this.syntaxLayer];
- if (!prefs.disable_token_highlighting) {
- layers.push(new TokenHighlightLayer(this));
+ subscribe(
+ this,
+ () => this.getChangeModel().repo$,
+ x => (this.repoName = x)
+ );
+ subscribe(
+ this,
+ () => this.userModel.diffPreferences$,
+ x => this.syntaxLayer.setEnabled(!!x.syntax_highlighting)
+ );
+ subscribe(
+ this,
+ () => this.userModel.preferences$,
+ prefs => {
+ const layers: DiffLayer[] = [this.syntaxLayer];
+ if (!prefs.disable_token_highlighting) {
+ layers.push(new TokenHighlightLayer(this));
+ }
+ this.layers = layers;
}
- this.layers = layers;
- });
- subscribe(this, this.userModel.diffPreferences$, prefs => {
- this.prefs = {
- ...prefs,
- // set line_wrapping to true so that the context can take all the
- // remaining space after comment card has rendered
- line_wrapping: true,
- };
- });
+ );
+ subscribe(
+ this,
+ () => this.userModel.diffPreferences$,
+ prefs => {
+ this.prefs = {
+ ...prefs,
+ // set line_wrapping to true so that the context can take all the
+ // remaining space after comment card has rendered
+ line_wrapping: true,
+ };
+ }
+ );
}
static override get styles() {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index c460ad7..3e4a555 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -248,27 +248,36 @@
});
}
}
- }
-
- override connectedCallback() {
- super.connectedCallback();
subscribe(
this,
- this.configModel().repoCommentLinks$,
+ () => this.configModel().repoCommentLinks$,
x => (this.commentLinks = x)
);
- subscribe(this, this.userModel.account$, x => (this.account = x));
- subscribe(this, this.userModel.isAdmin$, x => (this.isAdmin = x));
-
- subscribe(this, this.getChangeModel().repo$, x => (this.repoName = x));
subscribe(
this,
- this.getChangeModel().changeNum$,
+ () => this.userModel.account$,
+ x => (this.account = x)
+ );
+ subscribe(
+ this,
+ () => this.userModel.isAdmin$,
+ x => (this.isAdmin = x)
+ );
+
+ subscribe(
+ this,
+ () => this.getChangeModel().repo$,
+ x => (this.repoName = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().changeNum$,
x => (this.changeNum = x)
);
subscribe(
this,
- this.autoSaveTrigger$.pipe(debounceTime(AUTO_SAVE_DEBOUNCE_DELAY_MS)),
+ () =>
+ this.autoSaveTrigger$.pipe(debounceTime(AUTO_SAVE_DEBOUNCE_DELAY_MS)),
() => {
this.autoSave();
}
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
index 5fd7db4..cb51337 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.ts
@@ -65,13 +65,17 @@
private readonly userModel = getAppContext().userModel;
- override connectedCallback() {
- super.connectedCallback();
- subscribe(this, this.userModel.diffPreferences$, diffPreferences => {
- if (!diffPreferences) return;
- this.originalDiffPrefs = diffPreferences;
- this.diffPrefs = {...diffPreferences};
- });
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.userModel.diffPreferences$,
+ diffPreferences => {
+ if (!diffPreferences) return;
+ this.originalDiffPrefs = diffPreferences;
+ this.diffPrefs = {...diffPreferences};
+ }
+ );
}
static override get styles() {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 99b72fa..bdd2b52 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -424,8 +424,9 @@
}
}
- handleEditCommitMessage() {
+ async handleEditCommitMessage() {
this.editing = true;
+ await this.updateComplete;
this.focusTextarea();
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 4456381..dc2cbc7 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -25,7 +25,7 @@
<g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#unfold_more -->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=unfold_more -->
<g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"></path><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path></g>
@@ -61,11 +61,11 @@
<g id="info"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="info-outline"><path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=ic_hourglass_full-->
<g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#mode_comment-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=mode_comment-->
<g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#calendar_today-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=calendar_today-->
<g id="calendar"><path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z"></path><path d="M0 0h24v24H0z" fill="none"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="error"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></g>
@@ -77,11 +77,13 @@
<g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"></path></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
<g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
+ <g id="build"><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#check_circle-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=check_circle-->
<g id="check-circle"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#check_circle_outline-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=check_circle_outline-->
<g id="check-circle-outline"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
<!-- This SVG is a copy from https://fonts.google.com/icons?selected=Material+Icons:event_busy&icon.query=check+circle-->
<g id="check-circle-filled"><path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M10,17l-4-4l1.4-1.4l2.6,2.6l6.6-6.6 L18,9L10,17z"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
@@ -110,45 +112,45 @@
<g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
- <!-- This SVG is an adaptation of material.io https://material.io/icons/#label_important-->
+ <!-- This SVG is an adaptation of material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=label_important-->
<g id="attention"><path d="M1 23 l13 0 c.67 0 1.27 -.33 1.63 -.84 l7.37 -10.16 l-7.37 -10.16 c-.36 -.51 -.96 -.84 -1.63 -.84 L1 1 L7 12 z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#pets-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=pets-->
<g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#visibility-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=visibility-->
<g id="ready"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons -->
<g id="schedule"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#bug_report-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=bug_report-->
<g id="bug"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/></g>
<!-- This SVG is a copy from material.io https://fonts.gstatic.com/s/i/googlematerialicons/move_item/v1/24px.svg -->
<g id="move-item"><path d="M15,19H5V5h10v4h2V5c0-1.1-0.89-2-2-2H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h10c1.11,0,2-0.9,2-2v-4h-2V19z"/><polygon points="20.01,8.01 18.59,9.41 20.17,11 8,11 8,13 20.17,13 18.59,14.59 20.01,15.99 24,12"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#warning-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=warning-->
<g id="warning"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#timelapse-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=timelapse-->
<g id="timelapse"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.24 7.76C15.07 6.59 13.54 6 12 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0 2.34-2.34 2.34-6.14-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#mark_chat_read-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=mark_chat_read-->
<g id="markChatRead"><path d="M12,18l-6,0l-4,4V4c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2v7l-2,0V4H4v12l8,0V18z M23,14.34l-1.41-1.41l-4.24,4.24l-2.12-2.12 l-1.41,1.41L17.34,20L23,14.34z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#message-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=message-->
<g id="message"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#launch-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=launch-->
<g id="launch"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#filter-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=filter-->
<g id="filter"><path d="M0,0h24 M24,24H0" fill="none"/><path d="M4.25,5.61C6.27,8.2,10,13,10,13v6c0,0.55,0.45,1,1,1h2c0.55,0,1-0.45,1-1v-6c0,0,3.72-4.8,5.74-7.39 C20.25,4.95,19.78,4,18.95,4H5.04C4.21,4,3.74,4.95,4.25,5.61z"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#arrow_drop_down-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=arrow_drop_down-->
<g id="arrowDropDown"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 10l5 5 5-5z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#arrow_drop_up-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=arrow_drop_up-->
<g id="arrowDropUp"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 14l5-5 5 5z"/></g>
<!-- This is just a placeholder, i.e. an empty icon that has the same size as a normal icon. -->
<g id="placeholder"><path d="M0 0h24v24H0z" fill="none"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#insert_photo-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=insert_photo-->
<g id="insert-photo"><path d="M0 0h24v24H0z" fill="none"/><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#download-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=download-->
<g id="download"><path d="M0 0h24v24H0z" fill="none"/><path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#system_update-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=system_update-->
<g id="system-update"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#swap_horiz-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=swap_horiz-->
<g id="swapHoriz"><path d="M0 0h24v24H0z" fill="none"/><path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/icons/#link-->
+ <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=link-->
<g id="link"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></g>
<!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Aplay_arrow-->
<g id="playArrow"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></g>
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-limited-text/gr-limited-text.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
index 9bb112e..6b5fc39 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import {customElement, property} from 'lit/decorators';
-import {html, LitElement} from 'lit';
+import {css, html, LitElement} from 'lit';
import '../gr-tooltip-content/gr-tooltip-content';
declare global {
@@ -38,13 +38,19 @@
/** The maximum length for the text to display before truncating. */
@property({type: Number})
- limit = 0;
+ limit = 25;
@property({type: String})
tooltip?: string;
static override get styles() {
- return [];
+ return [
+ css`
+ :host {
+ white-space: nowrap;
+ }
+ `,
+ ];
}
override render() {
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 77a5dfb..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
@@ -200,8 +200,8 @@
}
`;
- override connectedCallback() {
- super.connectedCallback();
+ constructor() {
+ super();
this.setupButtonHoverHandler();
}
@@ -220,16 +220,17 @@
private setupButtonHoverHandler() {
subscribe(
this,
- this.expandButtonsHover.pipe(
- switchMap(e => {
- if (e.eventType === 'leave') {
- // cancel any previous delay
- // for mouse enter
- return EMPTY;
- }
- return of(e).pipe(delay(500));
- })
- ),
+ () =>
+ this.expandButtonsHover.pipe(
+ switchMap(e => {
+ if (e.eventType === 'leave') {
+ // cancel any previous delay
+ // for mouse enter
+ return EMPTY;
+ }
+ return of(e).pipe(delay(500));
+ })
+ ),
({buttonType, linesToExpand}) => {
fire(this, 'diff-context-button-hovered', {
buttonType,
@@ -461,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/models/dependency.ts b/polygerrit-ui/app/models/dependency.ts
index f39fa32..9f7398d 100644
--- a/polygerrit-ui/app/models/dependency.ts
+++ b/polygerrit-ui/app/models/dependency.ts
@@ -102,7 +102,7 @@
* Type Safety
* ---
*
- * Dependency injection is guaranteed npmtype-safe by construction due to the
+ * Dependency injection is guaranteed type-safe by construction due to the
* typing of the token used to tie together dependency providers and dependency
* consumers.
*
@@ -153,14 +153,17 @@
dependency: DependencyToken<T>,
provider: Provider<T>
) {
- const hostProviders = (host[PROVIDERS_SYMBOL] ||= new Map());
+ const hostProviders = (host[PROVIDERS_SYMBOL] ||= new Map<
+ DependencyToken<unknown>,
+ DependencyProvider<unknown>
+ >());
const oldController = hostProviders.get(dependency);
if (oldController) {
host.removeController(oldController);
oldController.hostDisconnected();
}
const controller = new DependencyProvider<T>(host, dependency, provider);
- hostProviders.set(dependency, provider);
+ hostProviders.set(dependency, controller);
host.addController(controller);
}
@@ -173,7 +176,10 @@
host: ReactiveControllerHost & HTMLElement & Registrations,
dependency: DependencyToken<T>
): Provider<T> {
- const hostResolvers = (host[RESOLVERS_SYMBOL] ||= new Map());
+ const hostResolvers = (host[RESOLVERS_SYMBOL] ||= new Map<
+ DependencyToken<unknown>,
+ Provider<unknown>
+ >());
let resolver = hostResolvers.get(dependency);
if (!resolver) {
const controller = new DependencySubscriber(host, dependency);
@@ -181,7 +187,7 @@
resolver = () => controller.get();
hostResolvers.set(dependency, resolver);
}
- return resolver;
+ return resolver as Provider<T>;
}
/**
@@ -274,7 +280,7 @@
}
/**
- * A resolved dependency is valid within the econnectd lifetime of a component,
+ * A resolved dependency is valid within the connected lifetime of a component,
* namely between connectedCallback and disconnectedCallback.
*/
interface ResolvedDependency<T> {
diff --git a/polygerrit-ui/app/models/di-provider-element_test.ts b/polygerrit-ui/app/models/di-provider-element_test.ts
index 83feac7..36d73e5 100644
--- a/polygerrit-ui/app/models/di-provider-element_test.ts
+++ b/polygerrit-ui/app/models/di-provider-element_test.ts
@@ -26,9 +26,13 @@
@state()
private injectedValue = '';
- override connectedCallback() {
- super.connectedCallback();
- subscribe(this, this.getModel(), value => (this.injectedValue = value));
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getModel(),
+ value => (this.injectedValue = value)
+ );
}
override render() {
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
index 95e7f2a..5cb57aa 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
@@ -33,13 +33,7 @@
import {allSettled, isFulfilled} from '../../utils/async-util';
import {notUndefined} from '../../types/types';
import {accountKey} from '../../utils/account-util';
-
-// TODO(TS): enum name doesn't follow typescript style guide rules
-// Rename it
-export enum SUGGESTIONS_PROVIDERS_USERS_TYPES {
- REVIEWER = 'reviewers',
- CC = 'ccs',
-}
+import {ReviewerState} from '../../api/rest-api';
export interface ReviewerSuggestionsProvider {
getSuggestions(input: string): Promise<Suggestion[]>;
@@ -55,7 +49,7 @@
constructor(
private restApi: RestApiService,
- private type: SUGGESTIONS_PROVIDERS_USERS_TYPES,
+ private type: ReviewerState.REVIEWER | ReviewerState.CC,
private config: ServerInfo | undefined,
private loggedIn: boolean,
...changeNumbers: NumericChangeId[]
@@ -112,8 +106,8 @@
private getSuggestionsForChange(
changeNumber: NumericChangeId,
input: string
- ): Promise<Suggestion[] | undefined> {
- return this.type === SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER
+ ): Promise<SuggestedReviewerInfo[] | undefined> {
+ return this.type === ReviewerState.REVIEWER
? this.restApi.getChangeSuggestedReviewers(changeNumber, input)
: this.restApi.getChangeSuggestedCCs(changeNumber, input);
}
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
index 3d1a15f..3dc30dd 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.ts
@@ -16,10 +16,7 @@
*/
import '../../test/common-test-setup-karma';
-import {
- GrReviewerSuggestionsProvider,
- SUGGESTIONS_PROVIDERS_USERS_TYPES,
-} from './gr-reviewer-suggestions-provider';
+import {GrReviewerSuggestionsProvider} from './gr-reviewer-suggestions-provider';
import {getAppContext} from '../../services/app-context';
import {stubRestApi} from '../../test/test-utils';
import {
@@ -27,6 +24,7 @@
GroupId,
GroupName,
NumericChangeId,
+ ReviewerState,
} from '../../api/rest-api';
import {
SuggestedReviewerAccountInfo,
@@ -70,7 +68,7 @@
]);
provider = new GrReviewerSuggestionsProvider(
getAppContext().restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER,
+ ReviewerState.REVIEWER,
createServerInfo(),
true,
change._number
@@ -90,7 +88,7 @@
// not logged in
provider = new GrReviewerSuggestionsProvider(
getAppContext().restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER,
+ ReviewerState.REVIEWER,
createServerInfo(),
false,
change._number
@@ -108,7 +106,7 @@
.resolves([suggestion2, suggestion3]);
provider = new GrReviewerSuggestionsProvider(
getAppContext().restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER,
+ ReviewerState.REVIEWER,
createServerInfo(),
true,
...[change._number, 43 as NumericChangeId]
@@ -128,7 +126,7 @@
.resolves([suggestion2, suggestion3]);
provider = new GrReviewerSuggestionsProvider(
getAppContext().restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES.CC,
+ ReviewerState.CC,
createServerInfo(),
true,
...[change._number, 43 as NumericChangeId]
@@ -172,7 +170,7 @@
provider = new GrReviewerSuggestionsProvider(
getAppContext().restApiService,
- SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER,
+ ReviewerState.REVIEWER,
{
...createServerInfo(),
user: {
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/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