Merge "Fix infinite update loop in gr-autogrow-textarea"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index aadfe1c..836b93a 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2293,6 +2293,21 @@
}
----
+[[cache-def]]
+== Cache Definitions
+
+Plugins can request a view of the definitions of all registered caches by
+injecting a `DynamicMap<CacheDef<?,?>>`. This will allow plugins to inspect what
+caches are currently registered and what their schema looks like.
+
+[source,java]
+----
+@Inject
+public MyClass(DynamicMap<CacheDef<?, ?>> cacheMap) {
+ // your code
+}
+----
+
[[secure-store]]
== SecureStore
diff --git a/java/com/google/gerrit/server/cache/CacheModule.java b/java/com/google/gerrit/server/cache/CacheModule.java
index 85c5cc56..f7ed408 100644
--- a/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/java/com/google/gerrit/server/cache/CacheModule.java
@@ -37,6 +37,7 @@
public static final String PERSISTENT_MODULE = "cache-persistent";
private static final TypeLiteral<Cache<?, ?>> ANY_CACHE = new TypeLiteral<>() {};
+ private static final TypeLiteral<CacheDef<?, ?>> ANY_CACHE_DEF = new TypeLiteral<>() {};
/**
* Declare a named in-memory cache.
@@ -182,7 +183,7 @@
@SuppressWarnings("unchecked")
Key<CacheDef<K, V>> cacheDefKey = (Key<CacheDef<K, V>>) Key.get(cacheDefType, named);
bind(cacheDefKey).toInstance(m);
-
+ bind(ANY_CACHE_DEF).annotatedWith(Exports.named(name)).to(cacheDefKey);
m.maximumWeight(1024);
}
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 39dfba2..ec251d0 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -117,6 +117,7 @@
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.gerrit.server.cache.CacheDef;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.change.AbandonOp;
import com.google.gerrit.server.change.AccountPatchReviewStore;
@@ -367,6 +368,7 @@
bind(GitReferenceUpdated.class);
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
+ DynamicMap.mapOf(binder(), new TypeLiteral<CacheDef<?, ?>>() {});
DynamicSet.setOf(binder(), CacheRemovalListener.class);
DynamicMap.mapOf(binder(), CapabilityDefinition.class);
DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
diff --git a/javatests/com/google/gerrit/server/cache/CacheFactoryIT.java b/javatests/com/google/gerrit/server/cache/CacheFactoryIT.java
new file mode 100644
index 0000000..593685e
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/CacheFactoryIT.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
+import com.google.inject.Inject;
+import java.util.Optional;
+import java.util.stream.StreamSupport;
+import org.junit.Test;
+
+public class CacheFactoryIT extends AbstractDaemonTest {
+ private static final String CACHE_NAME = "dummy-cache";
+
+ @Inject private DynamicMap<CacheDef<?, ?>> cacheDefs;
+
+ @Override
+ public com.google.inject.Module createModule() {
+ return new TestModule();
+ }
+
+ @Test
+ public void newCacheDefIsAvailableInDynamicMap() {
+ assertThat(cacheDefs).isNotNull();
+
+ Optional<? extends CacheDef<?, ?>> def =
+ StreamSupport.stream(cacheDefs.spliterator(), false)
+ .filter(e -> e.getExportName().equals(CACHE_NAME))
+ .map(Extension::get)
+ .findFirst();
+ assertThat(def).isPresent();
+ assertThat(def.get().keyType().getRawType()).isEqualTo(String.class);
+ assertThat(def.get().valueType().getRawType()).isEqualTo(String.class);
+ }
+
+ public static class TestModule extends CacheModule {
+
+ @Override
+ protected void configure() {
+ cache(CACHE_NAME, String.class, String.class);
+ }
+ }
+}
diff --git a/polygerrit-ui/GEMINI.md b/polygerrit-ui/GEMINI.md
index 1dc48ad..015a548 100644
--- a/polygerrit-ui/GEMINI.md
+++ b/polygerrit-ui/GEMINI.md
@@ -23,6 +23,7 @@
- `npm run eslintfix`: Fix lint errors in all files
**Note**: Imports should NOT have spaces around braces (e.g., `import {css} from 'lit';`, not `import { css } from 'lit';`).
+**Note**: Do not use `_` prefix for private properties or variables.
## Key Commands
diff --git a/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog.ts b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog.ts
index 26d6e9d..a990898 100644
--- a/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog.ts
@@ -313,8 +313,15 @@
}
private getNumParents() {
- return this.change?.revisions[this.change.current_revision].commit?.parents
- .length;
+ if (
+ !this.change ||
+ !this.change.current_revision ||
+ !this.change.revisions
+ ) {
+ return 0;
+ }
+ const revision = this.change.revisions[this.change.current_revision];
+ return revision?.commit?.parents.length ?? 0;
}
private async loadPatchContent() {
@@ -323,13 +330,11 @@
const content = await this.restApiService.getPatchContent(
this.change._number,
this.patchNum,
- this.context
+ this.context,
+ () => fireError(this, 'Failed to get patch content')
);
this.loading = false;
- if (!content) {
- fireError(this, 'Failed to get patch content');
- return;
- }
+ if (!content) return;
this.patchContent = content;
}
diff --git a/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog_test.ts
index 699035e..8165a0f 100644
--- a/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-ai-prompt-dialog/gr-ai-prompt-dialog_test.ts
@@ -9,12 +9,14 @@
import {GrAiPromptDialog} from './gr-ai-prompt-dialog';
import {createParsedChange} from '../../../test/test-data-generators';
import {CommitId, PatchSetNum} from '../../../api/rest-api';
-import {stubRestApi} from '../../../test/test-utils';
+import {stubRestApi, waitUntil} from '../../../test/test-utils';
suite('gr-ai-prompt-dialog test', () => {
let element: GrAiPromptDialog;
+ let getPatchContentStub: sinon.SinonStub;
setup(async () => {
- stubRestApi('getPatchContent').returns(Promise.resolve('test code'));
+ getPatchContentStub = stubRestApi('getPatchContent');
+ getPatchContentStub.resolves('test code');
element = await fixture(html`<gr-ai-prompt-dialog></gr-ai-prompt-dialog>`);
element.change = createParsedChange();
element.change.revisions['abc'].commit!.parents = [
@@ -142,4 +144,27 @@
</section>`
);
});
+
+ test('handles failed patch content fetch', async () => {
+ getPatchContentStub.callsFake((_c, _p, _ctx, errFn) => {
+ if (errFn) errFn();
+ return Promise.resolve(undefined);
+ });
+ const fireStub = sinon.stub(element, 'dispatchEvent');
+
+ element.open();
+
+ await waitUntil(() => fireStub.called);
+
+ assert.isTrue(fireStub.called);
+ const events = fireStub.args.map(arg => arg[0]);
+ assert.isTrue(
+ events.some(
+ event =>
+ event.type === 'show-error' &&
+ (event as CustomEvent).detail.message ===
+ 'Failed to get patch content'
+ )
+ );
+ });
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts
index b561da5..4a700d7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_screenshot_test.ts
@@ -294,7 +294,7 @@
try {
// Wait for all nested components to render
- await new Promise(resolve => setTimeout(resolve, 500));
+ await new Promise(resolve => setTimeout(resolve, 300));
await element.updateComplete;
if (element.fileList) {
await element.fileList.updateComplete;
@@ -326,7 +326,7 @@
try {
// Wait for all nested components to render
- await new Promise(resolve => setTimeout(resolve, 500));
+ await new Promise(resolve => setTimeout(resolve, 300));
await element.updateComplete;
if (element.fileList) {
await element.fileList.updateComplete;
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 c9f2604..207aaf6 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
@@ -628,7 +628,7 @@
element.loading = false;
await element.updateComplete;
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
});
teardown(() => {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 5bccb07..95df2fac 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -280,12 +280,11 @@
if (!this.change || !this.patchNum) return;
const patchContent = await this.restApiService.getPatchContent(
this.change._number,
- this.patchNum
+ this.patchNum,
+ undefined,
+ () => fireError(this, 'Failed to get patch content')
);
- if (!patchContent) {
- fireError(this, 'Failed to get patch content');
- return;
- }
+ if (!patchContent) return;
await copyToClipboard(patchContent, 'patch file content');
this.handleCloseTap(e);
}
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
index d71c2af..c107c20 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
@@ -350,7 +350,10 @@
});
test('handles failed patch content fetch', async () => {
- getPatchContentStub.resolves(undefined);
+ getPatchContentStub.callsFake((_c, _p, _ctx, errFn) => {
+ if (errFn) errFn();
+ return Promise.resolve(undefined);
+ });
const copyButton = queryAndAssert<GrButton>(
element,
@@ -362,20 +365,5 @@
assert.isFalse(copyToClipboardStub.called);
assert.isTrue(fireStub.called);
});
-
- test('handles error during patch content fetch', async () => {
- const error = new Error('Network error');
- getPatchContentStub.rejects(error);
-
- const copyButton = queryAndAssert<GrButton>(
- element,
- '#copy-clipboard-button'
- );
- copyButton.click();
-
- await waitUntil(() => getPatchContentStub.called);
- assert.isFalse(copyToClipboardStub.called);
- assert.isFalse(fireStub.called);
- });
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index e499789..7b34016 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -282,6 +282,9 @@
@state()
cleanlyMergedOldPaths: string[] = [];
+ // Private but used in tests.
+ patchChange?: PatchChange;
+
private cancelForEachDiff?: () => void;
@state()
@@ -927,6 +930,7 @@
this.updateDiffPreferences();
}
if (changedProperties.has('files')) {
+ this.patchChange = undefined;
this.filesChanged();
this.numFilesShown = Math.min(this.files.length, DEFAULT_NUM_FILES_SHOWN);
this.updateSizeBarLayout();
@@ -1892,11 +1896,12 @@
// Private but used in tests.
calculatePatchChange(): PatchChange {
+ if (this.patchChange) return this.patchChange;
const magicFilesExcluded = this.files.filter(
file => !isMagicPath(file.__path)
);
- return magicFilesExcluded.reduce((acc, obj) => {
+ this.patchChange = magicFilesExcluded.reduce((acc, obj) => {
const inserted = obj.lines_inserted ? obj.lines_inserted : 0;
const deleted = obj.lines_deleted ? obj.lines_deleted : 0;
const total_size = obj.size && obj.binary ? obj.size : 0;
@@ -1913,6 +1918,7 @@
total_size: acc.total_size + total_size,
};
}, createDefaultPatchChange());
+ return this.patchChange;
}
private toggleHideAllCommentsAndCodePointers() {
diff --git a/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts b/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
index 6cba4d9..b8af64a 100644
--- a/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-flows/gr-flows_test.ts
@@ -59,7 +59,7 @@
let userModel: UserModel;
setup(async () => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
changeModel = testResolver(changeModelToken);
userModel = testResolver(userModelToken);
diff --git a/polygerrit-ui/app/elements/chat-panel/prompt-box.ts b/polygerrit-ui/app/elements/chat-panel/prompt-box.ts
index bbfd052..4474b4f 100644
--- a/polygerrit-ui/app/elements/chat-panel/prompt-box.ts
+++ b/polygerrit-ui/app/elements/chat-panel/prompt-box.ts
@@ -269,6 +269,8 @@
color: var(--primary-text-color);
font: inherit;
font-size: 16px; /* $font-size-large */
+ /* Explicitly set line-height to ensure consistent height in tests (e.g. 20px). */
+ line-height: 20px;
border: none;
outline: none;
resize: none;
diff --git a/polygerrit-ui/app/elements/chat-panel/prompt-box_test.ts b/polygerrit-ui/app/elements/chat-panel/prompt-box_test.ts
index 00059b1..429bba7 100644
--- a/polygerrit-ui/app/elements/chat-panel/prompt-box_test.ts
+++ b/polygerrit-ui/app/elements/chat-panel/prompt-box_test.ts
@@ -57,7 +57,7 @@
spellcheck="false"
aria-label="Ask Gemini"
placeholder="Enter a prompt here..."
- style="height: 18px;"
+ style="height: 20px;"
></textarea>
</div>
</div>
@@ -256,6 +256,6 @@
textarea.dispatchEvent(new Event('input'));
await element.updateComplete;
- assert.equal(textarea.style.height, '90px');
+ assert.equal(textarea.style.height, '100px');
});
});
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
index 1655d6f..b52c84c 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
@@ -17,7 +17,7 @@
setup(async () => {
const fakeNow = new Date('Sep 26 2022 12:00:00');
- sinon.useFakeTimers(fakeNow);
+ sinon.useFakeTimers({now: fakeNow, shouldClearNativeTimers: true});
element = await fixture<GrHovercardRun>(html`
<gr-hovercard-run class="hovered"></gr-hovercard-run>
`);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index 3ff3df2..6a4d95b 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -266,13 +266,17 @@
let urlPromise: MockPromise<string>;
setup(() => {
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
stubRestApi('addRepoNameToCache');
urlPromise = mockPromise<string>();
redirectStub = sinon
.stub(router, 'redirect')
.callsFake(urlPromise.resolve);
router._testOnly_startRouter();
- clock = sinon.useFakeTimers();
+ });
+
+ teardown(() => {
+ clock.restore();
});
test('no blockers: normal redirect', async () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 7aaf3bb..f8fd10e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -392,7 +392,9 @@
});
test('keyboard shortcuts', async () => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({
+ toFake: ['Date'],
+ });
element.changeNum = 42 as NumericChangeId;
browserModel.setScreenWidth(0);
element.patchNum = 10 as RevisionPatchSetNum;
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
index f8edb8c..11df7d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
@@ -97,7 +97,7 @@
);
}
- override updated(changedProperties: PropertyValues) {
+ override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('accounts')) {
if (
this.forceFetch &&
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 61c23d8..165dc03 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -9,6 +9,7 @@
import {sortComments} from '../../../utils/comment-util';
import {GrCommentThread} from './gr-comment-thread';
import {
+ AccountId,
CommentInfo,
CommentThread,
DraftInfo,
@@ -55,14 +56,14 @@
import {suggestionsServiceToken} from '../../../services/suggestions/suggestions-service';
const c1: CommentInfo = {
- author: {name: 'Kermit'},
+ author: {name: 'Kermit', _account_id: 1 as AccountId},
id: 'the-root' as UrlEncodedCommentId,
message: 'start the conversation',
updated: '2021-11-01 10:11:12.000000000' as Timestamp,
};
const c2: CommentInfo = {
- author: {name: 'Ms Piggy'},
+ author: {name: 'Ms Piggy', _account_id: 2 as AccountId},
id: 'the-reply' as UrlEncodedCommentId,
message: 'keep it going',
updated: '2021-11-02 10:11:12.000000000' as Timestamp,
@@ -70,7 +71,7 @@
};
const c3: DraftInfo = {
- author: {name: 'Kermit'},
+ author: {name: 'Kermit', _account_id: 1 as AccountId},
id: 'the-draft' as UrlEncodedCommentId,
message: 'stop it',
updated: '2021-11-03 10:11:12.000000000' as Timestamp,
@@ -79,7 +80,7 @@
};
const commentWithContext = {
- author: {name: 'Kermit'},
+ author: {name: 'Kermit', _account_id: 1 as AccountId},
id: 'the-draft' as UrlEncodedCommentId,
message: 'just for context',
updated: '2021-11-03 10:11:12.000000000' as Timestamp,
@@ -343,6 +344,7 @@
.stub(testResolver(commentsModelToken), 'saveDraft')
.returns(savePromise);
stubAdd = sinon.stub(testResolver(commentsModelToken), 'addNewDraft');
+ sinon.stub(testResolver(commentsModelToken), 'discardDraft');
element.thread = createThread(c1, {...c2, unresolved: true});
await element.updateComplete;
@@ -707,7 +709,7 @@
...createComment(),
id: '123' as any,
message: 'Test comment',
- author: {name: 'Test User'},
+ author: {name: 'Test User', _account_id: 12345 as AccountId},
patch_set: 1 as RevisionPatchSetNum,
line: 10,
path: 'test.txt',
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 1f5a9eb..a4fc1e5 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -41,6 +41,7 @@
import {SinonStub, SinonStubbedMember} from 'sinon';
import {assert, fixture, html} from '@open-wc/testing';
import {GrButton} from '../gr-button/gr-button';
+import {GrTooltipContent} from '../gr-tooltip-content/gr-tooltip-content';
import {testResolver} from '../../../test/common-test-setup';
import {
CommentsModel,
@@ -384,7 +385,17 @@
<gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog">
</gr-confirm-delete-comment-dialog>
</dialog>
- `
+ `,
+ {ignoreAttributes: ['title']}
+ );
+
+ const tooltip = queryAndAssert<GrTooltipContent>(
+ element,
+ '.draftTooltip'
+ );
+ assert.equal(
+ tooltip.getAttribute('title') || (tooltip as any).originalTitle,
+ "This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key."
);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts
index 847d18a..60d8514 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.ts
@@ -49,7 +49,7 @@
};
setup(() => {
- clock = useFakeTimers();
+ clock = useFakeTimers({shouldClearNativeTimers: true});
stubRestApi('getAccount').resolves({
name: 'Judy Hopps',
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
index 3b07ddd..211d556 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.ts
@@ -24,7 +24,7 @@
let bodyStub: sinon.SinonStub;
setup(() => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
stubRestApi('getAccount').returns(
Promise.resolve({name: 'Judy Hopps', registered_on: '' as Timestamp})
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
index 030b4a1..ec49dd7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
@@ -48,7 +48,7 @@
let authService: AuthService;
setup(() => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
cache = new SiteBasedCache();
fetchPromisesCache = new FetchPromisesCache();
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 c5fef11..1fb0664 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -367,6 +367,15 @@
}
protected override willUpdate(changedProperties: PropertyValues<this>): void {
+ if (changedProperties.has('groups')) {
+ if (this.groups?.length > 0) {
+ this.loading = false;
+ }
+ }
+ if (changedProperties.has('diff')) {
+ this.loading = true;
+ }
+
if (
changedProperties.has('diff') ||
changedProperties.has('path') ||
@@ -472,11 +481,6 @@
// diffChanged relies on diffElement having been rendered.
this.diffChanged();
}
- if (changedProperties.has('groups')) {
- if (this.groups?.length > 0) {
- this.loading = false;
- }
- }
}
override render() {
@@ -688,7 +692,6 @@
}
private diffChanged() {
- this.loading = true;
if (this.diff && this.diffElement) {
this.diffSelection.init(this.diff, this.diffElement);
this.highlights.init(this.diffElement, this);
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index c43f7a15..94b3d0f 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -99,7 +99,7 @@
});
test('register and fetch', async () => {
- const clock = sinon.useFakeTimers();
+ const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
@@ -131,7 +131,7 @@
});
test('fetch throttle', async () => {
- const clock = sinon.useFakeTimers();
+ const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
@@ -337,7 +337,7 @@
});
test('polls for changes', async () => {
- const clock = sinon.useFakeTimers();
+ const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
@@ -364,7 +364,7 @@
});
test('does not poll when config specifies 0 seconds', async () => {
- const clock = sinon.useFakeTimers();
+ const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
let change: ParsedChangeInfo | undefined = undefined;
testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index add53ac..5327041 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -25,7 +25,7 @@
"resemblejs": "rsmbl/Resemble.js#66a55c5bfc3bda2303ad632ee8ce3c727b415917",
"rxjs": "^6.6.7",
"safevalues": "^1.2.0",
- "web-vitals": "^3.5.2"
+ "web-vitals": "^5.1.0"
},
"dependencies // comments": {
"@polymer/polymer": [
diff --git a/polygerrit-ui/app/scripts/polymer-resin-install.ts b/polygerrit-ui/app/scripts/polymer-resin-install.ts
index 527df12..584d83a 100644
--- a/polygerrit-ui/app/scripts/polymer-resin-install.ts
+++ b/polygerrit-ui/app/scripts/polymer-resin-install.ts
@@ -48,13 +48,18 @@
export const _testOnly_defaultResinReportHandler =
security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER;
+let resinInstalled = false;
export function installPolymerResin(
safeTypesBridge: SafeTypeBridge,
reportHandler = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER
) {
+ if (resinInstalled) {
+ return;
+ }
window.security.polymer_resin.install({
allowedIdentifierPrefixes: [''],
reportHandler,
safeTypesBridge,
});
+ resinInstalled = true;
}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
index 28be742..8840cd2 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.ts
@@ -56,7 +56,7 @@
let fakeFetch: sinon.SinonStub;
let clock: SinonFakeTimers;
setup(() => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
fakeFetch = sinon.stub(window, 'fetch');
});
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 4ce0dd7..86757ae 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -15,7 +15,12 @@
LifeCycle,
Timing,
} from '../../constants/reporting';
-import {Metric, onCLS, onINP, onLCP} from 'web-vitals';
+import {
+ MetricWithAttribution,
+ onCLS,
+ onINP,
+ onLCP,
+} from 'web-vitals/attribution';
import {getEventPath, isElementTarget} from '../../utils/dom-util';
import {Finalizable} from '../../types/types';
@@ -269,7 +274,7 @@
}
export function initWebVitals(reportingService: ReportingService) {
- function reportWebVitalMetric(name: Timing, metric: Metric) {
+ function reportWebVitalMetric(name: Timing, metric: MetricWithAttribution) {
let score = metric.value;
// CLS good score is 0.1 and poor score is 0.25. Logging system
// prefers integers, so we multiple by 100;
@@ -285,6 +290,7 @@
navigationType: metric.navigationType,
rating: metric.rating,
entries: metric.entries,
+ attribution: metric.attribution,
}
);
}
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 d1d6638..3398967 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
@@ -3651,7 +3651,8 @@
async getPatchContent(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
- context?: number
+ context?: number,
+ errFn?: ErrorCallback
): Promise<string | undefined> {
const url = await this._changeBaseURL(changeNum, patchNum);
const params: {[key: string]: string | number} = {
@@ -3665,6 +3666,7 @@
url: `${url}/patch`,
params,
anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/patch`,
+ errFn,
});
if (!response?.ok) return undefined;
return await response.text();
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index 311dbd3..9b41a9f 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -842,7 +842,8 @@
getPatchContent(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
- context?: number
+ context?: number,
+ errFn?: ErrorCallback
): Promise<string | undefined>;
getImagesForDiff(
diff --git a/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts b/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
index 2609002..1162596 100644
--- a/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
+++ b/polygerrit-ui/app/services/scheduler/retry-scheduler_test.ts
@@ -17,7 +17,7 @@
let fakeScheduler: FakeScheduler<number>;
let scheduler: Scheduler<number>;
setup(() => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
fakeScheduler = new FakeScheduler<number>();
scheduler = new RetryScheduler<number>(fakeScheduler, 3, 50, 1);
});
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 0322527..ace488c 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -44,12 +44,21 @@
declare global {
interface Window {
sinon: typeof sinon;
+ litIssuedWarnings?: Set<string>;
}
}
window.sinon = sinon;
+// Suppress 'Lit is in dev mode' warning. This is a development build, but we want
+// to keep the test output clean from unnecessary warnings.
+window.litIssuedWarnings = window.litIssuedWarnings || new Set();
+window.litIssuedWarnings.add('dev-mode');
installPolymerResin(safeTypesBridge, (isViolation, fmt, ...args) => {
+ // Suppress 'initResin' log message from polymer-resin.
+ if (fmt === 'initResin') {
+ return;
+ }
const log = _testOnly_defaultResinReportHandler;
log(isViolation, fmt, ...args);
if (isViolation) {
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 400a7e6..422d00a 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -231,7 +231,7 @@
return Promise.resolve({});
},
getChange(): Promise<ChangeInfo | undefined> {
- throw new Error('getChange() not implemented by RestApiMock.');
+ return Promise.resolve(undefined);
},
getChangeActionURL(): Promise<string> {
return Promise.resolve('');
diff --git a/polygerrit-ui/app/utils/async-util_test.ts b/polygerrit-ui/app/utils/async-util_test.ts
index 4383cbd..054a1b8 100644
--- a/polygerrit-ui/app/utils/async-util_test.ts
+++ b/polygerrit-ui/app/utils/async-util_test.ts
@@ -32,7 +32,11 @@
suite('timeoutPromise', () => {
let clock: SinonFakeTimers;
setup(() => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
+ });
+
+ teardown(() => {
+ clock.restore();
});
test('simple test', async () => {
let resolved = false;
@@ -77,7 +81,11 @@
suite('DelayedPromise', () => {
let clock: SinonFakeTimers;
setup(() => {
- clock = sinon.useFakeTimers();
+ clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
+ });
+
+ teardown(() => {
+ clock.restore();
});
test('It resolves after timeout', async () => {
@@ -137,13 +145,15 @@
100
);
let hasCanceled = false;
- promise.then((_value: number) => {
- assert.fail();
- });
- promise.catch((reason?: any) => {
- hasCanceled = true;
- assert.strictEqual(reason, 'because');
- });
+ promise.then(
+ (_value: number) => {
+ assert.fail();
+ },
+ (reason?: any) => {
+ hasCanceled = true;
+ assert.strictEqual(reason, 'because');
+ }
+ );
await waitEventLoop();
assert.isFalse(hasCanceled);
promise.cancel('because');
diff --git a/polygerrit-ui/app/utils/autocomplete-cache_test.ts b/polygerrit-ui/app/utils/autocomplete-cache_test.ts
index 970436b..9aca3ec 100644
--- a/polygerrit-ui/app/utils/autocomplete-cache_test.ts
+++ b/polygerrit-ui/app/utils/autocomplete-cache_test.ts
@@ -3,6 +3,7 @@
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import '../test/common-test-setup';
import {AutocompleteCache} from './autocomplete-cache';
import {assert} from '@open-wc/testing';
diff --git a/polygerrit-ui/app/utils/link-util_test.ts b/polygerrit-ui/app/utils/link-util_test.ts
index 1574b3e..7b09320 100644
--- a/polygerrit-ui/app/utils/link-util_test.ts
+++ b/polygerrit-ui/app/utils/link-util_test.ts
@@ -3,6 +3,7 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import '../test/common-test-setup';
import {linkifyUrlsAndApplyRewrite} from './link-util';
import {assert} from '@open-wc/testing';
diff --git a/polygerrit-ui/app/utils/message-util_test.ts b/polygerrit-ui/app/utils/message-util_test.ts
index bed9376..052c6a5 100644
--- a/polygerrit-ui/app/utils/message-util_test.ts
+++ b/polygerrit-ui/app/utils/message-util_test.ts
@@ -3,6 +3,7 @@
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import '../test/common-test-setup';
import {
getCodeReviewVotesFromMessage,
getRevertCreatedChangeIds,
diff --git a/polygerrit-ui/app/workers/service-worker-class_test.ts b/polygerrit-ui/app/workers/service-worker-class_test.ts
index 5368911..fd36168 100644
--- a/polygerrit-ui/app/workers/service-worker-class_test.ts
+++ b/polygerrit-ui/app/workers/service-worker-class_test.ts
@@ -24,7 +24,11 @@
registration: {
showNotification: () => {},
},
- } as {} as ServiceWorkerGlobalScope;
+ clients: {
+ matchAll: () => Promise.resolve([]),
+ openWindow: () => Promise.resolve(undefined),
+ },
+ } as unknown as ServiceWorkerGlobalScope;
serviceWorker = new ServiceWorker(moctCtx);
serviceWorker.allowBrowserNotificationsPreference = true;
});
@@ -44,12 +48,16 @@
},
},
};
- sinon.useFakeTimers(t3);
+ const clock = sinon.useFakeTimers({
+ now: t3,
+ shouldClearNativeTimers: true,
+ });
sinon
.stub(serviceWorker, 'getLatestAttentionSetChanges')
.returns(Promise.resolve([change]));
const changes = await serviceWorker.getChangesToNotify(account);
assert.equal(changes[0], change);
+ clock.restore();
});
test('check race condition', async () => {
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index c68262e..c26d0dd 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -420,10 +420,10 @@
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-web-vitals@^3.5.2:
- version "3.5.2"
- resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-3.5.2.tgz#5bb58461bbc173c3f00c2ddff8bfe6e680999ca9"
- integrity sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==
+web-vitals@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-5.1.0.tgz#2f117e92c8c4eeb107cb163cbb482ac20d685ebd"
+ integrity sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==
wrappy@1:
version "1.0.2"
diff --git a/polygerrit-ui/web-test-runner.config.mjs b/polygerrit-ui/web-test-runner.config.mjs
index f9b309d..0621a8f 100644
--- a/polygerrit-ui/web-test-runner.config.mjs
+++ b/polygerrit-ui/web-test-runner.config.mjs
@@ -4,14 +4,16 @@
import { visualRegressionPlugin } from '@web/test-runner-visual-regression/plugin';
import { playwrightLauncher } from '@web/test-runner-playwright';
-function testRunnerHtmlFactory() {
+const runUnderBazel = !!process.env['RUNFILES_DIR'];
+
+function testRunnerHtmlFactory(prefix) {
return (testFramework) => `
<!DOCTYPE html>
<html>
<head>
- <link rel="stylesheet" href="polygerrit-ui/app/styles/main.css">
- <link rel="stylesheet" href="polygerrit-ui/app/styles/fonts.css">
- <link rel="stylesheet" href="polygerrit-ui/app/styles/material-icons.css">
+ <link rel="stylesheet" href="${prefix}app/styles/main.css">
+ <link rel="stylesheet" href="${prefix}app/styles/fonts.css">
+ <link rel="stylesheet" href="${prefix}app/styles/material-icons.css">
</head>
<body>
<script type="module" src="${testFramework}"></script>
@@ -20,20 +22,18 @@
`;
}
-const runUnderBazel = !!process.env['RUNFILES_DIR'];
-
function getModulesDir() {
return runUnderBazel
? [
- path.join(process.cwd(), 'external/plugins_npm/node_modules'),
- path.join(process.cwd(), 'external/ui_npm/node_modules'),
- path.join(process.cwd(), 'external/ui_dev_npm/node_modules'),
- ]
+ path.join(process.cwd(), 'external/plugins_npm/node_modules'),
+ path.join(process.cwd(), 'external/ui_npm/node_modules'),
+ path.join(process.cwd(), 'external/ui_dev_npm/node_modules'),
+ ]
: [
- path.join(process.cwd(), 'plugins/node_modules'),
- path.join(process.cwd(), 'app/node_modules'),
- path.join(process.cwd(), 'node_modules'),
- ];
+ path.join(process.cwd(), 'plugins/node_modules'),
+ path.join(process.cwd(), 'app/node_modules'),
+ path.join(process.cwd(), 'node_modules'),
+ ];
}
function getArgValue(flag) {
@@ -54,6 +54,11 @@
const rootDir = getArgValue('--root-dir') ?? `${path.resolve(process.cwd())}/`;
const tsConfig = getArgValue('--ts-config') ?? `${pathPrefix}app/tsconfig.json`;
+// When running screenshots, we serve from the root directory, so we need to
+// prepend polygerrit-ui/ to the path.
+// When running under Bazel, we also need strictly fully qualified paths.
+const stylePathPrefix = 'polygerrit-ui/';
+
/** @type {import('@web/test-runner').TestRunnerConfig} */
const config = {
// Default is CPU cores / 2. Use default
@@ -77,22 +82,23 @@
],
files: runScreenshots
- ? [
- // If --run-screenshots is set, ONLY run screenshot tests.
- testFiles ?? `${pathPrefix}app/**/*_screenshot_test.{ts,js}`,
- `!${pathPrefix}**/node_modules/**/*`,
- ]
- : [
- // Otherwise, run all tests EXCEPT screenshot tests
- testFiles ?? `${pathPrefix}app/**/*_test.{ts,js}`,
- `!${pathPrefix}**/node_modules/**/*`,
- `!${pathPrefix}app/**/*_screenshot_test.{ts,js}`,
- ],
+ ? [
+ // If --run-screenshots is set, ONLY run screenshot tests.
+ testFiles ?? `${pathPrefix}app/**/*_screenshot_test.{ts,js}`,
+ `!${pathPrefix}**/node_modules/**/*`,
+ ]
+ : [
+ // Otherwise, run all tests EXCEPT screenshot tests
+ testFiles ?? `${pathPrefix}app/**/*_test.{ts,js}`,
+ `!${pathPrefix}**/node_modules/**/*`,
+ `!${pathPrefix}app/**/*_screenshot_test.{ts,js}`,
+ ],
port: 9876,
nodeResolve: {
modulePaths: getModulesDir(),
+ dedupe: ['lit', 'lit-html', 'lit-element'],
},
testFramework: {
@@ -127,7 +133,7 @@
// serve from gerrit root directory so that we can serve fonts from
// /lib/fonts/ for screenshots tests, see middleware.
- rootDir: runScreenshots ? '..' : rootDir,
+ rootDir: runUnderBazel ? rootDir : '..',
reporters: [defaultReporter(), summaryReporter()],
@@ -143,7 +149,7 @@
},
],
- testRunnerHtml: testRunnerHtmlFactory(),
+ testRunnerHtml: testRunnerHtmlFactory(stylePathPrefix),
};
export default config;