Merge "Some 'project' to 'repo' renaming"
diff --git a/WORKSPACE b/WORKSPACE
index da15966..ab4ab55 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -65,8 +65,8 @@
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "f10a3a12894fc3c9bf578ee5a5691769f6805c4be84359681a785a0c12e8d2b6",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.5.3/rules_nodejs-5.5.3.tar.gz"],
+ sha256 = "c29944ba9b0b430aadcaf3bf2570fece6fc5ebfb76df145c6cdad40d65c20811",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.7.0/rules_nodejs-5.7.0.tar.gz"],
)
load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
@@ -136,7 +136,7 @@
load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
node_repositories(
- node_version = "16.16.0",
+ node_version = "17.9.1",
yarn_version = "1.22.19",
)
diff --git a/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java b/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java
index 2f47107..e74af5b 100644
--- a/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java
+++ b/java/com/google/gerrit/server/notedb/CommentTimestampAdapter.java
@@ -16,6 +16,7 @@
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
+import com.google.common.annotations.VisibleForTesting;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
@@ -27,7 +28,7 @@
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle;
-import java.time.temporal.TemporalAccessor;
+import java.util.Locale;
/**
* Adapter that reads/writes {@link Timestamp}s as ISO 8601 instant in UTC.
@@ -49,6 +50,16 @@
private static final DateTimeFormatter FALLBACK =
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
+ /**
+ * Fixed format to parse date/time in the "Feb 7, 2017 2:20:30 AM" format
+ *
+ * <p>Some old comments (created in Jan-Feb 2017) can be stored in legacy format, which can't be
+ * parsed with {@link #FALLBACK} formatter if the system/default locale has been changed. We will
+ * try to parse with a fixed format if {@link #FALLBACK} doesn't work.
+ */
+ private static final DateTimeFormatter FIXED_FORMAT_FALLBACK =
+ DateTimeFormatter.ofPattern("MMM d, yyyy h:mm:ss a").withLocale(Locale.US);
+
@Override
public void write(JsonWriter out, Timestamp ts) throws IOException {
Timestamp truncated = new Timestamp(ts.getTime() / 1000 * 1000);
@@ -58,12 +69,26 @@
@Override
public Timestamp read(JsonReader in) throws IOException {
String str = in.nextString();
- TemporalAccessor ta;
try {
- ta = ISO_INSTANT.parse(str);
+ return Timestamp.from(Instant.from(ISO_INSTANT.parse(str)));
} catch (DateTimeParseException e) {
- ta = LocalDateTime.from(FALLBACK.parse(str)).atZone(ZoneId.systemDefault());
+ try {
+ return parseDateTimeWithDefaultLocaleFormat(str);
+ } catch (DateTimeParseException e2) {
+ return parseDateTimeWithFixedFormat(str);
+ }
}
- return Timestamp.from(Instant.from(ta));
+ }
+
+ public static Timestamp parseDateTimeWithDefaultLocaleFormat(String str) {
+ return Timestamp.from(
+ Instant.from(LocalDateTime.from(FALLBACK.parse(str)).atZone(ZoneId.systemDefault())));
+ }
+
+ @VisibleForTesting
+ public static Timestamp parseDateTimeWithFixedFormat(String str) {
+ return Timestamp.from(
+ Instant.from(
+ LocalDateTime.from(FIXED_FORMAT_FALLBACK.parse(str)).atZone(ZoneId.systemDefault())));
}
}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
index 347d547..7aadc08 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
@@ -18,10 +18,12 @@
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.projects.SubmitRequirementApi;
import com.google.gerrit.extensions.common.LabelDefinitionInfo;
import com.google.gerrit.extensions.common.LabelDefinitionInput;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
@@ -312,13 +314,14 @@
gApi.projects().name(project.get()).label(labelName).create(input);
}
- private void createSubmitRequirement(String name, String submitExpression, boolean canOverride)
- throws Exception {
+ @CanIgnoreReturnValue
+ private SubmitRequirementApi createSubmitRequirement(
+ String name, String submitExpression, boolean canOverride) throws Exception {
SubmitRequirementInput input = new SubmitRequirementInput();
input.name = name;
input.submittabilityExpression = submitExpression;
input.allowOverrideInChildProjects = canOverride;
- gApi.projects().name(project.get()).submitRequirement(name).create(input);
+ return gApi.projects().name(project.get()).submitRequirement(name).create(input);
}
private void assertLabelFunction(String labelName, String function) throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
index 2191f00..5a89584 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
@@ -123,6 +123,18 @@
}
@Test
+ public void fixedFallbackFormatCanParseOutputOfLegacyAdapter() {
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 7, 2017 2:20:30 AM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-07T10:20:30Z").toInstant()));
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 17, 2017 10:20:30 AM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-17T18:20:30Z").toInstant()));
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 17, 2017 02:20:30 PM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-17T22:20:30Z").toInstant()));
+ assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 07, 2017 10:20:30 PM"))
+ .isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-08T06:20:30Z").toInstant()));
+ }
+
+ @Test
public void newAdapterDisagreesWithLegacyAdapterDuringDstTransition() {
String duringJson = legacyGson.toJson(new Timestamp(MID_DST_MS));
Timestamp duringTs = legacyGson.fromJson(duringJson, Timestamp.class);
diff --git a/plugins/replication b/plugins/replication
index ced31c0..8f465cd 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit ced31c0c7d56cbb3e10a8da35a8ddd5db1bba550
+Subproject commit 8f465cd3f1f3039a937bd3d863c192e33d8347a2
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md
index 6673cdf..56b5aee 100644
--- a/polygerrit-ui/FE_Style_Guide.md
+++ b/polygerrit-ui/FE_Style_Guide.md
@@ -167,83 +167,3 @@
the element's class constructor.
Do not use appContext anywhere except the constructor of the class.
-
-**Note for legacy elements:** If a polymer element extends a LegacyElementMixin and overrides the `created()` method,
-move all code from this method to a constructor right after the call to a `super()`
-([example](#assign-dependencies-legacy-element-example)). The `created()`
-method is [deprecated](https://polymer-library.polymer-project.org/2.0/docs/about_20#lifecycle-changes) and is called
-when a super (i.e. base) class constructor is called. If you are unsure about moving the code from the `created` method
-to the class constructor, consult with the source code:
-[`LegacyElementMixin._initializeProperties`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/legacy/legacy-element-mixin.js#L318)
-and
-[`PropertiesChanged.constructor`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/mixins/properties-changed.js#L177)
-
-
-
-**Good:**
-```Javascript
-import {appContext} from `.../services/app-context.js`;
-
-export class MyCustomElement extends ...{
- constructor() {
- super(); //This is mandatory to call parent constructor
- this._userModel = appContext.userModel;
- }
- //...
- _getUserName() {
- return this._userModel.activeUserName();
- }
-}
-```
-
-**Bad:**
-```Javascript
-import {appContext} from `.../services/app-context.js`;
-
-export class MyCustomElement extends ...{
- created() {
- // Incorrect: assign all dependencies in the constructor
- this._userModel = appContext.userModel;
- }
- //...
- _getUserName() {
- // Incorrect: use appContext outside of a constructor
- return appContext.userModel.activeUserName();
- }
-}
-```
-
-<a name="assign-dependencies-legacy-element-example"></a>
-**Legacy element:**
-
-Before:
-```Javascript
-export class MyCustomElement extends ...LegacyElementMixin(...) {
- constructor() {
- super();
- someAction();
- }
- created() {
- super();
- createdAction1();
- createdAction2();
- }
-}
-```
-
-After:
-```Javascript
-export class MyCustomElement extends ...LegacyElementMixin(...) {
- constructor() {
- super();
- // Assign services here
- this._userModel = appContext.userModel;
- // Code from the created method - put it before existing actions in constructor
- createdAction1();
- createdAction2();
- // Original constructor code
- someAction();
- }
- // created method is removed
-}
-```
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index b9c065f..ef70a75 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -22,19 +22,16 @@
// Note: for new events, naming convention should be: `a-b`
export enum EventType {
- HISTORY = 'history',
LABEL_CHANGE = 'labelchange',
SHOW_CHANGE = 'showchange',
SUBMIT_CHANGE = 'submitchange',
SHOW_REVISION_ACTIONS = 'show-revision-actions',
COMMIT_MSG_EDIT = 'commitmsgedit',
- COMMENT = 'comment',
REVERT = 'revert',
REVERT_SUBMISSION = 'revert_submission',
POST_REVERT = 'postrevert',
ANNOTATE_DIFF = 'annotatediff',
ADMIN_MENU_LINKS = 'admin-menu-links',
- HIGHLIGHTJS_LOADED = 'highlightjs-loaded',
}
export declare interface PluginApi {
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index 0e00d07..ae4aad9 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -14,6 +14,8 @@
PLUGINS_INSTALLED = 'Plugins installed',
PLUGINS_FAILED = 'Some plugins failed to load',
USER_REFERRED_FROM = 'User referred from',
+ NOTIFICATION_PERMISSION = 'Notification Permission',
+ SERVICE_WORKER_UPDATE = 'Service worker update',
}
export enum Execution {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 088002c..85be2d5 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -34,7 +34,10 @@
} from '../../../types/common';
import {GroupNameChangedDetail} from '../gr-group/gr-group';
import {getAppContext} from '../../../services/app-context';
-import {GerritView} from '../../../services/router/router-model';
+import {
+ GerritView,
+ routerModelToken,
+} from '../../../services/router/router-model';
import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -120,7 +123,7 @@
private readonly getRepoViewModel = resolve(this, repoViewModelToken);
- private readonly routerModel = getAppContext().routerModel;
+ private readonly getRouterModel = resolve(this, routerModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -152,7 +155,7 @@
);
subscribe(
this,
- () => this.routerModel.routerView$,
+ () => this.getRouterModel().routerView$,
view => {
this.view = view;
if (this.needsReload()) this.reload();
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 b7b0c6b..8d6d89a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -40,6 +40,8 @@
import {customElement, property, state} from 'lit/decorators.js';
import {subscribe} from '../../lit/subscription-controller';
import {createSearchUrl} from '../../../models/views/search';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
const STATES = {
active: {value: RepoState.ACTIVE, label: 'Active'},
@@ -110,7 +112,7 @@
@state() private pluginConfigChanged = false;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly restApiService = getAppContext().restApiService;
@@ -118,7 +120,7 @@
super();
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
if (prefs?.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
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 5728529..714b588 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
@@ -39,12 +39,13 @@
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {Interaction} from '../../../constants/reporting';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-change-list-bulk-vote-flow')
export class GrChangeListBulkVoteFlow extends LitElement {
private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly reportingService = getAppContext().reportingService;
@@ -141,7 +142,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
account => (this.account = account)
);
}
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 436e435..cb6dd7d 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
@@ -44,6 +44,7 @@
import {fireAlert, fireReload} from '../../../utils/event-util';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {Interaction} from '../../../constants/reporting';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-change-list-reviewer-flow')
export class GrChangeListReviewerFlow extends LitElement {
@@ -95,9 +96,11 @@
private readonly reportingService = getAppContext().reportingService;
- private getBulkActionsModel = resolve(this, bulkActionsModelToken);
+ private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
- private getConfigModel = resolve(this, configModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ private readonly getUserModel = resolve(this, userModelToken);
private restApiService = getAppContext().restApiService;
@@ -169,12 +172,12 @@
);
subscribe(
this,
- () => getAppContext().userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
isLoggedIn => (this.isLoggedIn = isLoggedIn)
);
subscribe(
this,
- () => getAppContext().userModel.account$,
+ () => this.getUserModel().account$,
account => (this.account = account)
);
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 8000c22..cd5cb96 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -27,6 +27,7 @@
} from '../../../models/views/search';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
+import {userModelToken} from '../../../models/user/user-model';
const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
@@ -76,7 +77,7 @@
private reporting = getAppContext().reportingService;
- private userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getViewModel = resolve(this, searchViewModelToken);
@@ -117,22 +118,22 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
x => (this.loggedIn = x)
);
subscribe(
this,
- () => this.userModel.preferenceChangesPerPage$,
+ () => this.getUserModel().preferenceChangesPerPage$,
x => (this.changesPerPage = x)
);
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
x => (this.preferences = x)
);
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index 6fc2374..a2a4061 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -57,6 +57,7 @@
UserDashboard,
YOUR_TURN,
} from '../../../utils/dashboard-util';
+import {userModelToken} from '../../../models/user/user-model';
const PROJECT_PLACEHOLDER_PATTERN = /\${project}/g;
@@ -107,7 +108,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getViewModel = resolve(this, dashboardViewModelToken);
@@ -119,7 +120,7 @@
super();
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 917b7ca..834f2b0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -107,6 +107,7 @@
import {rootUrl} from '../../../utils/url-util';
import {createSearchUrl} from '../../../models/views/search';
import {createChangeUrl} from '../../../models/views/change';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -540,7 +541,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly storage = getAppContext().storageService;
+ private readonly getStorage = resolve(this, storageServiceToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -1720,7 +1721,7 @@
// We need to make sure that all cached version of a change
// edit are deleted.
- this.storage.eraseEditableContentItemsForChangeEdit(this.changeNum);
+ this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
this.fireAction(
'/edit',
@@ -2077,7 +2078,7 @@
// We need to make sure that all cached version of a change
// edit are deleted.
- this.storage.eraseEditableContentItemsForChangeEdit(this.changeNum);
+ this.getStorage().eraseEditableContentItemsForChangeEdit(this.changeNum);
this.fireAction(
'/edit:publish',
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 1b98c5d..a5431ed 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -22,7 +22,6 @@
query,
queryAll,
queryAndAssert,
- spyStorage,
stubReporting,
stubRestApi,
} from '../../../test/test-utils';
@@ -48,7 +47,6 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {UIActionInfo} from '../../shared/gr-js-api-interface/gr-change-actions-js-api';
-import {getAppContext} from '../../../services/app-context';
import {fixture, html, assert} from '@open-wc/testing';
import {GrConfirmCherrypickDialog} from '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
import {GrDropdown} from '../../shared/gr-dropdown/gr-dropdown';
@@ -60,6 +58,7 @@
import {GrConfirmRevertDialog} from '../gr-confirm-revert-dialog/gr-confirm-revert-dialog';
import {EventType} from '../../../types/events';
import {testResolver} from '../../../test/common-test-setup';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
// TODO(dhruvsri): remove use of _populateRevertMessage as it's private
suite('gr-change-actions tests', () => {
@@ -813,7 +812,7 @@
element.editPatchsetLoaded = true;
await element.updateComplete;
- const storage = getAppContext().storageService;
+ const storage = testResolver(storageServiceToken);
storage.setEditableContentItem(
'c42_ps2_index.php',
'<?php\necho 42_ps_2'
@@ -836,7 +835,8 @@
assert.isOk(storage.getEditableContentItem('c42_ps2_index.php')!);
assert.isNotOk(storage.getEditableContentItem('c50_psedit_index.php')!);
- const eraseEditableContentItemsForChangeEditSpy = spyStorage(
+ const eraseEditableContentItemsForChangeEditSpy = sinon.spy(
+ storage,
'eraseEditableContentItemsForChangeEdit'
);
sinon.stub(element, 'fireAction');
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 ed1f46d..84bdffb 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
@@ -57,6 +57,7 @@
import {when} from 'lit/directives/when.js';
import {KnownExperimentId} from '../../../services/flags/flags';
import {combineLatest} from 'rxjs';
+import {userModelToken} from '../../../models/user/user-model';
function handleSpaceOrEnter(e: KeyboardEvent, handler: () => void) {
if (modifierPressed(e)) return;
@@ -109,11 +110,9 @@
private readonly showAllChips = new Map<RunStatus | Category, boolean>();
- // private but used in tests
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
- // private but used in tests
- readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getChecksModel = resolve(this, checksModelToken);
@@ -172,7 +171,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.selfAccount = x)
);
if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
@@ -180,7 +179,7 @@
this,
() =>
combineLatest([
- this.userModel.account$,
+ this.getUserModel().account$,
this.getCommentsModel().threads$,
]),
([selfAccount, threads]) => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 9584637..05036ab 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -16,11 +16,22 @@
} from '../../../test/test-data-generators';
import {stubFlags} from '../../../test/test-utils';
import {Timestamp} from '../../../api/rest-api';
+import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
suite('gr-change-summary test', () => {
let element: GrChangeSummary;
+ let commentsModel: CommentsModel;
+ let userModel: UserModel;
+
setup(async () => {
element = await fixture(html`<gr-change-summary></gr-change-summary>`);
+ commentsModel = testResolver(commentsModelToken);
+ userModel = testResolver(userModelToken);
});
test('is defined', () => {
@@ -29,7 +40,7 @@
});
test('renders', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
drafts: {
a: [createDraft(), createDraft(), createDraft()],
},
@@ -112,7 +123,7 @@
element = await fixture(html`<gr-change-summary></gr-change-summary>`);
await element.updateComplete;
- element.getCommentsModel().setState({
+ commentsModel.setState({
drafts: {
a: [
{
@@ -139,7 +150,7 @@
},
discardedDrafts: [],
});
- element.userModel.setAccount({
+ userModel.setAccount({
...createAccountWithEmail('abc@def.com'),
registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
});
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 535ba6f..6a7aeeb 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
@@ -135,7 +135,10 @@
fireReload,
fireTitleChange,
} from '../../../utils/event-util';
-import {GerritView} from '../../../services/router/router-model';
+import {
+ GerritView,
+ routerModelToken,
+} from '../../../services/router/router-model';
import {
debounce,
DelayedTask,
@@ -181,6 +184,7 @@
} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
import {createEditUrl} from '../../../models/views/edit';
+import {userModelToken} from '../../../models/user/user-model';
const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
@@ -535,16 +539,13 @@
private readonly flagsService = getAppContext().flagsService;
- // Private but used in tests.
- readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
- // Private but used in tests.
- readonly getChangeModel = resolve(this, changeModelToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly routerModel = getAppContext().routerModel;
+ private readonly getRouterModel = resolve(this, routerModelToken);
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getConfigModel = resolve(this, configModelToken);
@@ -717,14 +718,14 @@
);
subscribe(
this,
- () => this.routerModel.routerView$,
+ () => this.getRouterModel().routerView$,
view => {
this.isViewCurrent = view === GerritView.CHANGE;
}
);
subscribe(
this,
- () => this.routerModel.routerPatchNum$,
+ () => this.getRouterModel().routerPatchNum$,
patchNum => {
this.routerPatchNum = patchNum;
}
@@ -738,7 +739,7 @@
);
subscribe(
this,
- () => this.userModel.preferenceDiffViewMode$,
+ () => this.getUserModel().preferenceDiffViewMode$,
diffViewMode => {
this.diffViewMode = diffViewMode;
}
@@ -768,14 +769,14 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
account => {
this.account = account;
}
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => {
this.loggedIn = loggedIn;
}
@@ -1726,9 +1727,9 @@
// Private but used in tests.
handleToggleDiffMode() {
if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) {
- this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+ this.getUserModel().updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
diff_view: DiffViewMode.SIDE_BY_SIDE,
});
}
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 77f080d..2491610 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
@@ -28,7 +28,6 @@
queryAndAssert,
stubFlags,
stubRestApi,
- stubUsers,
waitEventLoop,
waitQueryAndAssert,
waitUntil,
@@ -86,7 +85,11 @@
import {ParsedChangeInfo} from '../../../types/types';
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
-import {LoadingStatus} from '../../../models/change/change-model';
+import {
+ ChangeModel,
+ changeModelToken,
+ LoadingStatus,
+} from '../../../models/change/change-model';
import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
@@ -101,10 +104,18 @@
import {ChangeViewState} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
suite('gr-change-view tests', () => {
let element: GrChangeView;
let setUrlStub: sinon.SinonStub;
+ let userModel: UserModel;
+ let changeModel: ChangeModel;
+ let commentsModel: CommentsModel;
const ROBOT_COMMENTS_LIMIT = 10;
@@ -374,6 +385,9 @@
assertIsDefined(element.actions);
sinon.stub(element.actions, 'reload').returns(Promise.resolve());
});
+ userModel = testResolver(userModelToken);
+ commentsModel = testResolver(commentsModelToken);
+ changeModel = testResolver(changeModelToken);
});
teardown(async () => {
@@ -805,7 +819,7 @@
});
test('A fires an error event when not logged in', async () => {
- element.userModel.setAccount(undefined);
+ userModel.setAccount(undefined);
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
@@ -978,14 +992,14 @@
});
test('m should toggle diff mode', async () => {
- const updatePreferencesStub = stubUsers('updatePreferences');
+ const updatePreferencesStub = sinon.stub(userModel, 'updatePreferences');
await element.updateComplete;
const prefs = {
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
- element.userModel.setPreferences(prefs);
+ userModel.setPreferences(prefs);
element.handleToggleDiffMode();
assert.isTrue(
updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED})
@@ -995,7 +1009,7 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.UNIFIED,
};
- element.userModel.setPreferences(newPrefs);
+ userModel.setPreferences(newPrefs);
await element.updateComplete;
element.handleToggleDiffMode();
assert.isTrue(
@@ -1586,11 +1600,11 @@
sinon.stub(element, 'loadAndSetCommitInfo');
await element.updateComplete;
const reloadPortedCommentsStub = sinon.stub(
- element.getCommentsModel(),
+ commentsModel,
'reloadPortedComments'
);
const reloadPortedDraftsStub = sinon.stub(
- element.getCommentsModel(),
+ commentsModel,
'reloadPortedDrafts'
);
sinon.stub(element.fileList, 'collapseAllDiffs');
@@ -1683,7 +1697,7 @@
);
element.viewState = createChangeViewState();
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -1776,7 +1790,7 @@
test('topic is coalesced to null', async () => {
sinon.stub(element, 'changeChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -1791,7 +1805,7 @@
});
test('commit sha is populated from getChangeDetail', async () => {
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -2161,7 +2175,7 @@
test('selectedRevision updates when patchNum is changed', async () => {
const revision1: RevisionInfo = createRevision(1);
const revision2: RevisionInfo = createRevision(2);
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -2174,7 +2188,7 @@
current_revision: 'bbb' as CommitId,
},
});
- element.userModel.setPreferences(createPreferences());
+ userModel.setPreferences(createPreferences());
element.patchRange = {patchNum: 2 as RevisionPatchSetNum};
await element.performPostChangeLoadTasks();
@@ -2189,7 +2203,7 @@
const revision1 = createRevision(1);
const revision2 = createRevision(2);
const revision3 = createEditRevision();
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
@@ -2463,7 +2477,7 @@
changeNum: TEST_NUMERIC_CHANGE_ID,
repo: TEST_PROJECT_NAME,
};
- element.getChangeModel().setState({
+ changeModel.setState({
loadingStatus: LoadingStatus.LOADED,
change: {
...createChangeViewChange(),
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index 832738b..9bac7c2 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -38,10 +38,10 @@
shortcutsServiceToken,
} from '../../../services/shortcuts/shortcuts-service';
import {resolve} from '../../../models/dependency';
-import {getAppContext} from '../../../services/app-context';
import {subscribe} from '../../lit/subscription-controller';
import {configModelToken} from '../../../models/config/config-model';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-file-list-header')
export class GrFileListHeader extends LitElement {
@@ -123,7 +123,7 @@
// 'hide diffs' buttons still be functional.
private readonly maxFilesForBulkActions = 225;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -131,7 +131,7 @@
super();
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
if (!diffPreferences) return;
this.diffPrefs = diffPreferences;
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 d21f5a9..ccc885e 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
@@ -76,6 +76,7 @@
import {createDiffUrl} from '../../../models/views/diff';
import {createEditUrl} from '../../../models/views/edit';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -285,7 +286,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
@@ -766,7 +767,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
this.diffPrefs = diffPreferences;
}
@@ -775,7 +776,7 @@
this,
() =>
select(
- this.userModel.preferences$,
+ this.getUserModel().preferences$,
prefs => !!prefs?.size_bar_in_change_table
),
sizeBarInChangeTable => {
@@ -784,7 +785,7 @@
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => {
this.loggedIn = loggedIn;
}
@@ -2595,7 +2596,7 @@
}
private handleReloadingDiffPreference() {
- this.userModel.getDiffPreferences();
+ this.getUserModel().getDiffPreferences();
}
private getOldPath(file: NormalizedFileInfo) {
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 5417127..e7a54fb 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
@@ -322,8 +322,7 @@
@state()
private combinedMessages: CombinedMessage[] = [];
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly changeModel = resolve(this, changeModelToken);
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 2e62718..158ad8d 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
@@ -32,6 +32,8 @@
import {fixture, assert} from '@open-wc/testing';
import {GrButton} from '../../shared/gr-button/gr-button';
import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
+import {testResolver} from '../../../test/common-test-setup';
+import {commentsModelToken} from '../../../models/comments/comments-model';
const author = {
_account_id: 42 as AccountId,
@@ -136,7 +138,9 @@
element = await fixture<GrMessagesList>(
html`<gr-messages-list></gr-messages-list>`
);
- await element.getCommentsModel().reloadComments(0 as NumericChangeId);
+ await testResolver(commentsModelToken).reloadComments(
+ 0 as NumericChangeId
+ );
element.messages = messages;
await element.updateComplete;
});
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 9d6ec08..ec5760b 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
@@ -129,8 +129,10 @@
GrComment,
} from '../../shared/gr-comment/gr-comment';
import {ShortcutController} from '../../lit/shortcut-controller';
-import {Key, Modifier} from '../../../utils/dom-util';
+import {Key, Modifier, whenVisible} from '../../../utils/dom-util';
import {GrThreadList} from '../gr-thread-list/gr-thread-list';
+import {userModelToken} from '../../../models/user/user-model';
+import {accountsModelToken} from '../../../models/accounts-model/accounts-model';
export enum FocusTarget {
ANY = 'any',
@@ -216,8 +218,7 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
// TODO: update type to only ParsedChangeInfo
@property({type: Object})
@@ -241,6 +242,8 @@
@property({type: Object})
projectConfig?: ConfigInfo;
+ @query('#patchsetLevelComment') patchsetLevelGrComment?: GrComment;
+
@query('#reviewers') reviewersList?: GrAccountList;
@query('#ccs') ccsList?: GrAccountList;
@@ -393,7 +396,9 @@
private readonly getConfigModel = resolve(this, configModelToken);
- private readonly accountsModel = getAppContext().accountsModel;
+ private readonly getAccountsModel = resolve(this, accountsModelToken);
+
+ private readonly getUserModel = resolve(this, userModelToken);
private latestPatchNum?: PatchSetNumber;
@@ -629,7 +634,7 @@
subscribe(
this,
- () => getAppContext().userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
isLoggedIn => (this.isLoggedIn = isLoggedIn)
);
subscribe(
@@ -1415,7 +1420,9 @@
const newAttentionSetUsers = (
await Promise.all(
- newAttentionSetAdditions.map(a => this.accountsModel.fillDetails(a))
+ newAttentionSetAdditions.map(a =>
+ this.getAccountsModel().fillDetails(a)
+ )
)
).filter(isDefined);
@@ -1446,11 +1453,7 @@
reviewInput.remove_from_attention_set
);
- const patchsetLevelComment = queryAndAssert<GrComment>(
- this,
- '#patchsetLevelComment'
- );
- await patchsetLevelComment.save();
+ await this.patchsetLevelGrComment?.save();
assertIsDefined(this.change, 'change');
reviewInput.reviewers = this.computeReviewers();
@@ -1494,13 +1497,17 @@
if (!section || section === FocusTarget.ANY) {
section = this.chooseFocusTarget();
}
- if (section === FocusTarget.REVIEWERS) {
- const reviewerEntry = this.reviewersList?.focusStart;
- setTimeout(() => reviewerEntry?.focus());
- } else if (section === FocusTarget.CCS) {
- const ccEntry = this.ccsList?.focusStart;
- setTimeout(() => ccEntry?.focus());
- }
+ whenVisible(this, () => {
+ if (section === FocusTarget.REVIEWERS) {
+ const reviewerEntry = this.reviewersList?.focusStart;
+ reviewerEntry?.focus();
+ } else if (section === FocusTarget.CCS) {
+ const ccEntry = this.ccsList?.focusStart;
+ ccEntry?.focus();
+ } else {
+ this.patchsetLevelGrComment?.focus();
+ }
+ });
}
chooseFocusTarget() {
@@ -1860,11 +1867,7 @@
bubbles: false,
})
);
- const patchsetLevelComment = queryAndAssert<GrComment>(
- this,
- '#patchsetLevelComment'
- );
- await patchsetLevelComment.save();
+ await this.patchsetLevelGrComment?.save();
this.rebuildReviewerArrays();
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index acd1755..ef781ba 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -15,6 +15,7 @@
queryAndAssert,
stubFlags,
stubRestApi,
+ waitUntilVisible,
} from '../../../test/test-utils';
import {ChangeStatus, ReviewerState} from '../../../constants/constants';
import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
@@ -62,6 +63,11 @@
import {KnownExperimentId} from '../../../services/flags/flags';
import {Key, Modifier} from '../../../utils/dom-util';
import {GrComment} from '../../shared/gr-comment/gr-comment';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
function cloneableResponse(status: number, text: string) {
return {
@@ -87,6 +93,7 @@
let element: GrReplyDialog;
let changeNum: NumericChangeId;
let patchNum: PatchSetNum;
+ let commentsModel: CommentsModel;
let lastId = 1;
const makeAccount = function () {
@@ -147,6 +154,7 @@
element.draftCommentThreads = [];
await element.updateComplete;
+ commentsModel = testResolver(commentsModelToken);
});
function stubSaveReview(
@@ -1444,14 +1452,10 @@
test('focusOn', async () => {
await element.updateComplete;
- const clock = sinon.useFakeTimers();
const chooseFocusTargetSpy = sinon.spy(element, 'chooseFocusTarget');
element.focusOn();
- // element.focus() is called after a setTimeout(). The focusOn() method
- // does not trigger any changes in the element hence element.updateComplete
- // resolves immediately and cannot be used here, hence tick the clock here
- // explicitly instead
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 1);
assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-COMMENT');
assert.equal(
@@ -1460,7 +1464,8 @@
);
element.focusOn(element.FocusTarget.ANY);
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 2);
assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-COMMENT');
assert.equal(
@@ -1469,7 +1474,8 @@
);
element.focusOn(element.FocusTarget.BODY);
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 2);
assert.equal(element?.shadowRoot?.activeElement?.tagName, 'GR-COMMENT');
assert.equal(
@@ -1478,23 +1484,21 @@
);
element.focusOn(element.FocusTarget.REVIEWERS);
- clock.tick(1);
+ await waitUntilVisible(element); // let whenVisible resolve
+
assert.equal(chooseFocusTargetSpy.callCount, 2);
- assert.equal(
- element?.shadowRoot?.activeElement?.tagName,
- 'GR-ACCOUNT-LIST'
+ await waitUntil(
+ () => element?.shadowRoot?.activeElement?.tagName === 'GR-ACCOUNT-LIST'
);
assert.equal(element?.shadowRoot?.activeElement?.id, 'reviewers');
element.focusOn(element.FocusTarget.CCS);
- clock.tick(1);
assert.equal(chooseFocusTargetSpy.callCount, 2);
assert.equal(
element?.shadowRoot?.activeElement?.tagName,
'GR-ACCOUNT-LIST'
);
- assert.equal(element?.shadowRoot?.activeElement?.id, 'ccs');
- clock.restore();
+ await waitUntil(() => element?.shadowRoot?.activeElement?.id === 'ccs');
});
test('chooseFocusTarget', () => {
@@ -2381,7 +2385,7 @@
test('replies to patchset level comments are not filtered out', async () => {
const draft = {...createDraft(), in_reply_to: '1' as UrlEncodedCommentId};
- element.getCommentsModel().setState({
+ commentsModel.setState({
drafts: {
'abc.txt': [draft],
},
@@ -2417,7 +2421,7 @@
...createDraft(),
message: 'hey @abcd@def take a look at this',
};
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2452,7 +2456,7 @@
message: 'hey @abcd@def.com take a look at this',
unresolved: true,
};
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2492,7 +2496,7 @@
message: 'hey @abcd@def.com take a look at this',
unresolved: true,
};
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2545,7 +2549,7 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
@@ -2582,7 +2586,7 @@
registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {
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 27b5097..414fed9 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
@@ -44,6 +44,7 @@
import {Interaction} from '../../../constants/reporting';
import {KnownExperimentId} from '../../../services/flags/flags';
import {HtmlPatched} from '../../../utils/lit-util';
+import {userModelToken} from '../../../models/user/user-model';
enum SortDropdownState {
TIMESTAMP = 'Latest timestamp',
@@ -205,7 +206,7 @@
private readonly flagsService = getAppContext().flagsService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly patched = new HtmlPatched(key => {
this.reporting.reportInteraction(Interaction.AUTOCLOSE_HTML_PATCHED, {
@@ -228,7 +229,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
// for COMMENTS_AUTOCLOSE logging purposes only
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index dcc9a99..6c6d6a0 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -29,6 +29,7 @@
import {fireEvent} from '../../../utils/event-util';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
+import {userModelToken} from '../../../models/user/user-model';
type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
@@ -142,9 +143,9 @@
private readonly jsAPI = getAppContext().jsApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
- private readonly configModel = resolve(this, configModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
private subscriptions: Subscription[] = [];
@@ -153,8 +154,8 @@
this.loadAccount();
this.subscriptions.push(
- this.userModel.preferences$
- .pipe(
+ this.getUserModel()
+ .preferences$.pipe(
map(preferences => preferences?.my ?? []),
distinctUntilChanged()
)
@@ -163,7 +164,7 @@
})
);
this.subscriptions.push(
- this.configModel().serverConfig$.subscribe(config => {
+ this.getConfigModel().serverConfig$.subscribe(config => {
if (!config) return;
this.serverConfig = config;
this.retrieveFeedbackURL(config);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index a145b96..44a5616 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -343,6 +343,7 @@
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
+ this.subscriptions = [];
}
start() {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 17d7516..8e77f96 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -34,6 +34,7 @@
import {resolve} from '../../../models/dependency';
import {createChangeUrl} from '../../../models/views/change';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {userModelToken} from '../../../models/user/user-model';
interface FilePreview {
filepath: string;
@@ -89,7 +90,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -97,7 +98,7 @@
super();
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
preferences => {
if (!preferences?.disable_token_highlighting) {
this.layers = [new TokenHighlightLayer(this)];
@@ -106,7 +107,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
if (!diffPreferences) return;
this.diffPrefs = diffPreferences;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index baf89c0..d3428de 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -84,7 +84,10 @@
import {deepEqual} from '../../../utils/deep-util';
import {Category} from '../../../api/checks';
import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
-import {CODE_MAX_LINES} from '../../../services/highlight/highlight-service';
+import {
+ CODE_MAX_LINES,
+ highlightServiceToken,
+} from '../../../services/highlight/highlight-service';
import {html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {ValueChangedEvent} from '../../../types/events';
@@ -95,6 +98,7 @@
} from '../../../utils/async-util';
import {subscribe} from '../../lit/subscription-controller';
import {GeneratedWebLink} from '../../../utils/weblink-util';
+import {userModelToken} from '../../../models/user/user-model';
const EMPTY_BLAME = 'No blame information for this diff.';
@@ -330,7 +334,7 @@
private readonly restApiService = getAppContext().restApiService;
// visible for testing
- readonly userModel = getAppContext().userModel;
+ readonly getUserModel = resolve(this, userModelToken);
// visible for testing
readonly jsAPI = getAppContext().jsApiService;
@@ -345,7 +349,10 @@
constructor() {
super();
- this.syntaxLayer = new GrSyntaxLayerWorker();
+ this.syntaxLayer = new GrSyntaxLayerWorker(
+ resolve(this, highlightServiceToken),
+ () => getAppContext().reportingService
+ );
this.renderPrefs = {
...this.renderPrefs,
use_lit_components: this.flags.isEnabled(
@@ -372,7 +379,7 @@
);
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => (this.loggedIn = loggedIn)
);
subscribe(
@@ -384,7 +391,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
this.prefs = diffPreferences;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
index 598819b..4163989 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -53,11 +53,14 @@
import {GrAnnotationActionsInterface} from '../../shared/gr-js-api-interface/gr-annotation-actions-js-api';
import {fixture, html, assert} from '@open-wc/testing';
import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken, UserModel} from '../../../models/user/user-model';
suite('gr-diff-host tests', () => {
let element: GrDiffHost;
let account = createAccountDetailWithId(1);
let getDiffRestApiStub: SinonStub;
+ let userModel: UserModel;
setup(async () => {
stubRestApi('getAccount').callsFake(() => Promise.resolve(account));
@@ -70,6 +73,7 @@
// Fall back in case a test forgets to set one up
getDiffRestApiStub.returns(Promise.resolve(createDiff()));
await element.updateComplete;
+ userModel = testResolver(userModelToken);
});
suite('plugin layers', () => {
@@ -591,7 +595,7 @@
});
test('cannot create comments when not logged in', () => {
- element.userModel.setAccount(undefined);
+ userModel.setAccount(undefined);
element.patchRange = createPatchRange();
const showAuthRequireSpy = sinon.spy();
element.addEventListener('show-auth-required', showAuthRequireSpy);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 75117f9..17ea2e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -85,7 +85,10 @@
ValueChangedEvent,
} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
-import {GerritView} from '../../../services/router/router-model';
+import {
+ GerritView,
+ routerModelToken,
+} from '../../../services/router/router-model';
import {assertIsDefined} from '../../../utils/common-util';
import {Key, toggleClass} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
@@ -100,7 +103,6 @@
import {LoadingStatus} from '../../../models/change/change-model';
import {DisplayLine} from '../../../api/diff';
import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
-import {browserModelToken} from '../../../models/browser/browser-model';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
import {resolve} from '../../../models/dependency';
@@ -122,6 +124,7 @@
import {createChangeUrl} from '../../../models/views/change';
import {createEditUrl} from '../../../models/views/edit';
import {GeneratedWebLink} from '../../../utils/weblink-util';
+import {userModelToken} from '../../../models/user/user-model';
const LOADING_BLAME = 'Loading blame...';
const LOADED_BLAME = 'Blame loaded';
@@ -288,20 +291,13 @@
private readonly restApiService = getAppContext().restApiService;
- // Private but used in tests.
- readonly routerModel = getAppContext().routerModel;
+ private readonly getRouterModel = resolve(this, routerModelToken);
- // Private but used in tests.
- readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
- // Private but used in tests.
- readonly getChangeModel = resolve(this, changeModelToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
- // Private but used in tests.
- readonly getBrowserModel = resolve(this, browserModelToken);
-
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
@@ -396,7 +392,7 @@
private setupSubscriptions() {
subscribe(
this,
- () => this.userModel.loggedIn$,
+ () => this.getUserModel().loggedIn$,
loggedIn => {
this.loggedIn = loggedIn;
}
@@ -417,14 +413,14 @@
);
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
preferences => {
this.userPrefs = preferences;
}
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
this.prefs = diffPreferences;
}
@@ -478,8 +474,8 @@
switchMap(() =>
combineLatest([
this.getChangeModel().patchNum$,
- this.routerModel.routerView$,
- this.userModel.diffPreferences$,
+ this.getRouterModel().routerView$,
+ this.getUserModel().diffPreferences$,
this.getChangeModel().reviewedFiles$,
]).pipe(
filter(
@@ -1315,9 +1311,9 @@
handleToggleDiffMode() {
if (!this.userPrefs) return;
if (this.userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) {
- this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
+ this.getUserModel().updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
diff_view: DiffViewMode.SIDE_BY_SIDE,
});
}
@@ -2266,7 +2262,7 @@
}
private handleReloadingDiffPreference() {
- this.userModel.getDiffPreferences();
+ this.getUserModel().getDiffPreferences();
}
private computeCanEdit() {
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 a611361..a6c1308 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
@@ -20,12 +20,14 @@
queryAndAssert,
stubReporting,
stubRestApi,
- stubUsers,
waitEventLoop,
waitUntil,
} from '../../../test/test-utils';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
-import {GerritView} from '../../../services/router/router-model';
+import {
+ GerritView,
+ routerModelToken,
+} from '../../../services/router/router-model';
import {
createRevisions,
createComment as createCommentGeneric,
@@ -60,7 +62,11 @@
import {Files, GrDiffView} from './gr-diff-view';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {SinonFakeTimers, SinonStub, SinonSpy} from 'sinon';
-import {LoadingStatus} from '../../../models/change/change-model';
+import {
+ changeModelToken,
+ ChangeModel,
+ LoadingStatus,
+} from '../../../models/change/change-model';
import {CommentMap} from '../../../utils/comment-util';
import {ParsedChangeInfo} from '../../../types/types';
import {assertIsDefined} from '../../../utils/common-util';
@@ -70,6 +76,15 @@
import {Key} from '../../../utils/dom-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {
+ commentsModelToken,
+ CommentsModel,
+} from '../../../models/comments/comments-model';
+import {
+ BrowserModel,
+ browserModelToken,
+} from '../../../models/browser/browser-model';
function createComment(
id: string,
@@ -93,6 +108,10 @@
let diffCommentsStub;
let getDiffRestApiStub: SinonStub;
let setUrlStub: SinonStub;
+ let changeModel: ChangeModel;
+ let commentsModel: CommentsModel;
+ let browserModel: BrowserModel;
+ let userModel: UserModel;
function getFilesFromFileList(fileList: string[]): Files {
const changeFilesByPath = fileList.reduce((files, path) => {
@@ -140,8 +159,12 @@
],
});
await element.updateComplete;
+ commentsModel = testResolver(commentsModelToken);
+ changeModel = testResolver(changeModelToken);
+ browserModel = testResolver(browserModelToken);
+ userModel = testResolver(userModelToken);
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {},
robotComments: {},
drafts: {},
@@ -192,7 +215,7 @@
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -202,7 +225,7 @@
});
test('comment url resolves to comment.patch_set vs latest', () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -265,7 +288,7 @@
});
test('unchanged diff X vs latest from comment links navigates to base vs X', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -284,7 +307,7 @@
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, 'isFileUnchanged').returns(true);
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -311,7 +334,7 @@
});
test('unchanged diff Base vs latest from comment does not navigate', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -330,7 +353,7 @@
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, 'isFileUnchanged').returns(true);
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -385,7 +408,7 @@
});
test('diff toast to go to latest is shown and not base', async () => {
- element.getCommentsModel().setState({
+ commentsModel.setState({
comments: {
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -405,7 +428,7 @@
sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
element.change = undefined;
- element.getChangeModel().setState({
+ changeModel.setState({
change: {
...createParsedChange(),
revisions: createRevisions(11),
@@ -439,7 +462,7 @@
test('renders', async () => {
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
- element.getBrowserModel().setScreenWidth(0);
+ browserModel.setScreenWidth(0);
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
@@ -623,7 +646,7 @@
test('keyboard shortcuts', async () => {
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
- element.getBrowserModel().setScreenWidth(0);
+ browserModel.setScreenWidth(0);
element.patchRange = {
basePatchNum: PARENT,
patchNum: 10 as RevisionPatchSetNum,
@@ -1529,7 +1552,7 @@
'automatically called',
async () => {
const setReviewedFileStatusStub = sinon
- .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+ .stub(changeModel, 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
const setReviewedStatusStub = sinon.spy(element, 'setReviewedStatus');
@@ -1541,15 +1564,15 @@
...createDefaultDiffPrefs(),
manual_review: true,
};
- element.userModel.setDiffPreferences(diffPreferences);
- element.getChangeModel().setState({
+ userModel.setDiffPreferences(diffPreferences);
+ changeModel.setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- element.routerModel.setState({
+ testResolver(routerModelToken).setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 2 as RevisionPatchSetNum,
@@ -1564,7 +1587,7 @@
assert.isFalse(setReviewedFileStatusStub.called);
// if prefs are updated then the reviewed status should not be set again
- element.userModel.setDiffPreferences(createDefaultDiffPrefs());
+ userModel.setDiffPreferences(createDefaultDiffPrefs());
await element.updateComplete;
assert.isFalse(setReviewedFileStatusStub.called);
@@ -1573,7 +1596,7 @@
test('_prefs.manual_review false means set reviewed is called', async () => {
const setReviewedFileStatusStub = sinon
- .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+ .stub(changeModel, 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
assertIsDefined(element.diffHost);
@@ -1583,15 +1606,15 @@
...createDefaultDiffPrefs(),
manual_review: false,
};
- element.userModel.setDiffPreferences(diffPreferences);
- element.getChangeModel().setState({
+ userModel.setDiffPreferences(diffPreferences);
+ changeModel.setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- element.routerModel.setState({
+ testResolver(routerModelToken).setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 22 as RevisionPatchSetNum,
@@ -1607,7 +1630,7 @@
});
test('file review status', async () => {
- element.getChangeModel().setState({
+ changeModel.setState({
change: createParsedChange(),
diffPath: '/COMMIT_MSG',
reviewedFiles: [],
@@ -1615,14 +1638,14 @@
});
element.loggedIn = true;
const saveReviewedStub = sinon
- .stub(element.getChangeModel(), 'setReviewedFilesStatus')
+ .stub(changeModel, 'setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
- element.userModel.setDiffPreferences(createDefaultDiffPrefs());
+ userModel.setDiffPreferences(createDefaultDiffPrefs());
- element.routerModel.setState({
+ testResolver(routerModelToken).setState({
changeNum: TEST_NUMERIC_CHANGE_ID,
view: GerritView.DIFF,
patchNum: 2 as RevisionPatchSetNum,
@@ -1635,7 +1658,7 @@
await waitUntil(() => saveReviewedStub.called);
- element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', true);
+ changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
await element.updateComplete;
const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>(
@@ -1660,7 +1683,7 @@
false,
]);
- element.getChangeModel().updateStateFileReviewed('/COMMIT_MSG', false);
+ changeModel.updateStateFileReviewed('/COMMIT_MSG', false);
await element.updateComplete;
reviewedStatusCheckBox.click();
@@ -1688,7 +1711,7 @@
test('file review status with edit loaded', async () => {
const saveReviewedStub = sinon.stub(
- element.getChangeModel(),
+ changeModel,
'setReviewedFilesStatus'
);
@@ -1730,9 +1753,9 @@
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
};
- element.getBrowserModel().setScreenWidth(0);
+ browserModel.setScreenWidth(0);
- const userStub = stubUsers('updatePreferences');
+ const userStub = sinon.stub(userModel, 'updatePreferences');
await element.updateComplete;
// The mode selected in the view state reflects the selected option.
@@ -1926,7 +1949,7 @@
});
test('handleToggleDiffMode', () => {
- const userStub = stubUsers('updatePreferences');
+ const userStub = sinon.stub(userModel, 'updatePreferences');
element.userPrefs = {
...createDefaultPreferences(),
diff_view: DiffViewMode.SIDE_BY_SIDE,
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 a15a575..8e346c3 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
@@ -33,6 +33,8 @@
import {ShortcutController} from '../../lit/shortcut-controller';
import {editViewModelToken, EditViewState} from '../../../models/views/edit';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const SAVING_MESSAGE = 'Saving changes...';
@@ -85,11 +87,11 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly storage = getAppContext().storageService;
-
private readonly reporting = getAppContext().reportingService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getStorage = resolve(this, storageServiceToken);
+
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
@@ -109,7 +111,7 @@
});
subscribe(
this,
- () => this.userModel.editPreferences$,
+ () => this.getUserModel().editPreferences$,
editPreferences => (this.editPrefs = editPreferences)
);
subscribe(
@@ -379,7 +381,9 @@
assertIsDefined(patchNum, 'patchset number');
assertIsDefined(path, 'path');
- const storedContent = this.storage.getEditableContentItem(this.storageKey);
+ const storedContent = this.getStorage().getEditableContentItem(
+ this.storageKey
+ );
return this.restApiService
.getFileContent(changeNum, path, patchNum)
@@ -418,7 +422,7 @@
this.saving = true;
this.showAlert(SAVING_MESSAGE);
- this.storage.eraseEditableContentItem(this.storageKey);
+ this.getStorage().eraseEditableContentItem(this.storageKey);
if (!this.newContent)
return Promise.reject(new Error('new content undefined'));
return this.restApiService
@@ -499,9 +503,9 @@
const content = e.detail.value;
if (content) {
this.newContent = e.detail.value;
- this.storage.setEditableContentItem(this.storageKey, content);
+ this.getStorage().setEditableContentItem(this.storageKey, content);
} else {
- this.storage.eraseEditableContentItem(this.storageKey);
+ this.getStorage().eraseEditableContentItem(this.storageKey);
}
},
STORAGE_DEBOUNCE_INTERVAL_MS
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index 52581ed..d428e18 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -13,7 +13,6 @@
pressKey,
query,
stubRestApi,
- stubStorage,
} from '../../../test/test-utils';
import {
EDIT,
@@ -32,6 +31,8 @@
import {EventType} from '../../../types/events';
import {Modifier} from '../../../utils/dom-util';
import {testResolver} from '../../../test/common-test-setup';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {StorageService} from '../../../services/storage/gr-storage';
suite('gr-editor-view tests', () => {
let element: GrEditorView;
@@ -40,6 +41,7 @@
let saveFileStub: sinon.SinonStub;
let changeDetailStub: sinon.SinonStub;
let navigateStub: sinon.SinonStub;
+ let storageService: StorageService;
setup(async () => {
element = await fixture(html`<gr-editor-view></gr-editor-view>`);
@@ -53,6 +55,7 @@
};
element.latestPatchsetNumber = 1 as PatchSetNumber;
await element.updateComplete;
+ storageService = testResolver(storageServiceToken);
});
test('render', () => {
@@ -178,7 +181,7 @@
});
test('reacts to content-change event', async () => {
- const storageStub = stubStorage('setEditableContentItem');
+ const storageStub = sinon.stub(storageService, 'setEditableContentItem');
element.newContent = 'test';
await element.updateComplete;
query<GrEndpointDecorator>(element, '#editorEndpoint')!.dispatchEvent(
@@ -219,7 +222,7 @@
test('file modification and save, !ok response', async () => {
const saveSpy = sinon.spy(element, 'saveEdit');
- const eraseStub = stubStorage('eraseEditableContentItem');
+ const eraseStub = sinon.stub(storageService, 'eraseEditableContentItem');
const alertStub = sinon.stub(element, 'showAlert');
saveFileStub.returns(Promise.resolve({ok: false}));
element.newContent = newText;
@@ -355,7 +358,7 @@
element.newContent = 'initial';
element.content = 'initial';
element.type = 'initial';
- stubStorage('getEditableContentItem').returns(null);
+ sinon.stub(storageService, 'getEditableContentItem').returns(null);
});
test('res.ok', () => {
@@ -512,7 +515,7 @@
suite('gr-storage caching', () => {
test('local edit exists', () => {
- stubStorage('getEditableContentItem').returns({
+ sinon.stub(storageService, 'getEditableContentItem').returns({
message: 'pending edit',
updated: 0,
});
@@ -544,7 +547,7 @@
});
test('local edit exists, is same as remote edit', () => {
- stubStorage('getEditableContentItem').returns({
+ sinon.stub(storageService, 'getEditableContentItem').returns({
message: 'pending edit',
updated: 0,
});
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index c05e4c4..646727f 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -53,7 +53,7 @@
RpcLogEvent,
TitleChangeEventDetail,
} from '../types/events';
-import {GerritView} from '../services/router/router-model';
+import {GerritView, routerModelToken} from '../services/router/router-model';
import {Execution, LifeCycle} from '../constants/reporting';
import {fireIronAnnounce} from '../utils/event-util';
import {resolve} from '../models/dependency';
@@ -73,6 +73,7 @@
import {createSearchUrl, SearchViewState} from '../models/views/search';
import {createSettingsUrl} from '../models/views/settings';
import {createDashboardUrl} from '../models/views/dashboard';
+import {userModelToken} from '../models/user/user-model';
interface ErrorInfo {
text: string;
@@ -161,9 +162,9 @@
private readonly shortcuts = new ShortcutController(this);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
- private readonly routerModel = getAppContext().routerModel;
+ private readonly getRouterModel = resolve(this, routerModelToken);
constructor() {
super();
@@ -210,7 +211,7 @@
subscribe(
this,
- () => this.userModel.preferenceTheme$,
+ () => this.getUserModel().preferenceTheme$,
theme => {
this.theme = theme;
this.applyTheme();
@@ -218,7 +219,7 @@
);
subscribe(
this,
- () => this.routerModel.routerView$,
+ () => this.getRouterModel().routerView$,
view => {
this.view = view;
if (view) this.errorView?.classList.remove('show');
@@ -257,14 +258,14 @@
// TODO(milutin): Remove saving preferences after while. This code is
// for migration.
if (window.localStorage.getItem('dark-theme')) {
- this.userModel.updatePreferences({theme: AppTheme.DARK});
+ this.getUserModel().updatePreferences({theme: AppTheme.DARK});
window.localStorage.removeItem('dark-theme');
this.reporting.reportExecution(
Execution.REACHABLE_CODE,
'Dark theme was migrated from localstorage'
);
} else if (window.localStorage.getItem('light-theme')) {
- this.userModel.updatePreferences({theme: AppTheme.LIGHT});
+ this.getUserModel().updatePreferences({theme: AppTheme.LIGHT});
window.localStorage.removeItem('light-theme');
this.reporting.reportExecution(
Execution.REACHABLE_CODE,
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 0cec8dd..721b46d 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -24,12 +24,18 @@
import {initGlobalVariables} from './gr-app-global-var-init';
import './gr-app-element';
import {Finalizable} from '../services/registry';
-import {provide} from '../models/dependency';
+import {
+ DependencyError,
+ DependencyToken,
+ provide,
+ Provider,
+} from '../models/dependency';
import {installPolymerResin} from '../scripts/polymer-resin-install';
import {
createAppContext,
createAppDependencies,
+ Creator,
} from '../services/app-context-init';
import {
initVisibilityReporter,
@@ -41,6 +47,7 @@
import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ServiceWorkerInstaller} from '../services/service-worker-installer';
+import {userModelToken} from '../models/user/user-model';
const appContext = createAppContext();
injectAppContext(appContext);
@@ -60,15 +67,48 @@
override connectedCallback() {
super.connectedCallback();
- const dependencies = createAppDependencies(appContext);
- for (const [token, service] of dependencies) {
- this.finalizables.push(service);
- provide(this, token, () => service);
+ const dependencies = new Map<DependencyToken<unknown>, Provider<unknown>>();
+
+ const injectDependency = <T>(
+ token: DependencyToken<T>,
+ creator: Creator<T>
+ ) => {
+ let service: (T & Finalizable) | undefined = undefined;
+ dependencies.set(token, () => {
+ if (service) return service;
+ service = creator();
+ this.finalizables.push(service);
+ return service;
+ });
+ };
+
+ const resolver = <T>(token: DependencyToken<T>): T => {
+ const provider = dependencies.get(token);
+ if (provider) {
+ return provider() as T;
+ } else {
+ throw new DependencyError(
+ token,
+ 'Forgot to set up dependency for gr-app'
+ );
+ }
+ };
+
+ for (const [token, creator] of createAppDependencies(
+ appContext,
+ resolver
+ )) {
+ injectDependency(token, creator);
}
+ for (const [token, provider] of dependencies) {
+ provide(this, token, provider);
+ }
+ // TODO(milutin): Move inside app dependencies.
if (!this.serviceWorkerInstaller) {
this.serviceWorkerInstaller = new ServiceWorkerInstaller(
appContext.flagsService,
- appContext.userModel
+ appContext.reportingService,
+ resolver(userModelToken)
);
}
}
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
index a87973b..730548c 100644
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -16,7 +16,9 @@
createServerInfo,
} from '../test/test-data-generators';
import {GrAppElement} from './gr-app-element';
-import {GrRouter} from './core/gr-router/gr-router';
+import {GrRouter, routerToken} from './core/gr-router/gr-router';
+import {resolve} from '../models/dependency';
+import {removeRequestDependencyListener} from '../test/common-test-setup';
suite('gr-app tests', () => {
let grApp: GrApp;
@@ -34,9 +36,14 @@
stubRestApi('getPreferences').returns(Promise.resolve(createPreferences()));
stubRestApi('getVersion').returns(Promise.resolve('42'));
stubRestApi('probePath').returns(Promise.resolve(false));
-
grApp = await fixture<GrApp>(html`<gr-app id="app"></gr-app>`);
- await grApp.updateComplete;
+ });
+
+ test('models resolve', () => {
+ // Verify that models resolve on grApp without falling back
+ // to the ones instantiated by the test-setup.
+ removeRequestDependencyListener();
+ assert.ok(resolve(grApp, routerToken)());
});
test('reporting', () => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
index b0993b9..2add2bb 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
@@ -16,8 +16,7 @@
@state()
config?: ServerInfo;
- // visible for testing
- readonly getConfigModel = resolve(this, configModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
constructor() {
super();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
index bb89d12..a58314e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.ts
@@ -10,10 +10,16 @@
import {fixture, html, assert} from '@open-wc/testing';
import {SinonStub} from 'sinon';
import {createServerInfo} from '../../../test/test-data-generators';
+import {
+ ConfigModel,
+ configModelToken,
+} from '../../../models/config/config-model';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-plugin-host tests', () => {
let element: GrPluginHost;
let loadPluginsStub: SinonStub;
+ let configModel: ConfigModel;
setup(async () => {
loadPluginsStub = sinon.stub(getPluginLoader(), 'loadPlugins');
@@ -21,13 +27,14 @@
<gr-plugin-host></gr-plugin-host>
`);
await element.updateComplete;
+ configModel = testResolver(configModelToken);
sinon.stub(document.body, 'appendChild');
});
test('load plugins should be called', async () => {
loadPluginsStub.reset();
- element.getConfigModel().updateServerConfig({
+ configModel.updateServerConfig({
...createServerInfo(),
plugin: {
has_avatars: false,
@@ -46,7 +53,7 @@
test('theme plugins should be loaded if enabled', async () => {
loadPluginsStub.reset();
- element.getConfigModel().updateServerConfig({
+ configModel.updateServerConfig({
...createServerInfo(),
default_theme: 'gerrit-theme.js',
plugin: {
@@ -69,7 +76,7 @@
loadPluginsStub.reset();
const config = createServerInfo();
config.gerrit.instance_id = 'test-id';
- element.getConfigModel().updateServerConfig(config);
+ configModel.updateServerConfig(config);
assert.isTrue(loadPluginsStub.calledOnce);
assert.isTrue(loadPluginsStub.calledWith([], 'test-id'));
});
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 99b47da..f63f34e 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -6,9 +6,11 @@
import '@polymer/iron-input/iron-input';
import '../../shared/gr-avatar/gr-avatar';
import '../../shared/gr-date-formatter/gr-date-formatter';
+import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../../../styles/gr-form-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-account-chip/gr-account-chip';
+import '../../shared/gr-hovercard-account/gr-hovercard-account-contents';
import {AccountDetailInfo, ServerInfo} from '../../../types/common';
import {EditableAccountField} from '../../../constants/constants';
import {getAppContext} from '../../../services/app-context';
@@ -76,6 +78,15 @@
div section.hide {
display: none;
}
+ gr-hovercard-account-contents {
+ display: block;
+ max-width: 600px;
+ margin-top: var(--spacing-m);
+ background: var(--dialog-background-color);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--elevation-level-5);
+ }
`,
];
@@ -211,9 +222,20 @@
</span>
</section>
<section>
- <span class="title">Account chip preview</span>
+ <span class="title">
+ <gr-tooltip-content
+ title="This is how you appear to others"
+ has-tooltip
+ show-icon
+ >
+ Account preview
+ </gr-tooltip-content>
+ </span>
<span class="value">
<gr-account-chip .account=${this.account}></gr-account-chip>
+ <gr-hovercard-account-contents
+ .account=${this.account}
+ ></gr-hovercard-account-contents>
</span>
</section>
</div>`;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
index bcd3964..f7e5dee 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
@@ -110,8 +110,19 @@
</span>
</section>
<section>
- <span class="title">Account chip preview</span>
- <span class="value"><gr-account-chip></gr-account-chip></span>
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip=""
+ show-icon=""
+ title="This is how you appear to others"
+ >
+ Account preview
+ </gr-tooltip-content>
+ </span>
+ <span class="value"
+ ><gr-account-chip></gr-account-chip>
+ <gr-hovercard-account-contents></gr-hovercard-account-contents>
+ </span>
</section>
</div>
`
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 f554ff0..183425d 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
@@ -7,7 +7,6 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
import {EditPreferencesInfo} from '../../../types/common';
-import {getAppContext} from '../../../services/app-context';
import {formStyles} from '../../../styles/gr-form-styles';
import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -15,6 +14,8 @@
import {customElement, query, state} from 'lit/decorators.js';
import {convertToString} from '../../../utils/string-util';
import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-edit-preferences')
export class GrEditPreferences extends LitElement {
@@ -46,13 +47,13 @@
@state() private originalEditPrefs?: EditPreferencesInfo;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
subscribe(
this,
- () => this.userModel.editPreferences$,
+ () => this.getUserModel().editPreferences$,
editPreferences => {
this.originalEditPrefs = editPreferences;
this.editPrefs = {...editPreferences};
@@ -307,7 +308,7 @@
async save() {
if (!this.editPrefs) return;
- await this.userModel.updateEditPreference(this.editPrefs);
+ await this.getUserModel().updateEditPreference(this.editPrefs);
}
}
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 460cc7c..9c23857 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
@@ -12,12 +12,13 @@
import {state, customElement} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
import {subscribe} from '../../lit/subscription-controller';
-import {getAppContext} from '../../../services/app-context';
import {deepEqual} from '../../../utils/deep-util';
import {createDefaultPreferences} from '../../../constants/constants';
import {fontStyles} from '../../../styles/gr-font-styles';
import {classMap} from 'lit/directives/class-map.js';
import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
@customElement('gr-menu-editor')
export class GrMenuEditor extends LitElement {
@@ -33,13 +34,13 @@
@state()
newUrl = '';
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
this.originalPrefs = prefs;
this.menuItems = [...prefs.my];
@@ -196,7 +197,7 @@
}
private handleSave() {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
...this.originalPrefs,
my: this.menuItems,
});
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 ff2904a..92b1f86 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
@@ -63,6 +63,7 @@
import {resolve} from '../../../models/dependency';
import {settingsViewModelToken} from '../../../models/views/settings';
import {areNotificationsEnabled} from '../../../utils/worker-util';
+import {userModelToken} from '../../../models/user/user-model';
const GERRIT_DOCS_BASE_URL =
'https://gerrit-review.googlesource.com/' + 'Documentation';
@@ -201,7 +202,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
// private but used in test
readonly flagsService = getAppContext().flagsService;
@@ -220,14 +221,14 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
acc => {
this.account = acc;
}
);
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
if (!prefs) {
throw new Error('getPreferences returned undefined');
@@ -1148,7 +1149,7 @@
// private but used in test
handleSavePreferences() {
- return this.userModel.updatePreferences(this.localPrefs);
+ return this.getUserModel().updatePreferences(this.localPrefs);
}
// private but used in test
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index aa5fd58e..bb0200a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -22,6 +22,8 @@
import {getRemovedByIconClickReason} from '../../../utils/attention-set-util';
import {ifDefined} from 'lit/directives/if-defined.js';
import {createSearchUrl} from '../../../models/views/search';
+import {accountsModelToken} from '../../../models/accounts-model/accounts-model';
+import {resolve} from '../../../models/dependency';
@customElement('gr-account-label')
export class GrAccountLabel extends LitElement {
@@ -97,7 +99,7 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly accountsModel = getAppContext().accountsModel;
+ private readonly getAccountsModel = resolve(this, accountsModelToken);
static override get styles() {
return [
@@ -190,7 +192,7 @@
override async updated() {
assertIsDefined(this.account, 'account');
- const account = await this.accountsModel.fillDetails(this.account);
+ const account = await this.getAccountsModel().fillDetails(this.account);
if (account) this.account = account;
}
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 d02801a..df41b1f4 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
@@ -74,6 +74,8 @@
import {HtmlPatched} from '../../../utils/lit-util';
import {createDiffUrl} from '../../../models/views/diff';
import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
+import {highlightServiceToken} from '../../../services/highlight/highlight-service';
declare global {
interface HTMLElementEventMap {
@@ -247,18 +249,20 @@
@state()
saving = false;
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly reporting = getAppContext().reportingService;
private readonly shortcuts = new ShortcutController(this);
- private readonly syntaxLayer = new GrSyntaxLayerWorker();
+ private readonly syntaxLayer = new GrSyntaxLayerWorker(
+ resolve(this, highlightServiceToken),
+ () => getAppContext().reportingService
+ );
// for COMMENTS_AUTOCLOSE logging purposes only
readonly uid = performance.now().toString(36) + Math.random().toString(36);
@@ -281,7 +285,7 @@
);
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
@@ -291,12 +295,12 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
x => this.syntaxLayer.setEnabled(!!x.syntax_highlighting)
);
subscribe(
this,
- () => this.userModel.preferences$,
+ () => this.getUserModel().preferences$,
prefs => {
const layers: DiffLayer[] = [this.syntaxLayer];
if (!prefs.disable_token_highlighting) {
@@ -307,7 +311,7 @@
);
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
prefs => {
this.prefs = {
...prefs,
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 10e54c7..97eafc9 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
@@ -30,6 +30,8 @@
import {GrButton} from '../gr-button/gr-button';
import {SpecialFilePath} from '../../../constants/constants';
import {GrIcon} from '../gr-icon/gr-icon';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {testResolver} from '../../../test/common-test-setup';
const c1 = {
author: {name: 'Kermit'},
@@ -311,7 +313,7 @@
setup(async () => {
savePromise = mockPromise<DraftInfo>();
stub = sinon
- .stub(element.getCommentsModel(), 'saveDraft')
+ .stub(testResolver(commentsModelToken), 'saveDraft')
.returns(savePromise);
element.thread = createThread(c1, {...c2, unresolved: true});
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 00c2d12..af7c64f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -64,6 +64,7 @@
import {KnownExperimentId} from '../../../services/flags/flags';
import {isBase64FileContent} from '../../../api/rest-api';
import {createDiffUrl} from '../../../models/views/diff';
+import {userModelToken} from '../../../models/user/user-model';
const UNSAVED_MESSAGE = 'Unable to save draft';
@@ -229,10 +230,9 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- // Private but used in tests.
- readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private readonly shortcuts = new ShortcutController(this);
@@ -282,12 +282,12 @@
}
subscribe(
this,
- () => this.userModel.account$,
+ () => this.getUserModel().account$,
x => (this.account = x)
);
subscribe(
this,
- () => this.userModel.isAdmin$,
+ () => this.getUserModel().isAdmin$,
x => (this.isAdmin = x)
);
@@ -811,7 +811,8 @@
// fixed. Currently diff line doesn't match commit message line, because
// of metadata in diff, which aren't in content api request.
if (this.comment.path === SpecialFilePath.COMMIT_MESSAGE) return nothing;
- if (this.isOwner) return nothing;
+ // TODO(milutin): disable user suggestions for owners, after user study.
+ // if (this.isOwner) return nothing;
return html`<gr-button
link
class="action suggestEdit"
@@ -976,7 +977,7 @@
override updated(changed: PropertyValues) {
if (changed.has('editing')) {
- if (this.editing) {
+ if (this.editing && !this.permanentEditingMode) {
whenVisible(this, () => this.textarea?.putCursorAtEnd());
}
}
@@ -1088,6 +1089,10 @@
return !this.messageText?.trimEnd();
}
+ override focus() {
+ this.textarea?.focus();
+ }
+
private handleEsc() {
// vim users don't like ESC to cancel/discard, so only do this when the
// comment text is empty.
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 1b48658..4e8c70f 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
@@ -47,9 +47,15 @@
import {SinonStub} from 'sinon';
import {fixture, html, assert} from '@open-wc/testing';
import {GrButton} from '../gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../../../models/comments/comments-model';
suite('gr-comment tests', () => {
let element: GrComment;
+ let commentsModel: CommentsModel;
const account = {
email: 'dhruvsri@google.com' as EmailAddress,
name: 'Dhruv Srivastava',
@@ -77,6 +83,7 @@
.comment=${comment}
></gr-comment>`
);
+ commentsModel = testResolver(commentsModelToken);
});
suite('DOM rendering', () => {
@@ -548,9 +555,7 @@
test('save', async () => {
const savePromise = mockPromise<DraftInfo>();
- const stub = sinon
- .stub(element.getCommentsModel(), 'saveDraft')
- .returns(savePromise);
+ const stub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise);
element.comment = createDraft();
element.editing = true;
@@ -595,7 +600,7 @@
test('save failed', async () => {
sinon
- .stub(element.getCommentsModel(), 'saveDraft')
+ .stub(commentsModel, 'saveDraft')
.returns(Promise.reject(new Error('saving failed')));
element.comment = createDraft();
@@ -615,7 +620,7 @@
test('discard', async () => {
const discardPromise = mockPromise<void>();
const stub = sinon
- .stub(element.getCommentsModel(), 'discardDraft')
+ .stub(commentsModel, 'discardDraft')
.returns(discardPromise);
element.comment = createDraft();
@@ -638,7 +643,7 @@
});
test('resolved comment state indicated by checkbox', async () => {
- const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
+ const saveStub = sinon.stub(commentsModel, 'saveDraft');
element.comment = {
...createComment(),
__draft: true,
@@ -662,11 +667,8 @@
});
test('saving empty text calls discard()', async () => {
- const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
- const discardStub = sinon.stub(
- element.getCommentsModel(),
- 'discardDraft'
- );
+ const saveStub = sinon.stub(commentsModel, 'saveDraft');
+ const discardStub = sinon.stub(commentsModel, 'discardDraft');
element.comment = createDraft();
element.editing = true;
await element.updateComplete;
@@ -740,9 +742,7 @@
setup(async () => {
clock = sinon.useFakeTimers();
savePromise = mockPromise<DraftInfo>();
- saveStub = sinon
- .stub(element.getCommentsModel(), 'saveDraft')
- .returns(savePromise);
+ saveStub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise);
element.comment = createUnsaved();
element.editing = true;
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 15d7072..019bec1 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
@@ -8,7 +8,6 @@
import '../gr-button/gr-button';
import '../gr-select/gr-select';
import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
-import {getAppContext} from '../../../services/app-context';
import {subscribe} from '../../lit/subscription-controller';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -18,6 +17,8 @@
import {fire} from '../../../utils/event-util';
import {ValueChangedEvent} from '../../../types/events';
import {GrSelect} from '../gr-select/gr-select';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-diff-preferences')
export class GrDiffPreferences extends LitElement {
@@ -51,13 +52,13 @@
@state() private originalDiffPrefs?: DiffPreferencesInfo;
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
constructor() {
super();
subscribe(
this,
- () => this.userModel.diffPreferences$,
+ () => this.getUserModel().diffPreferences$,
diffPreferences => {
if (!diffPreferences) return;
this.originalDiffPrefs = diffPreferences;
@@ -314,7 +315,7 @@
async save() {
if (!this.diffPrefs) return;
- await this.userModel.updateDiffPreference(this.diffPrefs);
+ await this.getUserModel().updateDiffPreference(this.diffPrefs);
fire(this, 'has-unsaved-changes-changed', {
value: this.hasUnsavedChanges(),
});
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 2d93227..886894e 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -16,6 +16,8 @@
import {customElement, property, state} from 'lit/decorators.js';
import {fire} from '../../../utils/event-util';
import {BindValueChangeEvent} from '../../../types/events';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
declare global {
interface HTMLElementEventMap {
@@ -53,7 +55,7 @@
private readonly restApiService = getAppContext().restApiService;
// Private but used in tests.
- readonly userModel = getAppContext().userModel;
+ readonly getUserModel = resolve(this, userModelToken);
private subscriptions: Subscription[] = [];
@@ -63,7 +65,7 @@
this.loggedIn = loggedIn;
});
this.subscriptions.push(
- this.userModel.preferences$.subscribe(prefs => {
+ this.getUserModel().preferences$.subscribe(prefs => {
if (prefs?.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
this.selectedScheme = prefs.download_scheme.toLowerCase();
@@ -194,7 +196,7 @@
this.selectedScheme = scheme;
fire(this, 'selected-scheme-changed', {value: scheme});
if (this.loggedIn) {
- this.userModel.updatePreferences({
+ this.getUserModel().updatePreferences({
download_scheme: this.selectedScheme,
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 695c674..b1d4e36 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -18,6 +18,8 @@
import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
import {fixture, html, assert} from '@open-wc/testing';
import {PaperTabElement} from '@polymer/paper-tabs/paper-tab';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-download-commands', () => {
let element: GrDownloadCommands;
@@ -170,11 +172,16 @@
});
});
suite('authenticated', () => {
- test('loads scheme from preferences', async () => {
- const element: GrDownloadCommands = await fixture(
+ let element: GrDownloadCommands;
+ let userModel: UserModel;
+ setup(async () => {
+ userModel = testResolver(userModelToken);
+ element = await fixture(
html`<gr-download-commands></gr-download-commands>`
);
- element.userModel.setPreferences({
+ });
+ test('loads scheme from preferences', async () => {
+ userModel.setPreferences({
...createPreferences(),
download_scheme: 'repo',
});
@@ -182,10 +189,7 @@
});
test('normalize scheme from preferences', async () => {
- const element: GrDownloadCommands = await fixture(
- html`<gr-download-commands></gr-download-commands>`
- );
- element.userModel.setPreferences({
+ userModel.setPreferences({
...createPreferences(),
download_scheme: 'REPO',
});
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 d6a4d94..e176598 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
@@ -26,6 +26,8 @@
import {classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';
import {fontStyles} from '../../../styles/gr-font-styles';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {resolve} from '../../../models/dependency';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -90,7 +92,7 @@
@state() newContent = '';
- private readonly storage = getAppContext().storageService;
+ private readonly getStorage = resolve(this, storageServiceToken);
private readonly reporting = getAppContext().reportingService;
@@ -321,14 +323,14 @@
this.storeTask,
() => {
if (this.newContent.length) {
- this.storage.setEditableContentItem(storageKey, this.newContent);
+ this.getStorage().setEditableContentItem(storageKey, this.newContent);
} else {
// This does not really happen, because we don't clear newContent
// after saving (see below). So this only occurs when the user clears
// all the content in the editable textarea. But GrStorage cleans
// up itself after one day, so we are not so concerned about leaving
// some garbage behind.
- this.storage.eraseEditableContentItem(storageKey);
+ this.getStorage().eraseEditableContentItem(storageKey);
}
},
STORAGE_DEBOUNCE_INTERVAL_MS
@@ -358,7 +360,7 @@
let content;
if (this.storageKey) {
- const storedContent = this.storage.getEditableContentItem(
+ const storedContent = this.getStorage().getEditableContentItem(
this.storageKey
);
if (storedContent?.message) {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
index fec347c6..b4f25ce 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.ts
@@ -6,17 +6,22 @@
import '../../../test/common-test-setup';
import './gr-editable-content';
import {GrEditableContent} from './gr-editable-content';
-import {query, queryAndAssert, stubStorage} from '../../../test/test-utils';
+import {query, queryAndAssert} from '../../../test/test-utils';
import {GrButton} from '../gr-button/gr-button';
import {fixture, html, assert} from '@open-wc/testing';
import {EventType} from '../../../types/events';
+import {StorageService} from '../../../services/storage/gr-storage';
+import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-editable-content tests', () => {
let element: GrEditableContent;
+ let storageService: StorageService;
setup(async () => {
element = await fixture(html`<gr-editable-content></gr-editable-content>`);
await element.updateComplete;
+ storageService = testResolver(storageServiceToken);
});
test('renders', () => {
@@ -177,7 +182,7 @@
});
test('editing toggled to true, has stored data', async () => {
- stubStorage('getEditableContentItem').returns({
+ sinon.stub(storageService, 'getEditableContentItem').returns({
message: 'stored content',
updated: 0,
});
@@ -189,7 +194,7 @@
});
test('editing toggled to true, has no stored data', async () => {
- stubStorage('getEditableContentItem').returns(null);
+ sinon.stub(storageService, 'getEditableContentItem').returns(null);
element.editing = true;
await element.updateComplete;
@@ -199,8 +204,8 @@
});
test('edits are cached', async () => {
- const storeStub = stubStorage('setEditableContentItem');
- const eraseStub = stubStorage('eraseEditableContentItem');
+ const storeStub = sinon.stub(storageService, 'setEditableContentItem');
+ const eraseStub = sinon.stub(storageService, 'eraseEditableContentItem');
element.editing = true;
// Needed because editingChanged resets newContent
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
new file mode 100644
index 0000000..dc08648
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -0,0 +1,566 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../gr-avatar/gr-avatar';
+import '../gr-button/gr-button';
+import '../gr-icon/gr-icon';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import {getAppContext} from '../../../services/app-context';
+import {
+ accountKey,
+ computeVoteableText,
+ isAccountEmailOnly,
+ isSelf,
+} from '../../../utils/account-util';
+import {customElement, property, state} from 'lit/decorators.js';
+import {
+ AccountInfo,
+ ChangeInfo,
+ ServerInfo,
+ ReviewInput,
+} from '../../../types/common';
+import {
+ canHaveAttention,
+ getAddedByReason,
+ getLastUpdate,
+ getReason,
+ getRemovedByReason,
+ hasAttention,
+} from '../../../utils/attention-set-util';
+import {ReviewerState} from '../../../constants/constants';
+import {CURRENT} from '../../../utils/patch-set-util';
+import {isInvolved, isRemovableReviewer} from '../../../utils/change-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {css, html, LitElement, nothing} from 'lit';
+import {ifDefined} from 'lit/directives/if-defined.js';
+import {EventType} from '../../../types/events';
+import {subscribe} from '../../lit/subscription-controller';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
+import {createSearchUrl} from '../../../models/views/search';
+import {createDashboardUrl} from '../../../models/views/dashboard';
+import {fire, fireEvent} from '../../../utils/event-util';
+import {userModelToken} from '../../../models/user/user-model';
+
+@customElement('gr-hovercard-account-contents')
+export class GrHovercardAccountContents extends LitElement {
+ @property({type: Object})
+ account!: AccountInfo;
+
+ @state()
+ selfAccount?: AccountInfo;
+
+ /**
+ * Optional ChangeInfo object, typically comes from the change page or
+ * from a row in a list of search results. This is needed for some change
+ * related features like adding the user as a reviewer.
+ */
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ /**
+ * Should attention set related features be shown in the component? Note
+ * that the information whether the user is in the attention set or not is
+ * part of the ChangeInfo object in the change property.
+ */
+ @property({type: Boolean})
+ highlightAttention = false;
+
+ @state()
+ serverConfig?: ServerInfo;
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly reporting = getAppContext().reportingService;
+
+ private readonly getUserModel = resolve(this, userModelToken);
+
+ private readonly getConfigModel = resolve(this, configModelToken);
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getUserModel().account$,
+ x => (this.selfAccount = x)
+ );
+ subscribe(
+ this,
+ () => this.getConfigModel().serverConfig$,
+ config => {
+ this.serverConfig = config;
+ }
+ );
+ }
+
+ static override get styles() {
+ return [
+ sharedStyles,
+ fontStyles,
+ css`
+ .top,
+ .attention,
+ .status,
+ .voteable {
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .links {
+ padding: var(--spacing-m) 0px var(--spacing-l) var(--spacing-xxl);
+ }
+ .top {
+ display: flex;
+ padding-top: var(--spacing-xl);
+ min-width: 300px;
+ }
+ gr-avatar {
+ height: 48px;
+ width: 48px;
+ margin-right: var(--spacing-l);
+ }
+ .title,
+ .email {
+ color: var(--deemphasized-text-color);
+ }
+ .action {
+ border-top: 1px solid var(--border-color);
+ padding: var(--spacing-s) var(--spacing-l);
+ --gr-button-padding: var(--spacing-s) var(--spacing-m);
+ }
+ .attention {
+ background-color: var(--emphasis-color);
+ }
+ .attention a {
+ text-decoration: none;
+ }
+ .status gr-icon {
+ font-size: 14px;
+ position: relative;
+ top: 2px;
+ }
+ gr-icon.attentionIcon {
+ transform: scaleX(0.8);
+ }
+ gr-icon.linkIcon {
+ font-size: var(--line-height-normal, 20px);
+ color: var(--deemphasized-text-color);
+ padding-right: 12px;
+ }
+ .links a {
+ color: var(--link-color);
+ padding: 0px 4px;
+ }
+ .reason {
+ padding-top: var(--spacing-s);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar .account=${this.account} .imageSize=${56}></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="name heading-3">${this.account.name}</h3>
+ <div class="email">${this.account.email}</div>
+ </div>
+ </div>
+ ${this.renderAccountStatusPlugins()} ${this.renderAccountStatus()}
+ ${this.renderLinks()} ${this.renderChangeRelatedInfoAndActions()}
+ `;
+ }
+
+ private renderChangeRelatedInfoAndActions() {
+ if (this.change === undefined) {
+ return nothing;
+ }
+ const voteableText = computeVoteableText(this.change, this.account);
+ return html`
+ ${voteableText
+ ? html`
+ <div class="voteable">
+ <span class="title">Voteable:</span>
+ <span class="value">${voteableText}</span>
+ </div>
+ `
+ : ''}
+ ${this.renderNeedsAttention()} ${this.renderAddToAttention()}
+ ${this.renderRemoveFromAttention()} ${this.renderReviewerOrCcActions()}
+ `;
+ }
+
+ private renderReviewerOrCcActions() {
+ // `selfAccount` is required so that logged out users can't perform actions.
+ if (!this.selfAccount || !isRemovableReviewer(this.change, this.account))
+ return nothing;
+ return html`
+ <div class="action">
+ <gr-button
+ class="removeReviewerOrCC"
+ link
+ no-uppercase
+ @click=${this.handleRemoveReviewerOrCC}
+ >
+ Remove ${this.computeReviewerOrCCText()}
+ </gr-button>
+ </div>
+ <div class="action">
+ <gr-button
+ class="changeReviewerOrCC"
+ link
+ no-uppercase
+ @click=${this.handleChangeReviewerOrCCStatus}
+ >
+ ${this.computeChangeReviewerOrCCText()}
+ </gr-button>
+ </div>
+ `;
+ }
+
+ private renderAccountStatusPlugins() {
+ return html`
+ <gr-endpoint-decorator name="hovercard-status">
+ <gr-endpoint-param
+ name="account"
+ .value=${this.account}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `;
+ }
+
+ private renderLinks() {
+ if (!this.account || isAccountEmailOnly(this.account)) return nothing;
+ return html` <div class="links">
+ <gr-icon icon="link" class="linkIcon"></gr-icon>
+ <a
+ href=${ifDefined(this.computeOwnerChangesLink())}
+ @click=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ @enter=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ >
+ Changes
+ </a>
+ ·
+ <a
+ href=${ifDefined(this.computeOwnerDashboardLink())}
+ @click=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ @enter=${() => {
+ fireEvent(this, 'link-clicked');
+ }}
+ >
+ Dashboard
+ </a>
+ </div>`;
+ }
+
+ private renderAccountStatus() {
+ if (!this.account.status) return nothing;
+ return html`
+ <div class="status">
+ <span class="title">About me:</span>
+ <span class="value">${this.account.status}</span>
+ </div>
+ `;
+ }
+
+ private renderNeedsAttention() {
+ if (!(this.isAttentionEnabled && this.hasUserAttention)) return nothing;
+ const lastUpdate = getLastUpdate(this.account, this.change);
+ return html`
+ <div class="attention">
+ <div>
+ <gr-icon
+ icon="label_important"
+ filled
+ small
+ class="attentionIcon"
+ ></gr-icon>
+ <span> ${this.computePronoun()} turn to take action. </span>
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
+ target="_blank"
+ >
+ <gr-icon icon="help" title="read documentation"></gr-icon>
+ </a>
+ </div>
+ <div class="reason">
+ <span class="title">Reason:</span>
+ <span class="value">
+ ${getReason(this.serverConfig, this.account, this.change)}
+ </span>
+ ${lastUpdate
+ ? html` (
+ <gr-date-formatter
+ withTooltip
+ .dateStr=${lastUpdate}
+ ></gr-date-formatter>
+ )`
+ : ''}
+ </div>
+ </div>
+ `;
+ }
+
+ private renderAddToAttention() {
+ if (!this.computeShowActionAddToAttentionSet()) return nothing;
+ return html`
+ <div class="action">
+ <gr-button
+ class="addToAttentionSet"
+ link
+ no-uppercase
+ @click=${this.handleClickAddToAttentionSet}
+ >
+ Add to attention set
+ </gr-button>
+ </div>
+ `;
+ }
+
+ private renderRemoveFromAttention() {
+ if (!this.computeShowActionRemoveFromAttentionSet()) return nothing;
+ return html`
+ <div class="action">
+ <gr-button
+ class="removeFromAttentionSet"
+ link
+ no-uppercase
+ @click=${this.handleClickRemoveFromAttentionSet}
+ >
+ Remove from attention set
+ </gr-button>
+ </div>
+ `;
+ }
+
+ // private but used by tests
+ computePronoun() {
+ if (!this.account || !this.selfAccount) return '';
+ return isSelf(this.account, this.selfAccount) ? 'Your' : 'Their';
+ }
+
+ computeOwnerChangesLink() {
+ if (!this.account) return undefined;
+ return createSearchUrl({
+ owner:
+ this.account.email ||
+ this.account.username ||
+ this.account.name ||
+ `${this.account._account_id}`,
+ });
+ }
+
+ computeOwnerDashboardLink() {
+ if (!this.account) return undefined;
+ if (this.account._account_id)
+ return createDashboardUrl({user: `${this.account._account_id}`});
+ if (this.account.email)
+ return createDashboardUrl({user: this.account.email});
+ return undefined;
+ }
+
+ get isAttentionEnabled() {
+ return (
+ !!this.highlightAttention &&
+ !!this.change &&
+ canHaveAttention(this.account)
+ );
+ }
+
+ get hasUserAttention() {
+ return hasAttention(this.account, this.change);
+ }
+
+ private getReviewerState(change: ChangeInfo) {
+ if (
+ change.reviewers[ReviewerState.REVIEWER]?.some(
+ (reviewer: AccountInfo) =>
+ reviewer._account_id === this.account._account_id
+ )
+ ) {
+ return ReviewerState.REVIEWER;
+ }
+ return ReviewerState.CC;
+ }
+
+ private computeReviewerOrCCText() {
+ if (!this.change || !this.account) return '';
+ return this.getReviewerState(this.change) === ReviewerState.REVIEWER
+ ? 'Reviewer'
+ : 'CC';
+ }
+
+ private computeChangeReviewerOrCCText() {
+ if (!this.change || !this.account) return '';
+ return this.getReviewerState(this.change) === ReviewerState.REVIEWER
+ ? 'Move Reviewer to CC'
+ : 'Move CC to Reviewer';
+ }
+
+ private handleChangeReviewerOrCCStatus() {
+ assertIsDefined(this.change, 'change');
+ // accountKey() throws an error if _account_id & email is not found, which
+ // we want to check before showing reloading toast
+ const _accountKey = accountKey(this.account);
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Reloading page...',
+ });
+ const reviewInput: Partial<ReviewInput> = {};
+ reviewInput.reviewers = [
+ {
+ reviewer: _accountKey,
+ state:
+ this.getReviewerState(this.change) === ReviewerState.CC
+ ? ReviewerState.REVIEWER
+ : ReviewerState.CC,
+ },
+ ];
+
+ this.restApiService
+ .saveChangeReview(this.change._number, CURRENT, reviewInput)
+ .then(response => {
+ if (!response || !response.ok) {
+ throw new Error(
+ 'something went wrong when toggling' +
+ this.getReviewerState(this.change!)
+ );
+ }
+ fire(this, 'reload', {clearPatchset: true});
+ });
+ }
+
+ private handleRemoveReviewerOrCC() {
+ if (!this.change || !(this.account?._account_id || this.account?.email))
+ throw new Error('Missing change or account.');
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Reloading page...',
+ });
+ this.restApiService
+ .removeChangeReviewer(
+ this.change._number,
+ (this.account?._account_id || this.account?.email)!
+ )
+ .then((response: Response | undefined) => {
+ if (!response || !response.ok) {
+ throw new Error('something went wrong when removing user');
+ }
+ fire(this, 'reload', {clearPatchset: true});
+ return response;
+ });
+ }
+
+ private computeShowActionAddToAttentionSet() {
+ const involvedOrSelf =
+ isInvolved(this.change, this.selfAccount) ||
+ isSelf(this.account, this.selfAccount);
+ return involvedOrSelf && this.isAttentionEnabled && !this.hasUserAttention;
+ }
+
+ private computeShowActionRemoveFromAttentionSet() {
+ const involvedOrSelf =
+ isInvolved(this.change, this.selfAccount) ||
+ isSelf(this.account, this.selfAccount);
+ return involvedOrSelf && this.isAttentionEnabled && this.hasUserAttention;
+ }
+
+ private handleClickAddToAttentionSet() {
+ if (!this.change || !this.account._account_id) return;
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Reloading page...',
+ dismissOnNavigation: true,
+ });
+
+ // We are deliberately updating the UI before making the API call. It is a
+ // risk that we are taking to achieve a better UX for 99.9% of the cases.
+ const reason = getAddedByReason(this.selfAccount, this.serverConfig);
+
+ if (!this.change.attention_set) this.change.attention_set = {};
+ this.change.attention_set[this.account._account_id] = {
+ account: this.account,
+ reason,
+ reason_account: this.selfAccount,
+ };
+ fireEvent(this, 'attention-set-updated');
+
+ this.reporting.reportInteraction(
+ 'attention-hovercard-add',
+ this.reportingDetails()
+ );
+ this.restApiService
+ .addToAttentionSet(this.change._number, this.account._account_id, reason)
+ .then(() => {
+ fireEvent(this, 'hide-alert');
+ });
+ fireEvent(this, 'action-taken');
+ }
+
+ private handleClickRemoveFromAttentionSet() {
+ if (!this.change || !this.account._account_id) return;
+ fire(this, EventType.SHOW_ALERT, {
+ message: 'Saving attention set update ...',
+ dismissOnNavigation: true,
+ });
+
+ // We are deliberately updating the UI before making the API call. It is a
+ // risk that we are taking to achieve a better UX for 99.9% of the cases.
+
+ const reason = getRemovedByReason(this.selfAccount, this.serverConfig);
+ if (this.change.attention_set)
+ delete this.change.attention_set[this.account._account_id];
+ fireEvent(this, 'attention-set-updated');
+
+ this.reporting.reportInteraction(
+ 'attention-hovercard-remove',
+ this.reportingDetails()
+ );
+ this.restApiService
+ .removeFromAttentionSet(
+ this.change._number,
+ this.account._account_id,
+ reason
+ )
+ .then(() => {
+ fireEvent(this, 'hide-alert');
+ });
+ fireEvent(this, 'action-taken');
+ }
+
+ private reportingDetails() {
+ const targetId = this.account._account_id;
+ const ownerId =
+ (this.change && this.change.owner && this.change.owner._account_id) || -1;
+ const selfId = (this.selfAccount && this.selfAccount._account_id) || -1;
+ const reviewers =
+ this.change && this.change.reviewers && this.change.reviewers.REVIEWER
+ ? [...this.change.reviewers.REVIEWER]
+ : [];
+ const reviewerIds = reviewers
+ .map(r => r._account_id)
+ .filter(rId => rId !== ownerId);
+ return {
+ actionByOwner: selfId === ownerId,
+ actionByReviewer: selfId !== -1 && reviewerIds.includes(selfId),
+ targetIsOwner: targetId === ownerId,
+ targetIsReviewer: reviewerIds.includes(targetId),
+ targetIsSelf: targetId === selfId,
+ };
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-hovercard-account-contents': GrHovercardAccountContents;
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
new file mode 100644
index 0000000..b217562
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -0,0 +1,386 @@
+/**
+ * @license
+ * Copyright 2020 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {fixture, assert} from '@open-wc/testing';
+import {html} from 'lit';
+import './gr-hovercard-account-contents';
+import {GrHovercardAccountContents} from './gr-hovercard-account-contents';
+import {
+ mockPromise,
+ query,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {
+ AccountDetailInfo,
+ AccountId,
+ EmailAddress,
+ ReviewerState,
+} from '../../../api/rest-api';
+import {
+ createAccountDetailWithId,
+ createChange,
+ createDetailedLabelInfo,
+} from '../../../test/test-data-generators';
+import {GrButton} from '../gr-button/gr-button';
+import {EventType} from '../../../types/events';
+import {testResolver} from '../../../test/common-test-setup';
+import {userModelToken} from '../../../models/user/user-model';
+
+suite('gr-hovercard-account-contents tests', () => {
+ let element: GrHovercardAccountContents;
+
+ const ACCOUNT: AccountDetailInfo = {
+ ...createAccountDetailWithId(31),
+ email: 'kermit@gmail.com' as EmailAddress,
+ username: 'kermit',
+ name: 'Kermit The Frog',
+ status: 'I am a frog',
+ _account_id: 31415926535 as AccountId,
+ };
+
+ setup(async () => {
+ const change = {
+ ...createChange(),
+ attention_set: {},
+ reviewers: {},
+ owner: {...ACCOUNT},
+ };
+ element = await fixture(
+ html`<gr-hovercard-account-contents .account=${ACCOUNT} .change=${change}>
+ </gr-hovercard-account-contents>`
+ );
+ testResolver(userModelToken).setAccount({...ACCOUNT});
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar hidden=""></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="heading-3 name">Kermit The Frog</h3>
+ <div class="email">kermit@gmail.com</div>
+ </div>
+ </div>
+ <gr-endpoint-decorator name="hovercard-status">
+ <gr-endpoint-param name="account"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <div class="status">
+ <span class="title">About me:</span>
+ <span class="value">I am a frog</span>
+ </div>
+ <div class="links">
+ <gr-icon icon="link" class="linkIcon"></gr-icon>
+ <a href="/q/owner:kermit%2540gmail.com">Changes</a>
+ ·
+ <a href="/dashboard/31415926535">Dashboard</a>
+ </div>
+ `
+ );
+ });
+
+ test('renders without change data', async () => {
+ const elementWithoutChange = await fixture(
+ html`<gr-hovercard-account-contents
+ .account=${ACCOUNT}
+ ></gr-hovercard-account-contents>`
+ );
+ assert.shadowDom.equal(
+ elementWithoutChange,
+ /* HTML */ `
+ <div class="top">
+ <div class="avatar">
+ <gr-avatar hidden=""></gr-avatar>
+ </div>
+ <div class="account">
+ <h3 class="heading-3 name">Kermit The Frog</h3>
+ <div class="email">kermit@gmail.com</div>
+ </div>
+ </div>
+ <gr-endpoint-decorator name="hovercard-status">
+ <gr-endpoint-param name="account"> </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <div class="status">
+ <span class="title"> About me: </span>
+ <span class="value"> I am a frog </span>
+ </div>
+ <div class="links">
+ <gr-icon class="linkIcon" icon="link"> </gr-icon>
+ <a href="/q/owner:kermit%2540gmail.com"> Changes </a>
+ ·
+ <a href="/dashboard/31415926535"> Dashboard </a>
+ </div>
+ `
+ );
+ });
+
+ test('account name is shown', () => {
+ const name = queryAndAssert<HTMLHeadingElement>(element, '.name');
+ assert.equal(name.innerText, 'Kermit The Frog');
+ });
+
+ test('computePronoun', async () => {
+ element.account = createAccountDetailWithId(1);
+ element.selfAccount = createAccountDetailWithId(1);
+ await element.updateComplete;
+ assert.equal(element.computePronoun(), 'Your');
+ element.account = createAccountDetailWithId(2);
+ await element.updateComplete;
+ assert.equal(element.computePronoun(), 'Their');
+ });
+
+ test('account status is not shown if the property is not set', async () => {
+ element.account = {...ACCOUNT, status: undefined};
+ await element.updateComplete;
+ assert.isUndefined(query(element, '.status'));
+ });
+
+ test('account status is displayed', () => {
+ const status = queryAndAssert<HTMLSpanElement>(element, '.status .value');
+ assert.equal(status.innerText, 'I am a frog');
+ });
+
+ test('voteable div is not shown if the property is not set', () => {
+ assert.isUndefined(query(element, '.voteable'));
+ });
+
+ test('voteable div is displayed', async () => {
+ element.change = {
+ ...createChange(),
+ labels: {
+ Foo: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ _account_id: 7 as AccountId,
+ permitted_voting_range: {max: 2, min: 0},
+ },
+ ],
+ },
+ Bar: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ ...createAccountDetailWithId(1),
+ permitted_voting_range: {max: 1, min: 0},
+ },
+ {
+ _account_id: 7 as AccountId,
+ permitted_voting_range: {max: 1, min: 0},
+ },
+ ],
+ },
+ FooBar: {
+ ...createDetailedLabelInfo(),
+ all: [{_account_id: 7 as AccountId, value: 0}],
+ },
+ },
+ permitted_labels: {
+ Foo: ['-1', ' 0', '+1', '+2'],
+ FooBar: ['-1', ' 0'],
+ },
+ };
+ element.account = createAccountDetailWithId(1);
+
+ await element.updateComplete;
+ const voteableEl = queryAndAssert<HTMLSpanElement>(
+ element,
+ '.voteable .value'
+ );
+ assert.equal(voteableEl.innerText, 'Bar: +1');
+ });
+
+ test('remove reviewer', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [ACCOUNT],
+ },
+ };
+ await element.updateComplete;
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+ const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
+ assert.isOk(button);
+ assert.equal(button.innerText, 'Remove Reviewer');
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('move reviewer to cc', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [ACCOUNT],
+ },
+ };
+ await element.updateComplete;
+ const saveReviewStub = stubRestApi('saveChangeReview').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+
+ const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
+
+ assert.isOk(button);
+ assert.equal(button.innerText, 'Move Reviewer to CC');
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(saveReviewStub.called);
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('move reviewer to cc', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [],
+ },
+ };
+ await element.updateComplete;
+ const saveReviewStub = stubRestApi('saveChangeReview').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+
+ const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
+ assert.isOk(button);
+ assert.equal(button.innerText, 'Move CC to Reviewer');
+
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(saveReviewStub.called);
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('remove cc', async () => {
+ element.change = {
+ ...createChange(),
+ removable_reviewers: [ACCOUNT],
+ reviewers: {
+ [ReviewerState.REVIEWER]: [],
+ },
+ };
+ await element.updateComplete;
+ stubRestApi('removeChangeReviewer').returns(
+ Promise.resolve({...new Response(), ok: true})
+ );
+ const reloadListener = sinon.spy();
+ element.addEventListener('reload', reloadListener);
+
+ const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
+
+ assert.equal(button.innerText, 'Remove CC');
+ assert.isOk(button);
+ button.click();
+ await element.updateComplete;
+ assert.isTrue(reloadListener.called);
+ });
+
+ test('add to attention set', async () => {
+ const apiPromise = mockPromise<Response>();
+ const apiSpy = stubRestApi('addToAttentionSet').returns(apiPromise);
+ element.highlightAttention = true;
+ await element.updateComplete;
+ const showAlertListener = sinon.spy();
+ const hideAlertListener = sinon.spy();
+ const updatedListener = sinon.spy();
+ element.addEventListener(EventType.SHOW_ALERT, showAlertListener);
+ element.addEventListener('hide-alert', hideAlertListener);
+ element.addEventListener('attention-set-updated', updatedListener);
+
+ const button = queryAndAssert<GrButton>(element, '.addToAttentionSet');
+ assert.isOk(button);
+ button.click();
+
+ assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 1);
+ const attention_set_info = Object.values(
+ element.change?.attention_set ?? {}
+ )[0];
+ assert.equal(
+ attention_set_info.reason,
+ `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
+ ' using the hovercard menu'
+ );
+ assert.equal(
+ attention_set_info.reason_account?._account_id,
+ ACCOUNT._account_id
+ );
+ assert.isTrue(showAlertListener.called, 'showAlertListener was called');
+ assert.isTrue(updatedListener.called, 'updatedListener was called');
+
+ apiPromise.resolve({...new Response(), ok: true});
+ await element.updateComplete;
+ assert.isTrue(apiSpy.calledOnce);
+ assert.equal(
+ apiSpy.lastCall.args[2],
+ `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
+ ' using the hovercard menu'
+ );
+ assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+ });
+
+ test('remove from attention set', async () => {
+ const apiPromise = mockPromise<Response>();
+ const apiSpy = stubRestApi('removeFromAttentionSet').returns(apiPromise);
+ element.highlightAttention = true;
+ element.change = {
+ ...createChange(),
+ attention_set: {
+ '31415926535': {account: ACCOUNT, reason: 'a good reason'},
+ },
+ reviewers: {},
+ owner: {...ACCOUNT},
+ };
+ await element.updateComplete;
+ const showAlertListener = sinon.spy();
+ const hideAlertListener = sinon.spy();
+ const updatedListener = sinon.spy();
+ element.addEventListener(EventType.SHOW_ALERT, showAlertListener);
+ element.addEventListener('hide-alert', hideAlertListener);
+ element.addEventListener('attention-set-updated', updatedListener);
+
+ const button = queryAndAssert<GrButton>(element, '.removeFromAttentionSet');
+ assert.isOk(button);
+ button.click();
+
+ assert.isDefined(element.change?.attention_set);
+ assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 0);
+ assert.isTrue(showAlertListener.called, 'showAlertListener was called');
+ assert.isTrue(updatedListener.called, 'updatedListener was called');
+
+ apiPromise.resolve({...new Response(), ok: true});
+ await element.updateComplete;
+
+ assert.isTrue(apiSpy.calledOnce);
+ assert.equal(
+ apiSpy.lastCall.args[2],
+ `Removed by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
+ ' using the hovercard menu'
+ );
+ assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index 9647141..543f5bc 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -8,42 +8,12 @@
import '../gr-icon/gr-icon';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
-import {getAppContext} from '../../../services/app-context';
-import {
- accountKey,
- computeVoteableText,
- isAccountEmailOnly,
- isSelf,
-} from '../../../utils/account-util';
-import {customElement, property, state} from 'lit/decorators.js';
-import {
- AccountInfo,
- ChangeInfo,
- ServerInfo,
- ReviewInput,
-} from '../../../types/common';
-import {
- canHaveAttention,
- getAddedByReason,
- getLastUpdate,
- getReason,
- getRemovedByReason,
- hasAttention,
-} from '../../../utils/attention-set-util';
-import {ReviewerState} from '../../../constants/constants';
-import {CURRENT} from '../../../utils/patch-set-util';
-import {isInvolved, isRemovableReviewer} from '../../../utils/change-util';
-import {assertIsDefined} from '../../../utils/common-util';
-import {fontStyles} from '../../../styles/gr-font-styles';
-import {css, html, LitElement, nothing} from 'lit';
-import {ifDefined} from 'lit/directives/if-defined.js';
+import {customElement, property} from 'lit/decorators.js';
+import {AccountInfo, ChangeInfo} from '../../../types/common';
+import {html, LitElement} from 'lit';
import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
-import {EventType} from '../../../types/events';
-import {subscribe} from '../../lit/subscription-controller';
-import {resolve} from '../../../models/dependency';
-import {configModelToken} from '../../../models/config/config-model';
-import {createSearchUrl} from '../../../models/views/search';
-import {createDashboardUrl} from '../../../models/views/dashboard';
+import {when} from 'lit/directives/when.js';
+import './gr-hovercard-account-contents';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
const base = HovercardMixin(LitElement);
@@ -53,9 +23,6 @@
@property({type: Object})
account!: AccountInfo;
- @state()
- selfAccount?: AccountInfo;
-
/**
* Optional ChangeInfo object, typically comes from the change page or
* from a row in a list of search results. This is needed for some change
@@ -72,498 +39,30 @@
@property({type: Boolean})
highlightAttention = false;
- @state()
- serverConfig?: ServerInfo;
-
- private readonly restApiService = getAppContext().restApiService;
-
- private readonly reporting = getAppContext().reportingService;
-
- // private but used in tests
- readonly userModel = getAppContext().userModel;
-
- private readonly getConfigModel = resolve(this, configModelToken);
-
- constructor() {
- super();
- subscribe(
- this,
- () => this.userModel.account$,
- x => (this.selfAccount = x)
- );
- subscribe(
- this,
- () => this.getConfigModel().serverConfig$,
- config => {
- this.serverConfig = config;
- }
- );
- }
-
- static override get styles() {
- return [
- fontStyles,
- base.styles || [],
- css`
- .top,
- .attention,
- .status,
- .voteable {
- padding: var(--spacing-s) var(--spacing-l);
- }
- .links {
- padding: var(--spacing-m) 0px var(--spacing-l) var(--spacing-xxl);
- }
- .top {
- display: flex;
- padding-top: var(--spacing-xl);
- min-width: 300px;
- }
- gr-avatar {
- height: 48px;
- width: 48px;
- margin-right: var(--spacing-l);
- }
- .title,
- .email {
- color: var(--deemphasized-text-color);
- }
- .action {
- border-top: 1px solid var(--border-color);
- padding: var(--spacing-s) var(--spacing-l);
- --gr-button-padding: var(--spacing-s) var(--spacing-m);
- }
- .attention {
- background-color: var(--emphasis-color);
- }
- .attention a {
- text-decoration: none;
- }
- .status gr-icon {
- font-size: 14px;
- position: relative;
- top: 2px;
- }
- gr-icon.attentionIcon {
- transform: scaleX(0.8);
- }
- gr-icon.linkIcon {
- font-size: var(--line-height-normal, 20px);
- color: var(--deemphasized-text-color);
- padding-right: 12px;
- }
- .links a {
- color: var(--link-color);
- padding: 0px 4px;
- }
- .reason {
- padding-top: var(--spacing-s);
- }
- `,
- ];
- }
-
override render() {
return html`
<div id="container" role="tooltip" tabindex="-1">
- ${this.renderContent()}
+ ${when(
+ this._isShowing,
+ () =>
+ html`<gr-hovercard-account-contents
+ .account=${this.account}
+ .change=${this.change}
+ .highlightAttention=${this.highlightAttention}
+ @link-clicked=${this.forceHide}
+ @action-taken=${this.mouseHide}
+ @attention-set-updated=${this.redirectEventToTarget}
+ @hide-alert=${this.redirectEventToTarget}
+ @show-alert=${this.redirectEventToTarget}
+ @reload=${this.redirectEventToTarget}
+ ></gr-hovercard-account-contents>`
+ )}
</div>
`;
}
- private renderContent() {
- if (!this._isShowing) return;
- return html`
- <div class="top">
- <div class="avatar">
- <gr-avatar .account=${this.account} imageSize="56"></gr-avatar>
- </div>
- <div class="account">
- <h3 class="name heading-3">${this.account.name}</h3>
- <div class="email">${this.account.email}</div>
- </div>
- </div>
- ${this.renderAccountStatusPlugins()} ${this.renderAccountStatus()}
- ${this.renderLinks()} ${this.renderChangeRelatedInfoAndActions()}
- `;
- }
-
- private renderChangeRelatedInfoAndActions() {
- if (this.change === undefined) {
- return;
- }
- const voteableText = computeVoteableText(this.change, this.account);
- return html`
- ${voteableText
- ? html`
- <div class="voteable">
- <span class="title">Voteable:</span>
- <span class="value">${voteableText}</span>
- </div>
- `
- : ''}
- ${this.renderNeedsAttention()} ${this.renderAddToAttention()}
- ${this.renderRemoveFromAttention()} ${this.renderReviewerOrCcActions()}
- `;
- }
-
- private renderReviewerOrCcActions() {
- if (!this.selfAccount || !isRemovableReviewer(this.change, this.account))
- return;
- return html`
- <div class="action">
- <gr-button
- class="removeReviewerOrCC"
- link=""
- no-uppercase
- @click=${this.handleRemoveReviewerOrCC}
- >
- Remove ${this.computeReviewerOrCCText()}
- </gr-button>
- </div>
- <div class="action">
- <gr-button
- class="changeReviewerOrCC"
- link=""
- no-uppercase
- @click=${this.handleChangeReviewerOrCCStatus}
- >
- ${this.computeChangeReviewerOrCCText()}
- </gr-button>
- </div>
- `;
- }
-
- private renderAccountStatusPlugins() {
- return html`
- <gr-endpoint-decorator name="hovercard-status">
- <gr-endpoint-param
- name="account"
- .value=${this.account}
- ></gr-endpoint-param>
- </gr-endpoint-decorator>
- `;
- }
-
- private renderLinks() {
- if (!this.account || isAccountEmailOnly(this.account)) return nothing;
- return html` <div class="links">
- <gr-icon icon="link" class="linkIcon"></gr-icon
- ><a
- href=${ifDefined(this.computeOwnerChangesLink())}
- @click=${() => {
- this.forceHide();
- return true;
- }}
- @enter=${() => {
- this.forceHide();
- return true;
- }}
- >Changes</a
- >·<a
- href=${ifDefined(this.computeOwnerDashboardLink())}
- @click=${() => {
- this.forceHide();
- return true;
- }}
- @enter=${() => {
- this.forceHide();
- return true;
- }}
- >Dashboard</a
- >
- </div>`;
- }
-
- private renderAccountStatus() {
- if (!this.account.status) return;
- return html`
- <div class="status">
- <span class="title">About me:</span>
- <span class="value">${this.account.status}</span>
- </div>
- `;
- }
-
- private renderNeedsAttention() {
- if (!(this.isAttentionEnabled && this.hasUserAttention)) return;
- const lastUpdate = getLastUpdate(this.account, this.change);
- return html`
- <div class="attention">
- <div>
- <gr-icon
- icon="label_important"
- filled
- small
- class="attentionIcon"
- ></gr-icon>
- <span> ${this.computePronoun()} turn to take action. </span>
- <a
- href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
- target="_blank"
- >
- <gr-icon icon="help" title="read documentation"></gr-icon>
- </a>
- </div>
- <div class="reason">
- <span class="title">Reason:</span>
- <span class="value">
- ${getReason(this.serverConfig, this.account, this.change)}
- </span>
- ${lastUpdate
- ? html` (<gr-date-formatter
- withTooltip
- .dateStr=${lastUpdate}
- ></gr-date-formatter
- >)`
- : ''}
- </div>
- </div>
- `;
- }
-
- private renderAddToAttention() {
- if (!this.computeShowActionAddToAttentionSet()) return;
- return html`
- <div class="action">
- <gr-button
- class="addToAttentionSet"
- link=""
- no-uppercase
- @click=${this.handleClickAddToAttentionSet}
- >
- Add to attention set
- </gr-button>
- </div>
- `;
- }
-
- private renderRemoveFromAttention() {
- if (!this.computeShowActionRemoveFromAttentionSet()) return;
- return html`
- <div class="action">
- <gr-button
- class="removeFromAttentionSet"
- link=""
- no-uppercase
- @click=${this.handleClickRemoveFromAttentionSet}
- >
- Remove from attention set
- </gr-button>
- </div>
- `;
- }
-
- // private but used by tests
- computePronoun() {
- if (!this.account || !this.selfAccount) return '';
- return isSelf(this.account, this.selfAccount) ? 'Your' : 'Their';
- }
-
- computeOwnerChangesLink() {
- if (!this.account) return undefined;
- return createSearchUrl({
- owner:
- this.account.email ||
- this.account.username ||
- this.account.name ||
- `${this.account._account_id}`,
- });
- }
-
- computeOwnerDashboardLink() {
- if (!this.account) return undefined;
- if (this.account._account_id)
- return createDashboardUrl({user: `${this.account._account_id}`});
- if (this.account.email)
- return createDashboardUrl({user: this.account.email});
- return undefined;
- }
-
- get isAttentionEnabled() {
- return (
- !!this.highlightAttention &&
- !!this.change &&
- canHaveAttention(this.account)
- );
- }
-
- get hasUserAttention() {
- return hasAttention(this.account, this.change);
- }
-
- private getReviewerState() {
- if (
- this.change!.reviewers[ReviewerState.REVIEWER]?.some(
- (reviewer: AccountInfo) =>
- reviewer._account_id === this.account._account_id
- )
- ) {
- return ReviewerState.REVIEWER;
- }
- return ReviewerState.CC;
- }
-
- private computeReviewerOrCCText() {
- if (!this.change || !this.account) return '';
- return this.getReviewerState() === ReviewerState.REVIEWER
- ? 'Reviewer'
- : 'CC';
- }
-
- private computeChangeReviewerOrCCText() {
- if (!this.change || !this.account) return '';
- return this.getReviewerState() === ReviewerState.REVIEWER
- ? 'Move Reviewer to CC'
- : 'Move CC to Reviewer';
- }
-
- private handleChangeReviewerOrCCStatus() {
- assertIsDefined(this.change, 'change');
- // accountKey() throws an error if _account_id & email is not found, which
- // we want to check before showing reloading toast
- const _accountKey = accountKey(this.account);
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Reloading page...',
- });
- const reviewInput: Partial<ReviewInput> = {};
- reviewInput.reviewers = [
- {
- reviewer: _accountKey,
- state:
- this.getReviewerState() === ReviewerState.CC
- ? ReviewerState.REVIEWER
- : ReviewerState.CC,
- },
- ];
-
- this.restApiService
- .saveChangeReview(this.change._number, CURRENT, reviewInput)
- .then(response => {
- if (!response || !response.ok) {
- throw new Error(
- 'something went wrong when toggling' + this.getReviewerState()
- );
- }
- this.dispatchEventThroughTarget('reload', {clearPatchset: true});
- });
- }
-
- private handleRemoveReviewerOrCC() {
- if (!this.change || !(this.account?._account_id || this.account?.email))
- throw new Error('Missing change or account.');
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Reloading page...',
- });
- this.restApiService
- .removeChangeReviewer(
- this.change._number,
- (this.account?._account_id || this.account?.email)!
- )
- .then((response: Response | undefined) => {
- if (!response || !response.ok) {
- throw new Error('something went wrong when removing user');
- }
- this.dispatchEventThroughTarget('reload', {clearPatchset: true});
- return response;
- });
- }
-
- private computeShowActionAddToAttentionSet() {
- const involvedOrSelf =
- isInvolved(this.change, this.selfAccount) ||
- isSelf(this.account, this.selfAccount);
- return involvedOrSelf && this.isAttentionEnabled && !this.hasUserAttention;
- }
-
- private computeShowActionRemoveFromAttentionSet() {
- const involvedOrSelf =
- isInvolved(this.change, this.selfAccount) ||
- isSelf(this.account, this.selfAccount);
- return involvedOrSelf && this.isAttentionEnabled && this.hasUserAttention;
- }
-
- private handleClickAddToAttentionSet(e: MouseEvent) {
- if (!this.change || !this.account._account_id) return;
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Saving attention set update ...',
- dismissOnNavigation: true,
- });
-
- // We are deliberately updating the UI before making the API call. It is a
- // risk that we are taking to achieve a better UX for 99.9% of the cases.
- const reason = getAddedByReason(this.selfAccount, this.serverConfig);
-
- if (!this.change.attention_set) this.change.attention_set = {};
- this.change.attention_set[this.account._account_id] = {
- account: this.account,
- reason,
- reason_account: this.selfAccount,
- };
- this.dispatchEventThroughTarget('attention-set-updated');
-
- this.reporting.reportInteraction(
- 'attention-hovercard-add',
- this.reportingDetails()
- );
- this.restApiService
- .addToAttentionSet(this.change._number, this.account._account_id, reason)
- .then(() => {
- this.dispatchEventThroughTarget('hide-alert');
- });
- this.mouseHide(e);
- }
-
- private handleClickRemoveFromAttentionSet(e: MouseEvent) {
- if (!this.change || !this.account._account_id) return;
- this.dispatchEventThroughTarget(EventType.SHOW_ALERT, {
- message: 'Saving attention set update ...',
- dismissOnNavigation: true,
- });
-
- // We are deliberately updating the UI before making the API call. It is a
- // risk that we are taking to achieve a better UX for 99.9% of the cases.
-
- const reason = getRemovedByReason(this.selfAccount, this.serverConfig);
- if (this.change.attention_set)
- delete this.change.attention_set[this.account._account_id];
- this.dispatchEventThroughTarget('attention-set-updated');
-
- this.reporting.reportInteraction(
- 'attention-hovercard-remove',
- this.reportingDetails()
- );
- this.restApiService
- .removeFromAttentionSet(
- this.change._number,
- this.account._account_id,
- reason
- )
- .then(() => {
- this.dispatchEventThroughTarget('hide-alert');
- });
- this.mouseHide(e);
- }
-
- private reportingDetails() {
- const targetId = this.account._account_id;
- const ownerId =
- (this.change && this.change.owner && this.change.owner._account_id) || -1;
- const selfId = (this.selfAccount && this.selfAccount._account_id) || -1;
- const reviewers =
- this.change && this.change.reviewers && this.change.reviewers.REVIEWER
- ? [...this.change.reviewers.REVIEWER]
- : [];
- const reviewerIds = reviewers
- .map(r => r._account_id)
- .filter(rId => rId !== ownerId);
- return {
- actionByOwner: selfId === ownerId,
- actionByReviewer: selfId !== -1 && reviewerIds.includes(selfId),
- targetIsOwner: targetId === ownerId,
- targetIsReviewer: reviewerIds.includes(targetId),
- targetIsSelf: targetId === selfId,
- };
+ private redirectEventToTarget(e: CustomEvent<unknown>) {
+ this.dispatchEventThroughTarget(e.type, e.detail);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
index 89bc043..40e4c75 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
@@ -8,56 +8,35 @@
import {html} from 'lit';
import './gr-hovercard-account';
import {GrHovercardAccount} from './gr-hovercard-account';
-import {
- mockPromise,
- query,
- queryAndAssert,
- stubRestApi,
-} from '../../../test/test-utils';
-import {
- AccountDetailInfo,
- AccountId,
- EmailAddress,
- ReviewerState,
-} from '../../../api/rest-api';
+import {queryAndAssert} from '../../../test/test-utils';
import {
createAccountDetailWithId,
createChange,
- createDetailedLabelInfo,
} from '../../../test/test-data-generators';
import {GrButton} from '../gr-button/gr-button';
-import {EventType} from '../../../types/events';
+import {GrHovercardAccountContents} from './gr-hovercard-account-contents';
+import {userModelToken} from '../../../models/user/user-model';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-hovercard-account tests', () => {
let element: GrHovercardAccount;
-
- const ACCOUNT: AccountDetailInfo = {
- ...createAccountDetailWithId(31),
- email: 'kermit@gmail.com' as EmailAddress,
- username: 'kermit',
- name: 'Kermit The Frog',
- status: 'I am a frog',
- _account_id: 31415926535 as AccountId,
- };
+ let contents: GrHovercardAccountContents;
setup(async () => {
- const change = {
- ...createChange(),
- attention_set: {},
- reviewers: {},
- owner: {...ACCOUNT},
- };
+ const account = createAccountDetailWithId(31);
element = await fixture<GrHovercardAccount>(
html`<gr-hovercard-account
class="hovered"
- .account=${ACCOUNT}
- .change=${change}
+ .account=${account}
+ .change=${createChange()}
+ .highlightAttention=${true}
>
</gr-hovercard-account>`
);
await element.show({});
- element.userModel.setAccount({...ACCOUNT});
+ testResolver(userModelToken).setAccount({...account});
await element.updateComplete;
+ contents = queryAndAssert(element, 'gr-hovercard-account-contents');
});
teardown(async () => {
@@ -70,337 +49,29 @@
element,
/* HTML */ `
<div id="container" role="tooltip" tabindex="-1">
- <div class="top">
- <div class="avatar">
- <gr-avatar hidden="" imagesize="56"></gr-avatar>
- </div>
- <div class="account">
- <h3 class="heading-3 name">Kermit The Frog</h3>
- <div class="email">kermit@gmail.com</div>
- </div>
- </div>
- <gr-endpoint-decorator name="hovercard-status">
- <gr-endpoint-param name="account"></gr-endpoint-param>
- </gr-endpoint-decorator>
- <div class="status">
- <span class="title">About me:</span>
- <span class="value">I am a frog</span>
- </div>
- <div class="links">
- <gr-icon icon="link" class="linkIcon"></gr-icon>
- <a href="/q/owner:kermit%2540gmail.com">Changes</a>
- ·
- <a href="/dashboard/31415926535">Dashboard</a>
- </div>
+ <gr-hovercard-account-contents></gr-hovercard-account-contents>
</div>
`
);
});
- test('renders without change data', async () => {
- const elementWithoutChange = await fixture<GrHovercardAccount>(
- html`<gr-hovercard-account class="hovered" .account=${ACCOUNT}>
- </gr-hovercard-account>`
- );
- await elementWithoutChange.show({});
- assert.shadowDom.equal(
- elementWithoutChange,
- /* HTML */ `
- <div id="container" role="tooltip" tabindex="-1">
- <div class="top">
- <div class="avatar">
- <gr-avatar hidden="" imagesize="56"> </gr-avatar>
- </div>
- <div class="account">
- <h3 class="heading-3 name">Kermit The Frog</h3>
- <div class="email">kermit@gmail.com</div>
- </div>
- </div>
- <gr-endpoint-decorator name="hovercard-status">
- <gr-endpoint-param name="account"> </gr-endpoint-param>
- </gr-endpoint-decorator>
- <div class="status">
- <span class="title"> About me: </span>
- <span class="value"> I am a frog </span>
- </div>
- <div class="links">
- <gr-icon class="linkIcon" icon="link"> </gr-icon>
- <a href="/q/owner:kermit%2540gmail.com"> Changes </a>
- ·
- <a href="/dashboard/31415926535"> Dashboard </a>
- </div>
- </div>
- `
- );
- elementWithoutChange.mouseHide(new MouseEvent('click'));
- await elementWithoutChange.updateComplete;
+ test('hides when links are clicked', () => {
+ const changesLink = queryAndAssert<HTMLAnchorElement>(contents, 'a');
+ // Actually redirecting will break the test, replace URL with no-op
+ changesLink.href = 'javascript:';
+
+ assert.isTrue(element._isShowing);
+
+ changesLink.click();
+
+ assert.isFalse(element._isShowing);
});
- test('account name is shown', () => {
- const name = queryAndAssert<HTMLHeadingElement>(element, '.name');
- assert.equal(name.innerText, 'Kermit The Frog');
- });
+ test('hides when actions are performed', () => {
+ assert.isTrue(element._isShowing);
- test('computePronoun', async () => {
- element.account = createAccountDetailWithId(1);
- element.selfAccount = createAccountDetailWithId(1);
- await element.updateComplete;
- assert.equal(element.computePronoun(), 'Your');
- element.account = createAccountDetailWithId(2);
- await element.updateComplete;
- assert.equal(element.computePronoun(), 'Their');
- });
+ queryAndAssert<GrButton>(contents, 'gr-button.addToAttentionSet').click();
- test('account status is not shown if the property is not set', async () => {
- element.account = {...ACCOUNT, status: undefined};
- await element.updateComplete;
- assert.isUndefined(query(element, '.status'));
- });
-
- test('account status is displayed', () => {
- const status = queryAndAssert<HTMLSpanElement>(element, '.status .value');
- assert.equal(status.innerText, 'I am a frog');
- });
-
- test('voteable div is not shown if the property is not set', () => {
- assert.isUndefined(query(element, '.voteable'));
- });
-
- test('voteable div is displayed', async () => {
- element.change = {
- ...createChange(),
- labels: {
- Foo: {
- ...createDetailedLabelInfo(),
- all: [
- {
- _account_id: 7 as AccountId,
- permitted_voting_range: {max: 2, min: 0},
- },
- ],
- },
- Bar: {
- ...createDetailedLabelInfo(),
- all: [
- {
- ...createAccountDetailWithId(1),
- permitted_voting_range: {max: 1, min: 0},
- },
- {
- _account_id: 7 as AccountId,
- permitted_voting_range: {max: 1, min: 0},
- },
- ],
- },
- FooBar: {
- ...createDetailedLabelInfo(),
- all: [{_account_id: 7 as AccountId, value: 0}],
- },
- },
- permitted_labels: {
- Foo: ['-1', ' 0', '+1', '+2'],
- FooBar: ['-1', ' 0'],
- },
- };
- element.account = createAccountDetailWithId(1);
-
- await element.updateComplete;
- const voteableEl = queryAndAssert<HTMLSpanElement>(
- element,
- '.voteable .value'
- );
- assert.equal(voteableEl.innerText, 'Bar: +1');
- });
-
- test('remove reviewer', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [ACCOUNT],
- },
- };
- await element.updateComplete;
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
- const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
- assert.isOk(button);
- assert.equal(button.innerText, 'Remove Reviewer');
- button.click();
- await element.updateComplete;
- assert.isTrue(reloadListener.called);
- });
-
- test('move reviewer to cc', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [ACCOUNT],
- },
- };
- await element.updateComplete;
- const saveReviewStub = stubRestApi('saveChangeReview').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
-
- const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
-
- assert.isOk(button);
- assert.equal(button.innerText, 'Move Reviewer to CC');
- button.click();
- await element.updateComplete;
- assert.isTrue(saveReviewStub.called);
- assert.isTrue(reloadListener.called);
- });
-
- test('move reviewer to cc', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [],
- },
- };
- await element.updateComplete;
- const saveReviewStub = stubRestApi('saveChangeReview').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
-
- const button = queryAndAssert<GrButton>(element, '.changeReviewerOrCC');
- assert.isOk(button);
- assert.equal(button.innerText, 'Move CC to Reviewer');
-
- button.click();
- await element.updateComplete;
- assert.isTrue(saveReviewStub.called);
- assert.isTrue(reloadListener.called);
- });
-
- test('remove cc', async () => {
- element.change = {
- ...createChange(),
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [],
- },
- };
- await element.updateComplete;
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- const reloadListener = sinon.spy();
- element._target?.addEventListener('reload', reloadListener);
-
- const button = queryAndAssert<GrButton>(element, '.removeReviewerOrCC');
-
- assert.equal(button.innerText, 'Remove CC');
- assert.isOk(button);
- button.click();
- await element.updateComplete;
- assert.isTrue(reloadListener.called);
- });
-
- test('add to attention set', async () => {
- const apiPromise = mockPromise<Response>();
- const apiSpy = stubRestApi('addToAttentionSet').returns(apiPromise);
- element.highlightAttention = true;
- element._target = document.createElement('div');
- await element.updateComplete;
- const showAlertListener = sinon.spy();
- const hideAlertListener = sinon.spy();
- const updatedListener = sinon.spy();
- element._target.addEventListener(EventType.SHOW_ALERT, showAlertListener);
- element._target.addEventListener('hide-alert', hideAlertListener);
- element._target.addEventListener('attention-set-updated', updatedListener);
-
- const button = queryAndAssert<GrButton>(element, '.addToAttentionSet');
- assert.isOk(button);
- assert.isTrue(element._isShowing, 'hovercard is showing');
- button.click();
-
- assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 1);
- const attention_set_info = Object.values(
- element.change?.attention_set ?? {}
- )[0];
- assert.equal(
- attention_set_info.reason,
- `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
- ' using the hovercard menu'
- );
- assert.equal(
- attention_set_info.reason_account?._account_id,
- ACCOUNT._account_id
- );
- assert.isTrue(showAlertListener.called, 'showAlertListener was called');
- assert.isTrue(updatedListener.called, 'updatedListener was called');
- assert.isFalse(element._isShowing, 'hovercard is hidden');
-
- apiPromise.resolve({...new Response(), ok: true});
- await element.updateComplete;
- assert.isTrue(apiSpy.calledOnce);
- assert.equal(
- apiSpy.lastCall.args[2],
- `Added by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
- ' using the hovercard menu'
- );
- assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
- });
-
- test('remove from attention set', async () => {
- const apiPromise = mockPromise<Response>();
- const apiSpy = stubRestApi('removeFromAttentionSet').returns(apiPromise);
- element.highlightAttention = true;
- element.change = {
- ...createChange(),
- attention_set: {
- '31415926535': {account: ACCOUNT, reason: 'a good reason'},
- },
- reviewers: {},
- owner: {...ACCOUNT},
- };
- element._target = document.createElement('div');
- await element.updateComplete;
- const showAlertListener = sinon.spy();
- const hideAlertListener = sinon.spy();
- const updatedListener = sinon.spy();
- element._target.addEventListener(EventType.SHOW_ALERT, showAlertListener);
- element._target.addEventListener('hide-alert', hideAlertListener);
- element._target.addEventListener('attention-set-updated', updatedListener);
-
- const button = queryAndAssert<GrButton>(element, '.removeFromAttentionSet');
- assert.isOk(button);
- assert.isTrue(element._isShowing, 'hovercard is showing');
- button.click();
-
- assert.isDefined(element.change?.attention_set);
- assert.equal(Object.keys(element.change?.attention_set ?? {}).length, 0);
- assert.isTrue(showAlertListener.called, 'showAlertListener was called');
- assert.isTrue(updatedListener.called, 'updatedListener was called');
- assert.isFalse(element._isShowing, 'hovercard is hidden');
-
- apiPromise.resolve({...new Response(), ok: true});
- await element.updateComplete;
-
- assert.isTrue(apiSpy.calledOnce);
- assert.equal(
- apiSpy.lastCall.args[2],
- `Removed by <GERRIT_ACCOUNT_${ACCOUNT._account_id}>` +
- ' using the hovercard menu'
- );
- assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+ assert.isFalse(element._isShowing);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 353fc2a..6142aa2 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -20,7 +20,7 @@
ShowRevisionActionsDetail,
} from './gr-js-api-types';
import {EventType, TargetElement} from '../../../api/plugin';
-import {DiffLayer, HighlightJS, ParsedChangeInfo} from '../../../types/types';
+import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {MenuLink} from '../../../api/admin';
import {Finalizable} from '../../../services/registry';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
@@ -39,24 +39,15 @@
.awaitPluginsLoaded()
.then(() => {
switch (type) {
- case EventType.HISTORY:
- this._handleHistory(detail);
- break;
case EventType.SHOW_CHANGE:
this._handleShowChange(detail);
break;
- case EventType.COMMENT:
- this._handleComment(detail);
- break;
case EventType.LABEL_CHANGE:
this._handleLabelChange(detail);
break;
case EventType.SHOW_REVISION_ACTIONS:
this._handleShowRevisionActions(detail);
break;
- case EventType.HIGHLIGHTJS_LOADED:
- this._handleHighlightjsLoaded(detail);
- break;
default:
console.warn(
'handleEvent called with unsupported event type:',
@@ -107,21 +98,6 @@
}
}
- // TODO(TS): The HISTORY event and its handler seem unused.
- _handleHistory(detail: {path: string}) {
- for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
- try {
- cb(detail.path);
- } catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('handleHistory callback error'),
- err
- );
- }
- }
- }
-
_handleShowChange(detail: ShowChangeDetail) {
// Note (issue 8221) Shallow clone the change object and add a mergeable
// getter with deprecation warning. This makes the change detail appear as
@@ -198,21 +174,6 @@
}
}
- // TODO(TS): The COMMENT event and its handler seem unused.
- _handleComment(detail: {node: Node}) {
- for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
- try {
- cb(detail.node);
- } catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('comment callback error'),
- err
- );
- }
- }
- }
-
_handleLabelChange(detail: {change: ChangeInfo}) {
for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
try {
@@ -227,20 +188,6 @@
}
}
- _handleHighlightjsLoaded(detail: {hljs: HighlightJS}) {
- for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
- try {
- cb(detail.hljs);
- } catch (err: unknown) {
- this.reporting.error(
- 'GrJsApiInterface',
- new Error('HighlightjsLoaded callback error'),
- err
- );
- }
- }
- }
-
modifyRevertMsg(change: ChangeInfo, revertMsg: string, origMsg: string) {
for (const cb of this._getEventCallbacks(EventType.REVERT)) {
try {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 4fc403d..7c09bad 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -69,17 +69,6 @@
});
});
- test('history event', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- plugin.on(EventType.HISTORY, throwErrFn);
- plugin.on(EventType.HISTORY, resolve);
- element.handleEvent(EventType.HISTORY, {path: '/path/to/awesomesauce'});
- const path = await promise;
- assert.equal(path, '/path/to/awesomesauce');
- assert.isTrue(errorStub.calledOnce);
- });
-
test('showchange event', async () => {
let resolve;
const promise = new Promise(r => resolve = r);
@@ -141,19 +130,6 @@
assert.isTrue(spy.called);
});
- test('comment event', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- const testCommentNode = {foo: 'bar'};
- plugin.on(EventType.COMMENT, throwErrFn);
- plugin.on(EventType.COMMENT, resolve);
- element.handleEvent(EventType.COMMENT, {node: testCommentNode});
-
- const commentNode = await promise;
- assert.deepEqual(commentNode, testCommentNode);
- assert.isTrue(errorStub.calledOnce);
- });
-
test('revert event', () => {
function appendToRevertMsg(c, revertMsg, originalMsg) {
return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
@@ -243,19 +219,6 @@
assert.isTrue(errorStub.calledTwice);
});
- test('highlightjs-loaded event', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- const testHljs = {_number: 42};
- plugin.on(EventType.HIGHLIGHTJS_LOADED, throwErrFn);
- plugin.on(EventType.HIGHLIGHTJS_LOADED, resolve);
- element.handleEvent(EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs});
-
- const hljs = await promise;
- assert.deepEqual(hljs, testHljs);
- assert.isTrue(errorStub.calledOnce);
- });
-
test('getLoggedIn', () => {
// fake fetch for authCheck
sinon.stub(window, 'fetch').callsFake(() => Promise.resolve({status: 204}));
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 9c54349..1fbe5b2 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -102,7 +102,7 @@
get(key: '/accounts/self/emails'): EmailInfo[] | null;
- get(key: '/accounts/self/detail'): AccountDetailInfo[] | null;
+ get(key: '/accounts/self/detail'): AccountDetailInfo | null;
get(key: string): ParsedJSON | null;
@@ -112,7 +112,7 @@
set(key: '/accounts/self/emails', value: EmailInfo[]): void;
- set(key: '/accounts/self/detail', value: AccountDetailInfo[]): void;
+ set(key: '/accounts/self/detail', value: AccountDetailInfo): void;
set(key: string, value: ParsedJSON | null): void;
@@ -275,16 +275,14 @@
* by this method, it should be called immediately after the request
* finishes.
*
+ * Private, but used in tests.
+ *
* @param startTime the time that the request was started.
* @param status the HTTP status of the response. The status value
* is used here rather than the response object so there is no way this
* method can read the body stream.
*/
- private _logCall(
- req: FetchRequest,
- startTime: number,
- status: number | null
- ) {
+ _logCall(req: FetchRequest, startTime: number, status: number | null) {
const method =
req.fetchOptions && req.fetchOptions.method
? req.fetchOptions.method
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index d285dd0..b38d71b 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -302,6 +302,10 @@
return this.textarea!.textarea;
}
+ override focus() {
+ this.textarea?.textarea.focus();
+ }
+
putCursorAtEnd() {
const textarea = this.getNativeTextarea();
// Put the cursor at the end always.
@@ -341,7 +345,7 @@
e.preventDefault();
e.stopPropagation();
this.getVisibleDropdown().cursorUp();
- this.textarea!.textarea.focus();
+ this.focus();
}
private handleDownKey(e: KeyboardEvent) {
@@ -351,7 +355,7 @@
e.preventDefault();
e.stopPropagation();
this.getVisibleDropdown().cursorDown();
- this.textarea!.textarea.focus();
+ this.focus();
}
private handleTabKey(e: KeyboardEvent) {
@@ -566,7 +570,7 @@
async handleTextChanged() {
await this.computeSuggestions();
this.openOrResetDropdown();
- this.textarea!.textarea.focus();
+ this.focus();
}
private openEmojiDropdown() {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 5caffe6..5058ce8 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -11,12 +11,12 @@
import {customElement, property, state} from 'lit/decorators.js';
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import {FixIronA11yAnnouncer} from '../../../types/types';
-import {getAppContext} from '../../../services/app-context';
import {fireIronAnnounce} from '../../../utils/event-util';
import {browserModelToken} from '../../../models/browser/browser-model';
import {resolve} from '../../../models/dependency';
import {css, html, LitElement} from 'lit';
import {sharedStyles} from '../../../styles/shared-styles';
+import {userModelToken} from '../../../models/user/user-model';
@customElement('gr-diff-mode-selector')
export class GrDiffModeSelector extends LitElement {
@@ -34,7 +34,7 @@
private readonly getBrowserModel = resolve(this, browserModelToken);
- private readonly userModel = getAppContext().userModel;
+ private readonly getUserModel = resolve(this, userModelToken);
private subscriptions: Subscription[] = [];
@@ -118,7 +118,7 @@
*/
private setMode(newMode: DiffViewMode) {
if (this.saveOnChange && this.mode && this.mode !== newMode) {
- this.userModel.updatePreferences({diff_view: newMode});
+ this.getUserModel().updatePreferences({diff_view: newMode});
}
this.mode = newMode;
let announcement;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index 34af01e..d646988 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -7,21 +7,17 @@
import './gr-diff-mode-selector';
import {GrDiffModeSelector} from './gr-diff-mode-selector';
import {DiffViewMode} from '../../../constants/constants';
-import {
- queryAndAssert,
- stubUsers,
- waitUntilObserved,
-} from '../../../test/test-utils';
+import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
import {fixture, html, assert} from '@open-wc/testing';
import {wrapInProvider} from '../../../models/di-provider-element';
import {
BrowserModel,
browserModelToken,
} from '../../../models/browser/browser-model';
-import {getAppContext} from '../../../services/app-context';
-import {UserModel} from '../../../models/user/user-model';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
import {createPreferences} from '../../../test/test-data-generators';
import {GrButton} from '../../../elements/shared/gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-diff-mode-selector tests', () => {
let element: GrDiffModeSelector;
@@ -29,7 +25,7 @@
let userModel: UserModel;
setup(async () => {
- userModel = getAppContext().userModel;
+ userModel = testResolver(userModelToken);
browserModel = new BrowserModel(userModel);
element = (
await fixture(
@@ -129,7 +125,7 @@
test('set mode', async () => {
browserModel.setScreenWidth(0);
- const saveStub = stubUsers('updatePreferences');
+ const saveStub = sinon.stub(userModel, 'updatePreferences');
// Setting the mode initially does not save prefs.
element.saveOnChange = true;
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
index a9f88bd..e970664 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -8,9 +8,11 @@
import {DiffFileMetaInfo, DiffInfo} from '../../../types/diff';
import {DiffLayer, DiffLayerListener} from '../../../types/types';
import {Side} from '../../../constants/constants';
-import {getAppContext} from '../../../services/app-context';
import {SyntaxLayerLine} from '../../../types/syntax-worker-api';
import {CancelablePromise, makeCancelable} from '../../../scripts/util';
+import {HighlightService} from '../../../services/highlight/highlight-service';
+import {Provider} from '../../../models/dependency';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
const LANGUAGE_MAP = new Map<string, string>([
['application/dart', 'dart'],
@@ -162,9 +164,10 @@
private listeners: DiffLayerListener[] = [];
- private readonly highlightService = getAppContext().highlightService;
-
- private readonly reportingService = getAppContext().reportingService;
+ constructor(
+ private readonly getHighlightService: Provider<HighlightService>,
+ private readonly getReportingService: Provider<ReportingService>
+ ) {}
setEnabled(enabled: boolean) {
this.enabled = enabled;
@@ -276,7 +279,7 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (!err.isCanceled)
- this.reportingService.error('Diff Syntax Layer', err as Error);
+ this.getReportingService().error('Diff Syntax Layer', err as Error);
// One source of "error" can promise cancelation.
this.leftRanges = [];
this.rightRanges = [];
@@ -287,7 +290,7 @@
language?: string,
code?: string
): CancelablePromise<SyntaxLayerLine[]> {
- const hlPromise = this.highlightService.highlight(language, code);
+ const hlPromise = this.getHighlightService().highlight(language, code);
return makeCancelable(hlPromise);
}
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
index 5c9a6cc..c6c46f9 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
@@ -5,8 +5,14 @@
*/
import {assert} from '@open-wc/testing';
import {DiffInfo, GrDiffLineType, Side} from '../../../api/diff';
+import {getAppContext} from '../../../services/app-context';
+import {
+ HighlightService,
+ highlightServiceToken,
+} from '../../../services/highlight/highlight-service';
import '../../../test/common-test-setup';
-import {mockPromise, stubHighlightService} from '../../../test/test-utils';
+import {testResolver} from '../../../test/common-test-setup';
+import {mockPromise} from '../../../test/test-utils';
import {SyntaxLayerLine} from '../../../types/syntax-worker-api';
import {GrDiffLine} from '../gr-diff/gr-diff-line';
import {GrSyntaxLayerWorker} from './gr-syntax-layer-worker';
@@ -62,6 +68,7 @@
suite('gr-syntax-layer-worker tests', () => {
let layer: GrSyntaxLayerWorker;
let listener: sinon.SinonStub;
+ let highlightService: HighlightService;
const annotate = (side: Side, lineNumber: number, text: string) => {
const el = document.createElement('div');
@@ -76,7 +83,11 @@
};
setup(() => {
- layer = new GrSyntaxLayerWorker();
+ highlightService = testResolver(highlightServiceToken);
+ layer = new GrSyntaxLayerWorker(
+ () => highlightService,
+ () => getAppContext().reportingService
+ );
});
test('cancel processing', async () => {
@@ -84,7 +95,7 @@
const mockPromise2 = mockPromise<SyntaxLayerLine[]>();
const mockPromise3 = mockPromise<SyntaxLayerLine[]>();
const mockPromise4 = mockPromise<SyntaxLayerLine[]>();
- const stub = stubHighlightService('highlight');
+ const stub = sinon.stub(highlightService, 'highlight');
stub.onCall(0).returns(mockPromise1);
stub.onCall(1).returns(mockPromise2);
stub.onCall(2).returns(mockPromise3);
@@ -116,7 +127,7 @@
setup(() => {
listener = sinon.stub();
layer.addListener(listener);
- stubHighlightService('highlight').callsFake((lang?: string) => {
+ sinon.stub(highlightService, 'highlight').callsFake((lang?: string) => {
if (lang === 'lang-left') return Promise.resolve(leftRanges);
if (lang === 'lang-right') return Promise.resolve(rightRanges);
return Promise.resolve([]);
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index f865d6d..aedf493 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -66,27 +66,9 @@
jsApiService: (_ctx: Partial<AppContext>) => {
throw new Error('jsApiService is not implemented');
},
- storageService: (_ctx: Partial<AppContext>) => {
- throw new Error('storageService is not implemented');
- },
- userModel: (_ctx: Partial<AppContext>) => {
- throw new Error('userModel is not implemented');
- },
- accountsModel: (_ctx: Partial<AppContext>) => {
- throw new Error('accountsModel is not implemented');
- },
- routerModel: (_ctx: Partial<AppContext>) => {
- throw new Error('routerModel is not implemented');
- },
- shortcutsService: (_ctx: Partial<AppContext>) => {
- throw new Error('shortcutsService is not implemented');
- },
pluginsModel: (_ctx: Partial<AppContext>) => {
throw new Error('pluginsModel is not implemented');
},
- highlightService: (_ctx: Partial<AppContext>) => {
- throw new Error('highlightService is not implemented');
- },
};
return create<AppContext>(appRegistry);
}
diff --git a/polygerrit-ui/app/models/accounts-model/accounts-model.ts b/polygerrit-ui/app/models/accounts-model/accounts-model.ts
index 3f35127..2bf6068 100644
--- a/polygerrit-ui/app/models/accounts-model/accounts-model.ts
+++ b/polygerrit-ui/app/models/accounts-model/accounts-model.ts
@@ -6,7 +6,6 @@
import {AccountDetailInfo, AccountInfo} from '../../api/rest-api';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
-import {Finalizable} from '../../services/registry';
import {UserId} from '../../types/common';
import {getUserId, isDetailedAccount} from '../../utils/account-util';
import {define} from '../dependency';
@@ -18,7 +17,7 @@
export const accountsModelToken = define<AccountsModel>('accounts-model');
-export class AccountsModel extends Model<AccountsState> implements Finalizable {
+export class AccountsModel extends Model<AccountsState> {
constructor(readonly restApiService: RestApiService) {
super({
accounts: {},
diff --git a/polygerrit-ui/app/models/browser/browser-model.ts b/polygerrit-ui/app/models/browser/browser-model.ts
index 1592cd8..50b6325 100644
--- a/polygerrit-ui/app/models/browser/browser-model.ts
+++ b/polygerrit-ui/app/models/browser/browser-model.ts
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable, combineLatest} from 'rxjs';
-import {Finalizable} from '../../services/registry';
import {define} from '../dependency';
import {DiffViewMode} from '../../api/diff';
import {UserModel} from '../user/user-model';
@@ -26,7 +25,7 @@
export const browserModelToken = define<BrowserModel>('browser-model');
-export class BrowserModel extends Model<BrowserState> implements Finalizable {
+export class BrowserModel extends Model<BrowserState> {
private readonly isScreenTooSmall$ = select(
this.state$,
state =>
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 f706712..b13a16f 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -14,7 +14,6 @@
Hashtag,
} 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';
@@ -50,10 +49,7 @@
allChanges: new Map(),
};
-export class BulkActionsModel
- extends Model<BulkActionsState>
- implements Finalizable
-{
+export class BulkActionsModel extends Model<BulkActionsState> {
constructor(private readonly restApiService: RestApiService) {
super(initialState);
}
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 12d09b3..8282a3f 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -32,7 +32,6 @@
import {ChangeInfo} from '../../types/common';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
-import {Finalizable} from '../../services/registry';
import {select} from '../../utils/observable-util';
import {assertIsDefined} from '../../utils/common-util';
import {Model} from '../model';
@@ -148,7 +147,7 @@
export const changeModelToken = define<ChangeModel>('change-model');
-export class ChangeModel extends Model<ChangeState> implements Finalizable {
+export class ChangeModel extends Model<ChangeState> {
private change?: ParsedChangeInfo;
private patchNum?: PatchSetNum;
@@ -258,9 +257,9 @@
);
constructor(
- readonly routerModel: RouterModel,
- readonly restApiService: RestApiService,
- readonly userModel: UserModel
+ private readonly routerModel: RouterModel,
+ private readonly restApiService: RestApiService,
+ private readonly userModel: UserModel
) {
super(initialState);
this.subscriptions = [
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index 4b51d5b..a2fc7c9 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -28,10 +28,12 @@
} from '../../types/common';
import {ParsedChangeInfo} from '../../types/types';
import {getAppContext} from '../../services/app-context';
-import {GerritView} from '../../services/router/router-model';
+import {GerritView, routerModelToken} from '../../services/router/router-model';
import {ChangeState, LoadingStatus, updateChangeWithEdit} from './change-model';
import {ChangeModel} from './change-model';
import {assert} from '@open-wc/testing';
+import {testResolver} from '../../test/common-test-setup';
+import {userModelToken} from '../user/user-model';
suite('updateChangeWithEdit() tests', () => {
test('undefined change', async () => {
@@ -81,9 +83,9 @@
setup(() => {
changeModel = new ChangeModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
getAppContext().restApiService,
- getAppContext().userModel
+ testResolver(userModelToken)
);
knownChange = {
...createChange(),
@@ -119,7 +121,7 @@
assert.equal(stub.callCount, 0);
assert.isUndefined(state?.change);
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -138,7 +140,7 @@
const promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -162,7 +164,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -176,7 +178,7 @@
_number: 123 as NumericChangeId,
};
promise = mockPromise<ParsedChangeInfo | undefined>();
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: otherChange._number,
});
@@ -195,7 +197,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -206,7 +208,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(undefined);
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: undefined,
});
@@ -218,7 +220,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(knownChange);
- changeModel.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: knownChange._number,
});
@@ -297,7 +299,9 @@
assert.equal(spy.lastCall.firstArg, PARENT);
// test update
- changeModel.routerModel.updateState({basePatchNum: 1 as PatchSetNumber});
+ testResolver(routerModelToken).updateState({
+ basePatchNum: 1 as PatchSetNumber,
+ });
assert.equal(spy.callCount, 2);
assert.equal(spy.lastCall.firstArg, 1 as PatchSetNumber);
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 6922f6d..07e64a2 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -15,7 +15,6 @@
import {combineLatest, of, from} from 'rxjs';
import {switchMap, map} from 'rxjs/operators';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
-import {Finalizable} from '../../services/registry';
import {select} from '../../utils/observable-util';
import {FileInfoStatus, SpecialFilePath} from '../../constants/constants';
import {specialFilePathCompare} from '../../utils/path-list-util';
@@ -113,7 +112,7 @@
export const filesModelToken = define<FilesModel>('files-model');
-export class FilesModel extends Model<FilesState> implements Finalizable {
+export class FilesModel extends Model<FilesState> {
public readonly files$ = select(this.state$, state => state.files);
public readonly filesWithUnmodified$ = select(
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index 6b35056..e201b88 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -12,7 +12,6 @@
} from './checks-util';
import {assertIsDefined} from '../../utils/common-util';
import {select} from '../../utils/observable-util';
-import {Finalizable} from '../../services/registry';
import {
BehaviorSubject,
combineLatest,
@@ -53,7 +52,6 @@
import {ReportingService} from '../../services/gr-reporting/gr-reporting';
import {Execution, Interaction, Timing} from '../../constants/reporting';
import {fireAlert, fireEvent} from '../../utils/event-util';
-import {RouterModel} from '../../services/router/router-model';
import {Model} from '../model';
import {define} from '../dependency';
import {
@@ -182,7 +180,7 @@
[name: string]: string;
}
-export class ChecksModel extends Model<ChecksState> implements Finalizable {
+export class ChecksModel extends Model<ChecksState> {
private readonly providers: {[name: string]: ChecksProvider} = {};
private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
@@ -374,11 +372,10 @@
);
constructor(
- readonly routerModel: RouterModel,
- readonly changeViewModel: ChangeViewModel,
- readonly changeModel: ChangeModel,
- readonly reporting: ReportingService,
- readonly pluginsModel: PluginsModel
+ private readonly changeViewModel: ChangeViewModel,
+ private readonly changeModel: ChangeModel,
+ private readonly reporting: ReportingService,
+ private readonly pluginsModel: PluginsModel
) {
super({
pluginStateLatest: {},
@@ -753,15 +750,14 @@
patchset === ChecksPatchset.LATEST
? this.changeModel.latestPatchNum$
: this.checksSelectedPatchsetNumber$,
- this.reloadSubjects[pluginName].pipe(
- throttleTime(1000, undefined, {trailing: true, leading: true})
- ),
+ this.reloadSubjects[pluginName],
pollIntervalMs === 0 ? from([0]) : timer(0, pollIntervalMs),
this.documentVisibilityChange$,
])
.pipe(
takeWhile(_ => !!this.providers[pluginName]),
filter(_ => document.visibilityState !== 'hidden'),
+ throttleTime(500, undefined, {leading: true, trailing: true}),
switchMap(([change, patchNum]): Observable<FetchResponse> => {
if (!change || !patchNum) return of(this.empty());
if (typeof patchNum !== 'number') return of(this.empty());
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index 3489c5a..dc2ad4b 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -69,7 +69,6 @@
setup(() => {
model = new ChecksModel(
- getAppContext().routerModel,
testResolver(changeViewModelToken),
testResolver(changeModelToken),
getAppContext().reportingService,
@@ -84,7 +83,7 @@
test('register and fetch', async () => {
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -96,7 +95,7 @@
await waitUntil(() => change === undefined);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
await waitUntilCalled(fetchSpy, 'fetch');
@@ -108,10 +107,10 @@
assert.equal(model.changeNum, testChange._number);
});
- test('reload throttle', async () => {
+ test('fetch throttle', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -123,18 +122,33 @@
await waitUntil(() => change === undefined);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
- clock.tick(1);
+
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+
+ // Does not emit at 'leading' of throttle interval,
+ // because fetch() is not called when change is undefined.
+ assert.equal(fetchSpy.callCount, 0);
+
+ // 600 ms is greater than the 500 ms throttle time.
+ clock.tick(600);
+ // emits at 'trailing' of throttle interval
assert.equal(fetchSpy.callCount, 1);
- // The second reload call will be processed, but only after a 1s throttle.
model.reload('test-plugin');
- clock.tick(100);
- assert.equal(fetchSpy.callCount, 1);
- // 2000 ms is greater than the 1000 ms throttle time.
- clock.tick(2000);
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+ model.reload('test-plugin');
+ // emits at 'leading' of throttle interval
assert.equal(fetchSpy.callCount, 2);
+
+ // 600 ms is greater than the 500 ms throttle time.
+ clock.tick(600);
+ // emits at 'trailing' of throttle interval
+ assert.equal(fetchSpy.callCount, 3);
});
test('triggerAction', async () => {
@@ -268,7 +282,7 @@
test('polls for changes', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -280,10 +294,10 @@
await waitUntil(() => change === undefined);
clock.tick(1);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
+ clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
- clock.tick(1);
const pollCount = fetchSpy.callCount;
// polling should continue while we wait
@@ -295,7 +309,7 @@
test('does not poll when config specifies 0 seconds', async () => {
const clock = sinon.useFakeTimers();
let change: ParsedChangeInfo | undefined = undefined;
- model.changeModel.change$.subscribe(c => (change = c));
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
const provider = createProvider();
const fetchSpy = sinon.spy(provider, 'fetch');
@@ -307,8 +321,9 @@
await waitUntil(() => change === undefined);
clock.tick(1);
const testChange = createParsedChange();
- model.changeModel.updateStateChange(testChange);
+ testResolver(changeModelToken).updateStateChange(testChange);
await waitUntil(() => change === testChange);
+ clock.tick(600); // need to wait for 500ms throttle
await waitUntilCalled(fetchSpy, 'fetch');
clock.tick(1);
const pollCount = fetchSpy.callCount;
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index da49ddb..a7b43ca 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -28,7 +28,6 @@
import {deepEqual} from '../../utils/deep-util';
import {select} from '../../utils/observable-util';
import {RouterModel} from '../../services/router/router-model';
-import {Finalizable} from '../../services/registry';
import {define} from '../dependency';
import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
import {fire, fireAlert, fireEvent} from '../../utils/event-util';
@@ -225,7 +224,7 @@
}
export const commentsModelToken = define<CommentsModel>('comments-model');
-export class CommentsModel extends Model<CommentState> implements Finalizable {
+export class CommentsModel extends Model<CommentState> {
public readonly commentsLoading$ = select(
this.state$,
commentState =>
@@ -385,11 +384,11 @@
private discardedDrafts: DraftInfo[] = [];
constructor(
- readonly routerModel: RouterModel,
- readonly changeModel: ChangeModel,
- readonly accountsModel: AccountsModel,
- readonly restApiService: RestApiService,
- readonly reporting: ReportingService
+ private readonly routerModel: RouterModel,
+ private readonly changeModel: ChangeModel,
+ private readonly accountsModel: AccountsModel,
+ private readonly restApiService: RestApiService,
+ private readonly reporting: ReportingService
) {
super(initialState);
this.subscriptions.push(
diff --git a/polygerrit-ui/app/models/comments/comments-model_test.ts b/polygerrit-ui/app/models/comments/comments-model_test.ts
index 32ea1bc..4db5d57 100644
--- a/polygerrit-ui/app/models/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/models/comments/comments-model_test.ts
@@ -23,11 +23,12 @@
} from '../../test/test-data-generators';
import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
import {getAppContext} from '../../services/app-context';
-import {GerritView} from '../../services/router/router-model';
+import {GerritView, routerModelToken} from '../../services/router/router-model';
import {PathToCommentsInfoMap} from '../../types/common';
import {changeModelToken} from '../change/change-model';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
+import {accountsModelToken} from '../accounts-model/accounts-model';
suite('comments model tests', () => {
test('updateStateDeleteDraft', () => {
@@ -69,9 +70,9 @@
test('loads comments', async () => {
const model = new CommentsModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
testResolver(changeModelToken),
- getAppContext().accountsModel,
+ testResolver(accountsModelToken),
getAppContext().restApiService,
getAppContext().reportingService
);
@@ -97,11 +98,11 @@
model.portedComments$.subscribe(c => (portedComments = c ?? {}))
);
- model.routerModel.setState({
+ testResolver(routerModelToken).setState({
view: GerritView.CHANGE,
changeNum: TEST_NUMERIC_CHANGE_ID,
});
- model.changeModel.updateStateChange(createParsedChange());
+ testResolver(changeModelToken).updateStateChange(createParsedChange());
await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
await waitUntilCalled(diffRobotCommentsSpy, 'diffRobotCommentsSpy');
@@ -130,9 +131,9 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
testResolver(changeModelToken),
- getAppContext().accountsModel,
+ testResolver(accountsModelToken),
getAppContext().restApiService,
getAppContext().reportingService
);
@@ -158,9 +159,9 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- getAppContext().routerModel,
+ testResolver(routerModelToken),
testResolver(changeModelToken),
- getAppContext().accountsModel,
+ testResolver(accountsModelToken),
getAppContext().restApiService,
getAppContext().reportingService
);
diff --git a/polygerrit-ui/app/models/config/config-model.ts b/polygerrit-ui/app/models/config/config-model.ts
index 6e374d1..4c9bb35c 100644
--- a/polygerrit-ui/app/models/config/config-model.ts
+++ b/polygerrit-ui/app/models/config/config-model.ts
@@ -6,7 +6,6 @@
import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
import {from, of} from 'rxjs';
import {switchMap} from 'rxjs/operators';
-import {Finalizable} from '../../services/registry';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {ChangeModel} from '../change/change-model';
import {select} from '../../utils/observable-util';
@@ -20,7 +19,7 @@
}
export const configModelToken = define<ConfigModel>('config-model');
-export class ConfigModel extends Model<ConfigState> implements Finalizable {
+export class ConfigModel extends Model<ConfigState> {
public repoConfig$ = select(
this.state$,
configState => configState.repoConfig
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 7826c45..75de68a 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -3,7 +3,6 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {Finalizable} from '../../services/registry';
import {Observable, Subject} from 'rxjs';
import {
CheckResult,
@@ -37,7 +36,7 @@
export const pluginsModelToken = define<PluginsModel>('plugins-model');
-export class PluginsModel extends Model<PluginsState> implements Finalizable {
+export class PluginsModel extends Model<PluginsState> {
/** Private version of the event bus below. */
private checksAnnounceSubject$ = new Subject<ChecksPlugin>();
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index 428ddbb..97f90fa 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -23,8 +23,8 @@
} from '../../constants/constants';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {DiffPreferencesInfo} from '../../types/diff';
-import {Finalizable} from '../../services/registry';
import {select} from '../../utils/observable-util';
+import {define} from '../dependency';
import {Model} from '../model';
import {isDefined} from '../../types/types';
@@ -56,7 +56,9 @@
capabilities?: AccountCapabilityInfo;
}
-export class UserModel extends Model<UserState> implements Finalizable {
+export const userModelToken = define<UserModel>('user-model');
+
+export class UserModel extends Model<UserState> {
/**
* Note that the initially emitted `undefined` value can mean "not loaded
* the account into object yet" or "user is not logged in". Consider using
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index e662d5f..2b54eb5 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -15,13 +15,13 @@
import {FilesModel, filesModelToken} from '../models/change/files-model';
import {ChecksModel, checksModelToken} from '../models/checks/checks-model';
import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
-import {GrStorageService} from './storage/gr-storage_impl';
-import {UserModel} from '../models/user/user-model';
+import {GrStorageService, storageServiceToken} from './storage/gr-storage_impl';
+import {UserModel, userModelToken} from '../models/user/user-model';
import {
CommentsModel,
commentsModelToken,
} from '../models/comments/comments-model';
-import {RouterModel} from './router/router-model';
+import {RouterModel, routerModelToken} from './router/router-model';
import {
ShortcutsService,
shortcutsServiceToken,
@@ -30,8 +30,14 @@
import {ConfigModel, configModelToken} from '../models/config/config-model';
import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
import {PluginsModel} from '../models/plugins/plugins-model';
-import {HighlightService} from './highlight/highlight-service';
-import {AccountsModel} from '../models/accounts-model/accounts-model';
+import {
+ HighlightService,
+ highlightServiceToken,
+} from './highlight/highlight-service';
+import {
+ AccountsModel,
+ accountsModelToken,
+} from '../models/accounts-model/accounts-model';
import {
DashboardViewModel,
dashboardViewModelToken,
@@ -57,17 +63,13 @@
import {PluginViewModel, pluginViewModelToken} from '../models/views/plugin';
import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
import {SearchViewModel, searchViewModelToken} from '../models/views/search';
-import {
- NavigationService,
- navigationToken,
-} from '../elements/core/gr-navigation/gr-navigation';
+import {navigationToken} from '../elements/core/gr-navigation/gr-navigation';
/**
* The AppContext lazy initializator for all services
*/
export function createAppContext(): AppContext & Finalizable {
const appRegistry: Registry<AppContext> = {
- routerModel: (_ctx: Partial<AppContext>) => new RouterModel(),
flagsService: (_ctx: Partial<AppContext>) =>
new FlagsServiceImplementation(),
reportingService: (ctx: Partial<AppContext>) => {
@@ -81,131 +83,132 @@
},
restApiService: (ctx: Partial<AppContext>) => {
assertIsDefined(ctx.authService, 'authService');
- assertIsDefined(ctx.flagsService, 'flagsService');
- return new GrRestApiServiceImpl(ctx.authService, ctx.flagsService);
+ return new GrRestApiServiceImpl(ctx.authService);
},
jsApiService: (ctx: Partial<AppContext>) => {
const reportingService = ctx.reportingService;
assertIsDefined(reportingService, 'reportingService');
return new GrJsApiInterface(reportingService);
},
- storageService: (_ctx: Partial<AppContext>) => new GrStorageService(),
- userModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new UserModel(ctx.restApiService);
- },
- accountsModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new AccountsModel(ctx.restApiService);
- },
pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
- highlightService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new HighlightService(ctx.reportingService);
- },
};
return create<AppContext>(appRegistry);
}
+export type Creator<T> = () => T & Finalizable;
+
+// Dependencies are provided as creator functions to ensure that they are
+// not created until they are utilized.
+// This is mainly useful in tests: E.g. don't create a
+// change-model in change-model_test.ts because it creates one in the test
+// after setting up stubs.
export function createAppDependencies(
- appContext: AppContext
-): Map<DependencyToken<unknown>, Finalizable> {
- const dependencies = new Map<DependencyToken<unknown>, Finalizable>();
- const browserModel = new BrowserModel(appContext.userModel);
- dependencies.set(browserModelToken, browserModel);
-
- const adminViewModel = new AdminViewModel();
- dependencies.set(adminViewModelToken, adminViewModel);
- const agreementViewModel = new AgreementViewModel();
- dependencies.set(agreementViewModelToken, agreementViewModel);
- const changeViewModel = new ChangeViewModel();
- dependencies.set(changeViewModelToken, changeViewModel);
- const dashboardViewModel = new DashboardViewModel();
- dependencies.set(dashboardViewModelToken, dashboardViewModel);
- const diffViewModel = new DiffViewModel();
- dependencies.set(diffViewModelToken, diffViewModel);
- const documentationViewModel = new DocumentationViewModel();
- dependencies.set(documentationViewModelToken, documentationViewModel);
- const editViewModel = new EditViewModel();
- dependencies.set(editViewModelToken, editViewModel);
- const groupViewModel = new GroupViewModel();
- dependencies.set(groupViewModelToken, groupViewModel);
- const pluginViewModel = new PluginViewModel();
- dependencies.set(pluginViewModelToken, pluginViewModel);
- const repoViewModel = new RepoViewModel();
- dependencies.set(repoViewModelToken, repoViewModel);
- const searchViewModel = new SearchViewModel(
- appContext.restApiService,
- appContext.userModel,
- () => dependencies.get(navigationToken) as unknown as NavigationService
- );
- dependencies.set(searchViewModelToken, searchViewModel);
- const settingsViewModel = new SettingsViewModel();
- dependencies.set(settingsViewModelToken, settingsViewModel);
-
- const router = new GrRouter(
- appContext.reportingService,
- appContext.routerModel,
- appContext.restApiService,
- adminViewModel,
- agreementViewModel,
- changeViewModel,
- dashboardViewModel,
- diffViewModel,
- documentationViewModel,
- editViewModel,
- groupViewModel,
- pluginViewModel,
- repoViewModel,
- searchViewModel,
- settingsViewModel
- );
- dependencies.set(routerToken, router);
- dependencies.set(navigationToken, router);
-
- const changeModel = new ChangeModel(
- appContext.routerModel,
- appContext.restApiService,
- appContext.userModel
- );
- dependencies.set(changeModelToken, changeModel);
-
- const accountsModel = new AccountsModel(appContext.restApiService);
-
- const commentsModel = new CommentsModel(
- appContext.routerModel,
- changeModel,
- accountsModel,
- appContext.restApiService,
- appContext.reportingService
- );
- dependencies.set(commentsModelToken, commentsModel);
-
- const filesModel = new FilesModel(
- changeModel,
- commentsModel,
- appContext.restApiService
- );
- dependencies.set(filesModelToken, filesModel);
-
- const configModel = new ConfigModel(changeModel, appContext.restApiService);
- dependencies.set(configModelToken, configModel);
-
- const checksModel = new ChecksModel(
- appContext.routerModel,
- changeViewModel,
- changeModel,
- appContext.reportingService,
- appContext.pluginsModel
- );
-
- dependencies.set(checksModelToken, checksModel);
-
- const shortcutsService = new ShortcutsService(
- appContext.userModel,
- appContext.reportingService
- );
- dependencies.set(shortcutsServiceToken, shortcutsService);
-
- return dependencies;
+ appContext: AppContext,
+ resolver: <T>(token: DependencyToken<T>) => T
+): Map<DependencyToken<unknown>, Creator<unknown>> {
+ return new Map<DependencyToken<unknown>, Creator<unknown>>([
+ [routerModelToken, () => new RouterModel()],
+ [userModelToken, () => new UserModel(appContext.restApiService)],
+ [browserModelToken, () => new BrowserModel(resolver(userModelToken))],
+ [accountsModelToken, () => new AccountsModel(appContext.restApiService)],
+ [adminViewModelToken, () => new AdminViewModel()],
+ [agreementViewModelToken, () => new AgreementViewModel()],
+ [changeViewModelToken, () => new ChangeViewModel()],
+ [dashboardViewModelToken, () => new DashboardViewModel()],
+ [diffViewModelToken, () => new DiffViewModel()],
+ [documentationViewModelToken, () => new DocumentationViewModel()],
+ [editViewModelToken, () => new EditViewModel()],
+ [groupViewModelToken, () => new GroupViewModel()],
+ [pluginViewModelToken, () => new PluginViewModel()],
+ [repoViewModelToken, () => new RepoViewModel()],
+ [
+ searchViewModelToken,
+ () =>
+ new SearchViewModel(
+ appContext.restApiService,
+ resolver(userModelToken),
+ () => resolver(navigationToken)
+ ),
+ ],
+ [settingsViewModelToken, () => new SettingsViewModel()],
+ [
+ routerToken,
+ () =>
+ new GrRouter(
+ appContext.reportingService,
+ resolver(routerModelToken),
+ appContext.restApiService,
+ resolver(adminViewModelToken),
+ resolver(agreementViewModelToken),
+ resolver(changeViewModelToken),
+ resolver(dashboardViewModelToken),
+ resolver(diffViewModelToken),
+ resolver(documentationViewModelToken),
+ resolver(editViewModelToken),
+ resolver(groupViewModelToken),
+ resolver(pluginViewModelToken),
+ resolver(repoViewModelToken),
+ resolver(searchViewModelToken),
+ resolver(settingsViewModelToken)
+ ),
+ ],
+ [navigationToken, () => resolver(routerToken)],
+ [
+ changeModelToken,
+ () =>
+ new ChangeModel(
+ resolver(routerModelToken),
+ appContext.restApiService,
+ resolver(userModelToken)
+ ),
+ ],
+ [
+ commentsModelToken,
+ () =>
+ new CommentsModel(
+ resolver(routerModelToken),
+ resolver(changeModelToken),
+ resolver(accountsModelToken),
+ appContext.restApiService,
+ appContext.reportingService
+ ),
+ ],
+ [
+ filesModelToken,
+ () =>
+ new FilesModel(
+ resolver(changeModelToken),
+ resolver(commentsModelToken),
+ appContext.restApiService
+ ),
+ ],
+ [
+ configModelToken,
+ () =>
+ new ConfigModel(resolver(changeModelToken), appContext.restApiService),
+ ],
+ [
+ checksModelToken,
+ () =>
+ new ChecksModel(
+ resolver(changeViewModelToken),
+ resolver(changeModelToken),
+ appContext.reportingService,
+ appContext.pluginsModel
+ ),
+ ],
+ [
+ shortcutsServiceToken,
+ () =>
+ new ShortcutsService(
+ resolver(userModelToken),
+ appContext.reportingService
+ ),
+ ],
+ [storageServiceToken, () => new GrStorageService()],
+ [
+ highlightServiceToken,
+ () => new HighlightService(appContext.reportingService),
+ ],
+ ]);
}
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 5f47c43..a8074b6 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -10,26 +10,16 @@
import {AuthService} from './gr-auth/gr-auth';
import {RestApiService} from './gr-rest-api/gr-rest-api';
import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
-import {StorageService} from './storage/gr-storage';
-import {UserModel} from '../models/user/user-model';
-import {RouterModel} from './router/router-model';
import {PluginsModel} from '../models/plugins/plugins-model';
-import {HighlightService} from './highlight/highlight-service';
-import {AccountsModel} from '../models/accounts-model/accounts-model';
export interface AppContext {
- routerModel: RouterModel;
flagsService: FlagsService;
reportingService: ReportingService;
eventEmitter: EventEmitterService;
authService: AuthService;
restApiService: RestApiService;
jsApiService: JsApiService;
- storageService: StorageService;
- userModel: UserModel;
- accountsModel: AccountsModel;
pluginsModel: PluginsModel;
- highlightService: HighlightService;
}
/**
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 66498d1..5ce4e39 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
@@ -146,7 +146,6 @@
import {addDraftProp, DraftInfo} from '../../utils/comment-util';
import {BaseScheduler} from '../scheduler/scheduler';
import {MaxInFlightScheduler} from '../scheduler/max-in-flight-scheduler';
-import {FlagsService} from '../flags/flags';
const MAX_PROJECT_RESULTS = 25;
@@ -280,16 +279,14 @@
readonly _etags = grEtagDecorator; // Shared across instances.
- readonly _projectLookup = projectLookup; // Shared across instances.
+ // readonly, but set in tests.
+ _projectLookup = projectLookup; // Shared across instances.
// The value is set in created, before any other actions
- private readonly _restApiHelper: GrRestApiHelper;
+ // Private, but used in tests.
+ readonly _restApiHelper: GrRestApiHelper;
- constructor(
- private readonly authService: AuthService,
- // @ts-ignore: it's ok.
- private readonly _flagsService: FlagsService
- ) {
+ constructor(private readonly authService: AuthService) {
this._restApiHelper = new GrRestApiHelper(
this._cache,
this.authService,
@@ -3110,7 +3107,7 @@
}
/**
- * Alias for _changeBaseURL.then(_fetchJSON).
+ * Alias for _changeBaseURL.then(fetchJSON).
*/
_getChangeURLAndFetch(
req: FetchChangeJSON,
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js
deleted file mode 100644
index 5af123a..0000000
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.js
+++ /dev/null
@@ -1,1552 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../test/common-test-setup';
-import {
- addListenerForTest,
- mockPromise,
- stubAuth,
- waitEventLoop,
-} from '../../test/test-utils';
-import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
-import {
- ListChangesOption,
- listChangesOptionsToHex,
-} from '../../utils/change-util';
-import {getAppContext} from '../app-context';
-import {createChange} from '../../test/test-data-generators';
-import {CURRENT} from '../../utils/patch-set-util';
-import {
- parsePrefixedJSON,
- readResponsePayload,
- JSON_PREFIX,
- // eslint-disable-next-line max-len
-} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
-import {GrRestApiServiceImpl} from './gr-rest-api-impl';
-import {CommentSide} from '../../constants/constants';
-import {EDIT, PARENT} from '../../types/common';
-import {assert} from '@open-wc/testing';
-
-const EXPECTED_QUERY_OPTIONS = listChangesOptionsToHex(
- ListChangesOption.CHANGE_ACTIONS,
- ListChangesOption.CURRENT_ACTIONS,
- ListChangesOption.CURRENT_REVISION,
- ListChangesOption.DETAILED_LABELS,
- ListChangesOption.SUBMIT_REQUIREMENTS
-);
-
-suite('gr-rest-api-service-impl tests', () => {
- let element;
-
- let ctr = 0;
- let originalCanonicalPath;
-
- setup(() => {
- // Modify CANONICAL_PATH to effectively reset cache.
- ctr += 1;
- originalCanonicalPath = window.CANONICAL_PATH;
- window.CANONICAL_PATH = `test${ctr}`;
-
- const testJSON = ')]}\'\n{"hello": "bonjour"}';
- sinon.stub(window, 'fetch').returns(
- Promise.resolve({
- ok: true,
- text() {
- return Promise.resolve(testJSON);
- },
- })
- );
- // fake auth
- sinon
- .stub(getAppContext().authService, 'authCheck')
- .returns(Promise.resolve(true));
- element = new GrRestApiServiceImpl(
- getAppContext().authService,
- getAppContext().flagsService
- );
- element._projectLookup = {};
- });
-
- teardown(() => {
- window.CANONICAL_PATH = originalCanonicalPath;
- });
-
- test('parent diff comments are properly grouped', () => {
- sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(() =>
- Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- updated: '2017-02-03 22:32:28.000000000',
- message: 'this isn’t quite right',
- },
- {
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- })
- );
- return element
- ._getDiffComments('42', '', undefined, PARENT, 1, 'sieve.go')
- .then(obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- path: 'sieve.go',
- updated: '2017-02-03 22:33:28.000000000',
- });
- assert.equal(obj.comments.length, 1);
- assert.deepEqual(obj.comments[0], {
- message: 'this isn’t quite right',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- });
- });
-
- test('_setRange', () => {
- const comments = [
- {
- id: 1,
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ];
- const expectedResult = {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- };
- const comment = comments[1];
- assert.deepEqual(element._setRange(comments, comment), expectedResult);
- });
-
- test('_setRanges', () => {
- const comments = [
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- id: 1,
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- const expectedResult = [
- {
- id: 1,
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:32:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 2,
- in_reply_to: 1,
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:33:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- {
- id: 3,
- in_reply_to: 2,
- message: 'this isn’t quite right either',
- updated: '2017-02-03 22:34:28.000000000',
- range: {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 1,
- },
- },
- ];
- assert.deepEqual(element._setRanges(comments), expectedResult);
- });
-
- test('differing patch diff comments are properly grouped', () => {
- sinon
- .stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(request => {
- const url = request.url;
- if (url === '/changes/test~42/revisions/1') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'this isn’t quite right',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: CommentSide.PARENT,
- message: 'how did this work in the first place?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- ],
- });
- } else if (url === '/changes/test~42/revisions/2') {
- return Promise.resolve({
- '/COMMIT_MSG': [],
- 'sieve.go': [
- {
- message: 'What on earth are you thinking, here?',
- updated: '2017-02-03 22:32:28.000000000',
- },
- {
- side: CommentSide.PARENT,
- message: 'Yeah not sure how this worked either?',
- updated: '2017-02-03 22:33:28.000000000',
- },
- {
- message: '¯\\_(ツ)_/¯',
- updated: '2017-02-04 22:33:28.000000000',
- },
- ],
- });
- }
- });
- return element
- ._getDiffComments('42', '', undefined, 1, 2, 'sieve.go')
- .then(obj => {
- assert.equal(obj.baseComments.length, 1);
- assert.deepEqual(obj.baseComments[0], {
- message: 'this isn’t quite right',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- assert.equal(obj.comments.length, 2);
- assert.deepEqual(obj.comments[0], {
- message: 'What on earth are you thinking, here?',
- path: 'sieve.go',
- updated: '2017-02-03 22:32:28.000000000',
- });
- assert.deepEqual(obj.comments[1], {
- message: '¯\\_(ツ)_/¯',
- path: 'sieve.go',
- updated: '2017-02-04 22:33:28.000000000',
- });
- });
- });
-
- test('server error', () => {
- const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
- stubAuth('fetch').returns(Promise.resolve({ok: false}));
- const serverErrorEventPromise = new Promise(resolve => {
- addListenerForTest(document, 'server-error', resolve);
- });
-
- return Promise.all([
- element._restApiHelper.fetchJSON({}).then(response => {
- assert.isUndefined(response);
- assert.isTrue(getResponseObjectStub.notCalled);
- }),
- serverErrorEventPromise,
- ]);
- });
-
- test('legacy n,z key in change url is replaced', async () => {
- sinon.stub(element, 'getConfig').callsFake(async () => {
- return {};
- });
- const stub = sinon
- .stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve([]));
- await element.getChanges(1, null, 'n,z');
- assert.equal(stub.lastCall.args[0].params.S, 0);
- });
-
- test('saveDiffPreferences invalidates cache line', () => {
- const cacheKey = '/accounts/self/preferences.diff';
- const sendStub = sinon.stub(element._restApiHelper, 'send');
- element._cache.set(cacheKey, {tab_size: 4});
- element.saveDiffPreferences({tab_size: 8});
- assert.isTrue(sendStub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
- });
-
- suite('getAccountSuggestions', () => {
- let fetchStub;
- setup(() => {
- fetchStub = sinon
- .stub(element._restApiHelper, 'fetch')
- .returns(Promise.resolve(new Response()));
- });
-
- test('url with just email', () => {
- element.getSuggestedAccounts('bro');
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- 'test52/accounts/?o=DETAILS&q=bro'
- );
- });
-
- test('url with email and canSee changeId', () => {
- element.getSuggestedAccounts('bro', undefined, 341682);
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- 'test53/accounts/?o=DETAILS&q=bro%20and%20cansee%3A341682'
- );
- });
-
- test('url with email and canSee changeId and isActive', () => {
- element.getSuggestedAccounts('bro', undefined, 341682, true);
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(
- fetchStub.firstCall.args[0].url,
- 'test54/accounts/?o=DETAILS&q=bro%20and%20' +
- 'cansee%3A341682%20and%20is%3Aactive'
- );
- });
- });
-
- test('getAccount when resp is null does not add to cache', async () => {
- const cacheKey = '/accounts/self/detail';
- const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve());
-
- await element.getAccount();
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
-
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn();
- });
-
- test('getAccount does not add to cache when status is 403', async () => {
- const cacheKey = '/accounts/self/detail';
- const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve());
-
- await element.getAccount();
- assert.isTrue(stub.called);
- assert.isFalse(element._restApiHelper._cache.has(cacheKey));
-
- element._cache.set(cacheKey, 'fake cache');
- stub.lastCall.args[0].errFn({status: 403});
- });
-
- test('getAccount when resp is successful', async () => {
- const cacheKey = '/accounts/self/detail';
- const stub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve());
-
- await element.getAccount();
-
- element._restApiHelper._cache.set(cacheKey, 'fake cache');
- assert.isTrue(stub.called);
- assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
- stub.lastCall.args[0].errFn({});
- });
-
- const preferenceSetup = function(testJSON, loggedIn) {
- sinon
- .stub(element, 'getLoggedIn')
- .callsFake(() => Promise.resolve(loggedIn));
- sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .callsFake(() => Promise.resolve(testJSON));
- };
-
- test('getPreferences returns correctly logged in', () => {
- const testJSON = {diff_view: 'SIDE_BY_SIDE'};
- const loggedIn = true;
-
- preferenceSetup(testJSON, loggedIn);
-
- return element.getPreferences().then(obj => {
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- });
- });
-
- test('getPreferences returns correctly on larger screens logged in', () => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = true;
-
- preferenceSetup(testJSON, loggedIn);
-
- return element.getPreferences().then(obj => {
- assert.equal(obj.diff_view, 'UNIFIED_DIFF');
- });
- });
-
- test('getPreferences returns correctly on larger screens no login', () => {
- const testJSON = {diff_view: 'UNIFIED_DIFF'};
- const loggedIn = false;
-
- preferenceSetup(testJSON, loggedIn);
-
- return element.getPreferences().then(obj => {
- assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
- });
- });
-
- test('savPreferences normalizes download scheme', () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve(new Response()));
- element.savePreferences({download_scheme: 'HTTP'});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
- });
-
- test('getDiffPreferences returns correct defaults', () => {
- sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
-
- return element.getDiffPreferences().then(obj => {
- assert.equal(obj.context, 10);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.font_size, 12);
- assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.show_line_endings, true);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- });
- });
-
- test('saveDiffPreferences set show_tabs to false', () => {
- const sendStub = sinon.stub(element._restApiHelper, 'send');
- element.saveDiffPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
-
- test('getEditPreferences returns correct defaults', () => {
- sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
-
- return element.getEditPreferences().then(obj => {
- assert.equal(obj.auto_close_brackets, false);
- assert.equal(obj.cursor_blink_rate, 0);
- assert.equal(obj.hide_line_numbers, false);
- assert.equal(obj.hide_top_menu, false);
- assert.equal(obj.indent_unit, 2);
- assert.equal(obj.indent_with_tabs, false);
- assert.equal(obj.key_map_type, 'DEFAULT');
- assert.equal(obj.line_length, 100);
- assert.equal(obj.line_wrapping, false);
- assert.equal(obj.match_brackets, true);
- assert.equal(obj.show_base, false);
- assert.equal(obj.show_tabs, true);
- assert.equal(obj.show_whitespace_errors, true);
- assert.equal(obj.syntax_highlighting, true);
- assert.equal(obj.tab_size, 8);
- assert.equal(obj.theme, 'DEFAULT');
- });
- });
-
- test('saveEditPreferences set show_tabs to false', () => {
- const sendStub = sinon.stub(element._restApiHelper, 'send');
- element.saveEditPreferences({show_tabs: false});
- assert.isTrue(sendStub.called);
- assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
- });
-
- test('confirmEmail', () => {
- const sendStub = sinon.spy(element._restApiHelper, 'send');
- element.confirmEmail('foo');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'PUT');
- assert.equal(sendStub.lastCall.args[0].url, '/config/server/email.confirm');
- assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
- });
-
- test('setAccountStatus', () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve('OOO'));
- element._cache.set('/accounts/self/detail', {});
- return element.setAccountStatus('OOO').then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'PUT');
- assert.equal(sendStub.lastCall.args[0].url, '/accounts/self/status');
- assert.deepEqual(sendStub.lastCall.args[0].body, {status: 'OOO'});
- assert.deepEqual(
- element._restApiHelper._cache.get('/accounts/self/detail'),
- {status: 'OOO'}
- );
- });
- });
-
- suite('draft comments', () => {
- test('_sendDiffDraftRequest pending requests tracked', () => {
- const obj = element._pendingRequests;
- sinon
- .stub(element, '_getChangeURLAndSend')
- .callsFake(() => mockPromise());
- assert.notOk(element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 1);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 2);
- assert.isTrue(!!element.hasPendingDiffDrafts());
-
- for (const promise of obj.sendDiffDraft) {
- promise.resolve();
- }
-
- return element.awaitPendingDiffDrafts().then(() => {
- assert.equal(obj.sendDiffDraft.length, 0);
- assert.isFalse(!!element.hasPendingDiffDrafts());
- });
- });
-
- suite('_failForCreate200', () => {
- test('_sendDiffDraftRequest checks for 200 on create', () => {
- const sendPromise = Promise.resolve();
- sinon.stub(element, '_getChangeURLAndSend').returns(sendPromise);
- const failStub = sinon
- .stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
- assert.isTrue(failStub.calledOnce);
- assert.isTrue(failStub.calledWithExactly(sendPromise));
- });
- });
-
- test('_sendDiffDraftRequest no checks for 200 on non create', () => {
- sinon.stub(element, '_getChangeURLAndSend').returns(Promise.resolve());
- const failStub = sinon
- .stub(element, '_failForCreate200')
- .returns(Promise.resolve());
- return element
- ._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
- .then(() => {
- assert.isFalse(failStub.called);
- });
- });
-
- test('_failForCreate200 fails on 200', () => {
- const result = {
- ok: true,
- status: 200,
- headers: {
- entries: () => [
- ['Set-CoOkiE', 'secret'],
- ['Innocuous', 'hello'],
- ],
- },
- };
- return element
- ._failForCreate200(Promise.resolve(result))
- .then(() => {
- assert.fail('Error expected.');
- })
- .catch(e => {
- assert.isOk(e);
- assert.include(e.message, 'Saving draft resulted in HTTP 200');
- assert.include(e.message, 'hello');
- assert.notInclude(e.message, 'secret');
- });
- });
-
- test('_failForCreate200 does not fail on 201', () => {
- const result = {
- ok: true,
- status: 201,
- headers: {entries: () => []},
- };
- return element._failForCreate200(Promise.resolve(result));
- });
- });
- });
-
- test('saveChangeEdit', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const change_num = '1';
- const file_name = 'index.php';
- const file_contents = '<?php';
- sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve([change_num, file_name, file_contents]));
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, file_name, file_contents]));
- element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
- return element
- .saveChangeEdit(change_num, file_name, file_contents)
- .then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].method,
- 'PUT'
- );
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/edit/' + file_name
- );
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].body,
- file_contents
- );
- });
- });
-
- test('putChangeCommitMessage', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const change_num = '1';
- const message = 'this is a commit message';
- sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve([change_num, message]));
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, message]));
- element._cache.set('/changes/' + change_num + '/message', {});
- return element.putChangeCommitMessage(change_num, message).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/message'
- );
- assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body, {
- message,
- });
- });
- });
-
- test('deleteChangeCommitMessage', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const change_num = '1';
- const messageId = 'abc';
- sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve([change_num, messageId]));
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve([change_num, messageId]));
- return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
- assert.isTrue(element._restApiHelper.send.calledOnce);
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].method,
- 'DELETE'
- );
- assert.equal(
- element._restApiHelper.send.lastCall.args[0].url,
- '/changes/test~1/messages/abc'
- );
- });
- });
-
- test('startWorkInProgress', () => {
- const sendStub = sinon
- .stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('ok'));
- element.startWorkInProgress('42');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {});
-
- element.startWorkInProgress('42', 'revising...');
- assert.isTrue(sendStub.calledTwice);
- assert.equal(sendStub.lastCall.args[0].changeNum, '42');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.isNotOk(sendStub.lastCall.args[0].patchNum);
- assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
- assert.deepEqual(sendStub.lastCall.args[0].body, {
- message: 'revising...',
- });
- });
-
- test('deleteComment', () => {
- const sendStub = sinon
- .stub(element, '_getChangeURLAndSend')
- .returns(Promise.resolve('some response'));
- return element
- .deleteComment('foo', 'bar', '01234', 'removal reason')
- .then(response => {
- assert.equal(response, 'some response');
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].changeNum, 'foo');
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(sendStub.lastCall.args[0].patchNum, 'bar');
- assert.equal(
- sendStub.lastCall.args[0].endpoint,
- '/comments/01234/delete'
- );
- assert.deepEqual(sendStub.lastCall.args[0].body, {
- reason: 'removal reason',
- });
- });
- });
-
- test('createRepo encodes name', () => {
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
- return element.createRepo({name: 'x/y'}).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
- });
- });
-
- test('queryChangeFiles', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.queryChangeFiles('42', EDIT, 'test/path.js').then(() => {
- assert.equal(fetchStub.lastCall.args[0].changeNum, '42');
- assert.equal(
- fetchStub.lastCall.args[0].endpoint,
- '/files?q=test%2Fpath.js'
- );
- assert.equal(fetchStub.lastCall.args[0].revision, EDIT);
- });
- });
-
- test('normal use', () => {
- const defaultQuery = '';
-
- assert.equal(
- element._getReposUrl('test', 25).toString(),
- [false, '/projects/?n=26&S=0&d=&m=test'].toString()
- );
-
- assert.equal(
- element._getReposUrl(null, 25).toString(),
- [false, `/projects/?n=26&S=0&d=&m=${defaultQuery}`].toString()
- );
-
- assert.equal(
- element._getReposUrl('test', 25, 25).toString(),
- [false, '/projects/?n=26&S=25&d=&m=test'].toString()
- );
-
- assert.equal(
- element._getReposUrl('inname:test', 25, 25).toString(),
- [true, '/projects/?n=26&S=25&query=inname%3Atest'].toString()
- );
- });
-
- test('invalidateReposCache', () => {
- const url = '/projects/?n=26&S=0&query=test';
-
- element._cache.set(url, {});
-
- element.invalidateReposCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- test('invalidateAccountsCache', () => {
- const url = '/accounts/self/detail';
-
- element._cache.set(url, {});
-
- element.invalidateAccountsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getRepos', () => {
- const defaultQuery = '';
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub = sinon
- .stub(element._restApiHelper, 'fetchCacheURL')
- .returns(Promise.resolve([]));
- });
-
- test('normal use', () => {
- element.getRepos('test', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=test'
- );
-
- element.getRepos(null, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&d=&m=${defaultQuery}`
- );
-
- element.getRepos('test', 25, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=25&d=&m=test'
- );
- });
-
- test('with blank', () => {
- element.getRepos('test/test', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=test%2Ftest'
- );
- });
-
- test('with hyphen', () => {
- element.getRepos('foo-bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo-bar'
- );
- });
-
- test('with leading hyphen', () => {
- element.getRepos('-bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=-bar'
- );
- });
-
- test('with trailing hyphen', () => {
- element.getRepos('foo-bar-', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo-bar-'
- );
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo_bar'
- );
- });
-
- test('with underscore', () => {
- element.getRepos('foo_bar', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/?n=26&S=0&d=&m=foo_bar'
- );
- });
-
- test('hyphen only', () => {
- element.getRepos('-', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&d=&m=-`
- );
- });
-
- test('using query', () => {
- element.getRepos('description:project', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- `/projects/?n=26&S=0&query=description%3Aproject`
- );
- });
- });
-
- test('_getGroupsUrl normal use', () => {
- assert.equal(element._getGroupsUrl('test', 25), '/groups/?n=26&S=0&m=test');
-
- assert.equal(element._getGroupsUrl(null, 25), '/groups/?n=26&S=0');
-
- assert.equal(
- element._getGroupsUrl('test', 25, 25),
- '/groups/?n=26&S=25&m=test'
- );
- });
-
- test('invalidateGroupsCache', () => {
- const url = '/groups/?n=26&S=0&m=test';
-
- element._cache.set(url, {});
-
- element.invalidateGroupsCache();
-
- assert.isUndefined(element._sharedFetchPromises[url]);
-
- assert.isFalse(element._cache.has(url));
- });
-
- suite('getGroups', () => {
- let fetchCacheURLStub;
- setup(() => {
- fetchCacheURLStub = sinon.stub(element._restApiHelper, 'fetchCacheURL');
- });
-
- test('normal use', () => {
- element.getGroups('test', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&m=test'
- );
-
- element.getGroups(null, 25);
- assert.equal(fetchCacheURLStub.lastCall.args[0].url, '/groups/?n=26&S=0');
-
- element.getGroups('test', 25, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&m=test'
- );
- });
-
- test('regex', () => {
- element.getGroups('^test.*', 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=0&r=%5Etest.*'
- );
-
- element.getGroups('^test.*', 25, 25);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/groups/?n=26&S=25&r=%5Etest.*'
- );
- });
- });
-
- test('gerrit auth is used', () => {
- stubAuth('fetch').returns(Promise.resolve());
- element._restApiHelper.fetchJSON({url: 'foo'});
- assert(getAppContext().authService.fetch.called);
- });
-
- test('getSuggestedAccounts does not return _fetchJSON', () => {
- const _fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
- return element.getSuggestedAccounts().then(accts => {
- assert.isFalse(_fetchJSONSpy.called);
- assert.equal(accts.length, 0);
- });
- });
-
- test('_fetchJSON gets called by getSuggestedAccounts', () => {
- const _fetchJSONStub = sinon
- .stub(element._restApiHelper, 'fetchJSON')
- .callsFake(() => Promise.resolve());
- return element.getSuggestedAccounts('own').then(() => {
- assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
- q: 'own',
- o: 'DETAILS',
- });
- });
- });
-
- suite('getChangeDetail', () => {
- suite('change detail options', () => {
- setup(() => {
- sinon
- .stub(element, '_getChangeDetail')
- .callsFake(async (changeNum, options) => {
- return {changeNum, options};
- });
- });
-
- test('signed pushes disabled', async () => {
- sinon.stub(element, 'getConfig').callsFake(async () => {
- return {};
- });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.isNotOk(
- parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
- );
- });
-
- test('signed pushes enabled', async () => {
- sinon.stub(element, 'getConfig').callsFake(async () => {
- return {receive: {enable_signed_push: true}};
- });
- const {changeNum, options} = await element.getChangeDetail(123);
- assert.strictEqual(123, changeNum);
- assert.ok(
- parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
- );
- });
- });
-
- test('GrReviewerUpdatesParser.parse is used', () => {
- sinon
- .stub(GrReviewerUpdatesParser, 'parse')
- .returns(Promise.resolve('foo'));
- return element.getChangeDetail(42).then(result => {
- assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
- assert.equal(result, 'foo');
- });
- });
-
- test('_getChangeDetail passes params to ETags decorator', () => {
- const changeNum = 4321;
- element._projectLookup[changeNum] = Promise.resolve('test');
- const expectedUrl =
- window.CANONICAL_PATH + '/changes/test~4321/detail?O=516714';
- sinon.stub(element._etags, 'getOptions');
- sinon.stub(element._etags, 'collect');
- return element._getChangeDetail(changeNum, '516714').then(() => {
- assert.isTrue(element._etags.getOptions.calledWithExactly(expectedUrl));
- assert.equal(element._etags.collect.lastCall.args[0], expectedUrl);
- });
- });
-
- test('_getChangeDetail calls errFn on 500', () => {
- const errFn = sinon.stub();
- sinon.stub(element, 'getChangeActionURL').returns(Promise.resolve(''));
- sinon
- .stub(element._restApiHelper, 'fetchRawJSON')
- .returns(Promise.resolve({ok: false, status: 500}));
- return element._getChangeDetail(123, '516714', errFn).then(() => {
- assert.isTrue(errFn.called);
- });
- });
-
- test('_getChangeDetail populates _projectLookup', async () => {
- sinon.stub(element, 'getChangeActionURL').returns(Promise.resolve(''));
- sinon.stub(element._restApiHelper, 'fetchRawJSON').returns(
- Promise.resolve({
- ok: true,
- status: 200,
- text: () => Promise.resolve(`)]}'{"_number":1,"project":"test"}`),
- })
- );
- await element._getChangeDetail(1, '516714');
- assert.equal(Object.keys(element._projectLookup).length, 1);
- const project = await element._projectLookup[1];
- assert.equal(project, 'test');
- });
-
- suite('_getChangeDetail ETag cache', () => {
- let requestUrl;
- let mockResponseSerial;
- let collectSpy;
-
- setup(() => {
- requestUrl = '/foo/bar';
- const mockResponse = {foo: 'bar', baz: 42};
- mockResponseSerial = JSON_PREFIX + JSON.stringify(mockResponse);
- sinon.stub(element._restApiHelper, 'urlWithParams').returns(requestUrl);
- sinon
- .stub(element, 'getChangeActionURL')
- .returns(Promise.resolve(requestUrl));
- collectSpy = sinon.spy(element._etags, 'collect');
- });
-
- test('contributes to cache', () => {
- const getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
- sinon.stub(element._restApiHelper, 'fetchRawJSON').returns(
- Promise.resolve({
- text: () => Promise.resolve(mockResponseSerial),
- status: 200,
- ok: true,
- })
- );
-
- return element._getChangeDetail(123, '516714').then(detail => {
- assert.isFalse(getPayloadSpy.called);
- assert.isTrue(collectSpy.calledOnce);
- const cachedResponse = element._etags.getCachedPayload(requestUrl);
- assert.equal(cachedResponse, mockResponseSerial);
- });
- });
-
- test('uses cache on HTTP 304', () => {
- const getPayloadStub = sinon.stub(element._etags, 'getCachedPayload');
- getPayloadStub.returns(mockResponseSerial);
- sinon.stub(element._restApiHelper, 'fetchRawJSON').returns(
- Promise.resolve({
- text: () => Promise.resolve(''),
- status: 304,
- ok: true,
- })
- );
-
- return element._getChangeDetail(123, '').then(detail => {
- assert.isFalse(collectSpy.called);
- assert.isTrue(getPayloadStub.calledOnce);
- });
- });
- });
- });
-
- test('setInProjectLookup', async () => {
- await element.setInProjectLookup('test', 'project');
- const project = await element.getFromProjectLookup('test');
- assert.deepEqual(project, 'project');
- });
-
- suite('getFromProjectLookup', () => {
- test('getChange succeeds, no project', async () => {
- sinon.stub(element, 'getChange').returns(Promise.resolve(null));
- const val = await element.getFromProjectLookup();
- assert.strictEqual(val, undefined);
- });
-
- test('getChange succeeds with project', () => {
- sinon
- .stub(element, 'getChange')
- .returns(Promise.resolve({project: 'project'}));
- const projectLookup = element.getFromProjectLookup('test');
- return projectLookup.then(val => {
- assert.equal(val, 'project');
- assert.deepEqual(element._projectLookup, {test: projectLookup});
- });
- });
- });
-
- suite('getChanges populates _projectLookup', () => {
- test('multiple queries', async () => {
- sinon.stub(element._restApiHelper, 'fetchJSON').returns(
- Promise.resolve([
- [
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- ],
- [{_number: 3, project: 'test/test'}],
- ])
- );
- // When opt_query instanceof Array, _fetchJSON returns
- // Array<Array<Object>>.
- await element.getChangesForMultipleQueries(null, []);
- assert.equal(Object.keys(element._projectLookup).length, 3);
- const project1 = await element.getFromProjectLookup(1);
- assert.equal(project1, 'test');
- const project2 = await element.getFromProjectLookup(2);
- assert.equal(project2, 'test');
- const project3 = await element.getFromProjectLookup(3);
- assert.equal(project3, 'test/test');
- });
-
- test('no query', async () => {
- sinon.stub(element._restApiHelper, 'fetchJSON').returns(
- Promise.resolve([
- {_number: 1, project: 'test'},
- {_number: 2, project: 'test'},
- {_number: 3, project: 'test/test'},
- ])
- );
-
- // When opt_query !instanceof Array, _fetchJSON returns
- // Array<Object>.
- await element.getChanges();
- assert.equal(Object.keys(element._projectLookup).length, 3);
- const project1 = await element.getFromProjectLookup(1);
- assert.equal(project1, 'test');
- const project2 = await element.getFromProjectLookup(2);
- assert.equal(project2, 'test');
- const project3 = await element.getFromProjectLookup(3);
- assert.equal(project3, 'test/test');
- });
- });
-
- test('getDetailedChangesWithActions', async () => {
- const c1 = createChange();
- c1._number = 1;
- const c2 = createChange();
- c2._number = 2;
- const getChangesStub = sinon
- .stub(element, 'getChanges')
- .callsFake((changesPerPage, query, offset, options) => {
- assert.isUndefined(changesPerPage);
- assert.strictEqual(query, 'change:1 OR change:2');
- assert.isUndefined(offset);
- assert.strictEqual(options, EXPECTED_QUERY_OPTIONS);
- return Promise.resolve([]);
- });
- await element.getDetailedChangesWithActions([c1._number, c2._number]);
- assert.isTrue(getChangesStub.calledOnce);
- });
-
- test('_getChangeURLAndFetch', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const fetchStub = sinon
- .stub(element._restApiHelper, 'fetchJSON')
- .returns(Promise.resolve());
- const req = {changeNum: 1, endpoint: '/test', revision: 1};
- return element._getChangeURLAndFetch(req).then(() => {
- assert.equal(
- fetchStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test'
- );
- });
- });
-
- test('_getChangeURLAndSend', () => {
- element._projectLookup = {1: Promise.resolve('test')};
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
-
- const req = {
- changeNum: 1,
- method: 'POST',
- patchNum: 1,
- endpoint: '/test',
- };
- return element._getChangeURLAndSend(req).then(() => {
- assert.isTrue(sendStub.calledOnce);
- assert.equal(sendStub.lastCall.args[0].method, 'POST');
- assert.equal(
- sendStub.lastCall.args[0].url,
- '/changes/test~1/revisions/1/test'
- );
- });
- });
-
- suite('reading responses', () => {
- test('_readResponsePayload', async () => {
- const mockObject = {foo: 'bar', baz: 'foo'};
- const serial = JSON_PREFIX + JSON.stringify(mockObject);
- const mockResponse = {text: () => Promise.resolve(serial)};
- const payload = await readResponsePayload(mockResponse);
- assert.deepEqual(payload.parsed, mockObject);
- assert.equal(payload.raw, serial);
- });
-
- test('_parsePrefixedJSON', () => {
- const obj = {x: 3, y: {z: 4}, w: 23};
- const serial = JSON_PREFIX + JSON.stringify(obj);
- const result = parsePrefixedJSON(serial);
- assert.deepEqual(result, obj);
- });
- });
-
- test('setChangeTopic', () => {
- const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
- return element.setChangeTopic(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
- });
- });
-
- test('setChangeHashtag', () => {
- const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
- return element.setChangeHashtag(123, 'foo-bar').then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar');
- });
- });
-
- test('generateAccountHttpPassword', () => {
- const sendSpy = sinon.spy(element._restApiHelper, 'send');
- return element.generateAccountHttpPassword().then(() => {
- assert.isTrue(sendSpy.calledOnce);
- assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
- });
- });
-
- suite('getChangeFiles', () => {
- test('patch only', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: PARENT, patchNum: 2};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 2);
- assert.isNotOk(fetchStub.lastCall.args[0].params);
- });
- });
-
- test('simple range', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: 4, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- const range = {basePatchNum: -3, patchNum: 5};
- return element.getChangeFiles(123, range).then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- suite('getDiff', () => {
- test('patchOnly', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, PARENT, 2, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 2);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- });
- });
-
- test('simple range', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.parent);
- assert.equal(fetchStub.lastCall.args[0].params.base, 4);
- });
- });
-
- test('parent index', () => {
- const fetchStub = sinon
- .stub(element, '_getChangeURLAndFetch')
- .returns(Promise.resolve());
- return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => {
- assert.isTrue(fetchStub.calledOnce);
- assert.equal(fetchStub.lastCall.args[0].revision, 5);
- assert.isOk(fetchStub.lastCall.args[0].params);
- assert.isNotOk(fetchStub.lastCall.args[0].params.base);
- assert.equal(fetchStub.lastCall.args[0].params.parent, 3);
- });
- });
- });
-
- test('getDashboard', () => {
- const fetchCacheURLStub = sinon.stub(
- element._restApiHelper,
- 'fetchCacheURL'
- );
- element.getDashboard('gerrit/project', 'default:main');
- assert.isTrue(fetchCacheURLStub.calledOnce);
- assert.equal(
- fetchCacheURLStub.lastCall.args[0].url,
- '/projects/gerrit%2Fproject/dashboards/default%3Amain'
- );
- });
-
- test('getFileContent', () => {
- sinon.stub(element, '_getChangeURLAndSend').returns(
- Promise.resolve({
- ok: 'true',
- headers: {
- get(header) {
- if (header === 'X-FYI-Content-Type') {
- return 'text/java';
- }
- },
- },
- })
- );
-
- sinon
- .stub(element, 'getResponseObject')
- .returns(Promise.resolve('new content'));
-
- const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => {
- assert.deepEqual(res, {
- content: 'new content',
- type: 'text/java',
- ok: true,
- });
- });
-
- const normal = element.getFileContent('1', 'tst/path', '3').then(res => {
- assert.deepEqual(res, {
- content: 'new content',
- type: 'text/java',
- ok: true,
- });
- });
-
- return Promise.all([edit, normal]);
- });
-
- test('getFileContent suppresses 404s', () => {
- const res = {status: 404};
- const spy = sinon.spy();
- addListenerForTest(document, 'server-error', spy);
- sinon
- .stub(getAppContext().authService, 'fetch')
- .returns(Promise.resolve(res));
- sinon.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
- return element
- .getFileContent('1', 'tst/path', '1')
- .then(() => waitEventLoop())
- .then(() => {
- assert.isFalse(spy.called);
-
- res.status = 500;
- return element.getFileContent('1', 'tst/path', '1');
- })
- .then(() => {
- assert.isTrue(spy.called);
- assert.notEqual(spy.lastCall.args[0].detail.response.status, 404);
- });
- });
-
- test('getChangeFilesOrEditFiles is edit-sensitive', () => {
- const fn = element.getChangeOrEditFiles.bind(element);
- const getChangeFilesStub = sinon
- .stub(element, 'getChangeFiles')
- .returns(Promise.resolve({}));
- const getChangeEditFilesStub = sinon
- .stub(element, 'getChangeEditFiles')
- .returns(Promise.resolve({}));
-
- return fn('1', {patchNum: EDIT}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isFalse(getChangeFilesStub.called);
- return fn('1', {patchNum: '1'}).then(() => {
- assert.isTrue(getChangeEditFilesStub.calledOnce);
- assert.isTrue(getChangeFilesStub.calledOnce);
- });
- });
- });
-
- test('_fetch forwards request and logs', () => {
- const logStub = sinon.stub(element._restApiHelper, '_logCall');
- const response = {status: 404, text: sinon.stub()};
- const url = 'my url';
- const fetchOptions = {method: 'DELETE'};
- sinon.stub(element.authService, 'fetch').returns(Promise.resolve(response));
- const startTime = 123;
- sinon.stub(Date, 'now').returns(startTime);
- const req = {url, fetchOptions};
- return element._restApiHelper.fetch(req).then(() => {
- assert.isTrue(logStub.calledOnce);
- assert.isTrue(logStub.calledWith(req, startTime, response.status));
- assert.isFalse(response.text.called);
- });
- });
-
- test('_logCall only reports requests with anonymized URLss', async () => {
- sinon.stub(Date, 'now').returns(200);
- const handler = sinon.stub();
- addListenerForTest(document, 'gr-rpc-log', handler);
-
- element._restApiHelper._logCall({url: 'url'}, 100, 200);
- assert.isFalse(handler.called);
-
- element._restApiHelper._logCall(
- {url: 'url', anonymizedUrl: 'not url'},
- 100,
- 200
- );
- await waitEventLoop();
- assert.isTrue(handler.calledOnce);
- });
-
- test('ported comment errors do not trigger error dialog', () => {
- const change = createChange();
- const handler = sinon.stub();
- addListenerForTest(document, 'server-error', handler);
- sinon.stub(element._restApiHelper, 'fetchJSON').returns(
- Promise.resolve({
- ok: false,
- })
- );
-
- element.getPortedComments(change._number, CURRENT);
-
- assert.isFalse(handler.called);
- });
-
- test('ported drafts are not requested user is not logged in', () => {
- const change = createChange();
- sinon.stub(element, 'getLoggedIn').returns(Promise.resolve(false));
- const getChangeURLAndFetchStub = sinon.stub(
- element,
- '_getChangeURLAndFetch'
- );
-
- element.getPortedDrafts(change._number, CURRENT);
-
- assert.isFalse(getChangeURLAndFetchStub.called);
- });
-
- test('saveChangeStarred', async () => {
- sinon
- .stub(element, 'getFromProjectLookup')
- .returns(Promise.resolve('test'));
- const sendStub = sinon
- .stub(element._restApiHelper, 'send')
- .returns(Promise.resolve());
-
- await element.saveChangeStarred(123, true);
- assert.isTrue(sendStub.calledOnce);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'PUT',
- url: '/accounts/self/starred.changes/test~123',
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
-
- await element.saveChangeStarred(456, false);
- assert.isTrue(sendStub.calledTwice);
- assert.deepEqual(sendStub.lastCall.args[0], {
- method: 'DELETE',
- url: '/accounts/self/starred.changes/test~456',
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
- });
-});
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
new file mode 100644
index 0000000..00db4f4
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
@@ -0,0 +1,1587 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../test/common-test-setup';
+import {
+ addListenerForTest,
+ assertFails,
+ MockPromise,
+ mockPromise,
+ stubAuth,
+ waitEventLoop,
+} from '../../test/test-utils';
+import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
+import {
+ ListChangesOption,
+ listChangesOptionsToHex,
+} from '../../utils/change-util';
+import {getAppContext} from '../app-context';
+import {
+ createAccountDetailWithId,
+ createChange,
+ createComment,
+ createParsedChange,
+ createServerInfo,
+} from '../../test/test-data-generators';
+import {CURRENT} from '../../utils/patch-set-util';
+import {
+ parsePrefixedJSON,
+ readResponsePayload,
+ JSON_PREFIX,
+} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {GrRestApiServiceImpl} from './gr-rest-api-impl';
+import {
+ CommentSide,
+ createDefaultEditPrefs,
+ HttpMethod,
+} from '../../constants/constants';
+import {
+ BasePatchSetNum,
+ ChangeMessageId,
+ CommentInfo,
+ DashboardId,
+ DiffPreferenceInput,
+ EDIT,
+ EditPreferencesInfo,
+ Hashtag,
+ HashtagsInput,
+ NumericChangeId,
+ PARENT,
+ ParsedJSON,
+ PatchSetNum,
+ PreferencesInfo,
+ RepoName,
+ RevisionId,
+ RevisionPatchSetNum,
+ RobotCommentInfo,
+ Timestamp,
+ UrlEncodedCommentId,
+} from '../../types/common';
+import {assert} from '@open-wc/testing';
+import {AuthService} from '../gr-auth/gr-auth';
+
+const EXPECTED_QUERY_OPTIONS = listChangesOptionsToHex(
+ ListChangesOption.CHANGE_ACTIONS,
+ ListChangesOption.CURRENT_ACTIONS,
+ ListChangesOption.CURRENT_REVISION,
+ ListChangesOption.DETAILED_LABELS,
+ ListChangesOption.SUBMIT_REQUIREMENTS
+);
+
+suite('gr-rest-api-service-impl tests', () => {
+ let element: GrRestApiServiceImpl;
+ let authService: AuthService;
+
+ let ctr = 0;
+ let originalCanonicalPath: string | undefined;
+
+ setup(() => {
+ // Modify CANONICAL_PATH to effectively reset cache.
+ ctr += 1;
+ originalCanonicalPath = window.CANONICAL_PATH;
+ window.CANONICAL_PATH = `test${ctr}`;
+
+ const testJSON = ')]}\'\n{"hello": "bonjour"}';
+ sinon.stub(window, 'fetch').resolves(new Response(testJSON));
+ // fake auth
+ authService = getAppContext().authService;
+ sinon.stub(authService, 'authCheck').resolves(true);
+ element = new GrRestApiServiceImpl(authService);
+
+ element._projectLookup = {};
+ });
+
+ teardown(() => {
+ window.CANONICAL_PATH = originalCanonicalPath;
+ });
+
+ test('parent diff comments are properly grouped', async () => {
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ updated: '2017-02-03 22:32:28.000000000',
+ message: 'this isn’t quite right',
+ },
+ {
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ],
+ } as unknown as ParsedJSON);
+ const obj = await element._getDiffComments(
+ 42 as NumericChangeId,
+ '/comments',
+ undefined,
+ PARENT,
+ 1 as PatchSetNum,
+ 'sieve.go'
+ );
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ assert.equal(obj.comments.length, 1);
+ assert.deepEqual(obj.comments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ });
+
+ test('_setRange', () => {
+ const comments: CommentInfo[] = [
+ {
+ id: '1' as UrlEncodedCommentId,
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ },
+ ];
+ const expectedResult: CommentInfo = {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ };
+ const comment = comments[1];
+ assert.deepEqual(element._setRange(comments, comment), expectedResult);
+ });
+
+ test('_setRanges', () => {
+ const comments: CommentInfo[] = [
+ {
+ id: '3' as UrlEncodedCommentId,
+ in_reply_to: '2' as UrlEncodedCommentId,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000' as Timestamp,
+ },
+ {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ },
+ {
+ id: '1' as UrlEncodedCommentId,
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ const expectedResult: CommentInfo[] = [
+ {
+ id: '1' as UrlEncodedCommentId,
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: '2' as UrlEncodedCommentId,
+ in_reply_to: '1' as UrlEncodedCommentId,
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:33:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ {
+ id: '3' as UrlEncodedCommentId,
+ in_reply_to: '2' as UrlEncodedCommentId,
+ message: 'this isn’t quite right either',
+ updated: '2017-02-03 22:34:28.000000000' as Timestamp,
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 1,
+ },
+ },
+ ];
+ assert.deepEqual(element._setRanges(comments), expectedResult);
+ });
+
+ test('differing patch diff comments are properly grouped', async () => {
+ sinon.stub(element, 'getFromProjectLookup').resolves('test' as RepoName);
+ sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(async request => {
+ const url = request.url;
+ if (url === '/changes/test~42/revisions/1/comments') {
+ return {
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ message: 'this isn’t quite right',
+ updated: '2017-02-03 22:32:28.000000000',
+ },
+ {
+ side: CommentSide.PARENT,
+ message: 'how did this work in the first place?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ ],
+ } as unknown as ParsedJSON;
+ } else if (url === '/changes/test~42/revisions/2/comments') {
+ return {
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ message: 'What on earth are you thinking, here?',
+ updated: '2017-02-03 22:32:28.000000000',
+ },
+ {
+ side: CommentSide.PARENT,
+ message: 'Yeah not sure how this worked either?',
+ updated: '2017-02-03 22:33:28.000000000',
+ },
+ {
+ message: '¯\\_(ツ)_/¯',
+ updated: '2017-02-04 22:33:28.000000000',
+ },
+ ],
+ } as unknown as ParsedJSON;
+ }
+ return undefined;
+ });
+ const obj = await element._getDiffComments(
+ 42 as NumericChangeId,
+ '/comments',
+ undefined,
+ 1 as BasePatchSetNum,
+ 2 as PatchSetNum,
+ 'sieve.go'
+ );
+ assert.equal(obj.baseComments.length, 1);
+ assert.deepEqual(obj.baseComments[0], {
+ message: 'this isn’t quite right',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ assert.equal(obj.comments.length, 2);
+ assert.deepEqual(obj.comments[0], {
+ message: 'What on earth are you thinking, here?',
+ path: 'sieve.go',
+ updated: '2017-02-03 22:32:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ assert.deepEqual(obj.comments[1], {
+ message: '¯\\_(ツ)_/¯',
+ path: 'sieve.go',
+ updated: '2017-02-04 22:33:28.000000000' as Timestamp,
+ } as RobotCommentInfo);
+ });
+
+ test('server error', async () => {
+ const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
+ stubAuth('fetch').resolves(new Response(undefined, {status: 502}));
+ const serverErrorEventPromise = new Promise(resolve => {
+ addListenerForTest(document, 'server-error', resolve);
+ });
+ const response = await element._restApiHelper.fetchJSON({url: ''});
+ assert.isUndefined(response);
+ assert.isTrue(getResponseObjectStub.notCalled);
+ await serverErrorEventPromise;
+ });
+
+ test('legacy n,z key in change url is replaced', async () => {
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves([] as unknown as ParsedJSON);
+ await element.getChanges(1, undefined, 'n,z');
+ assert.equal(stub.lastCall.args[0].params!.S, 0);
+ });
+
+ test('saveDiffPreferences invalidates cache line', () => {
+ const cacheKey = '/accounts/self/preferences.diff';
+ const sendStub = sinon.stub(element._restApiHelper, 'send');
+ element._cache.set(cacheKey, {tab_size: 4} as unknown as ParsedJSON);
+ element.saveDiffPreferences({
+ tab_size: 8,
+ ignore_whitespace: 'IGNORE_NONE',
+ });
+ assert.isTrue(sendStub.called);
+ assert.isFalse(element._cache.has(cacheKey));
+ });
+
+ suite('getAccountSuggestions', () => {
+ let fetchStub: sinon.SinonStub;
+ setup(() => {
+ fetchStub = sinon
+ .stub(element._restApiHelper, 'fetch')
+ .resolves(new Response());
+ });
+
+ test('url with just email', () => {
+ element.getSuggestedAccounts('bro');
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.firstCall.args[0].url,
+ 'test52/accounts/?o=DETAILS&q=bro'
+ );
+ });
+
+ test('url with email and canSee changeId', () => {
+ element.getSuggestedAccounts('bro', undefined, 341682 as NumericChangeId);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.firstCall.args[0].url,
+ 'test53/accounts/?o=DETAILS&q=bro%20and%20cansee%3A341682'
+ );
+ });
+
+ test('url with email and canSee changeId and isActive', () => {
+ element.getSuggestedAccounts(
+ 'bro',
+ undefined,
+ 341682 as NumericChangeId,
+ true
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.firstCall.args[0].url,
+ 'test54/accounts/?o=DETAILS&q=bro%20and%20' +
+ 'cansee%3A341682%20and%20is%3Aactive'
+ );
+ });
+ });
+
+ test('getAccount when resp is undefined clears cache', async () => {
+ const cacheKey = '/accounts/self/detail';
+ const account = createAccountDetailWithId();
+ element._cache.set(cacheKey, account);
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(async req => {
+ req.errFn!(undefined);
+ return undefined;
+ });
+ assert.isTrue(element._cache.has(cacheKey));
+
+ await element.getAccount();
+ assert.isTrue(stub.called);
+ assert.isFalse(element._cache.has(cacheKey));
+ });
+
+ test('getAccount when status is 403 clears cache', async () => {
+ const cacheKey = '/accounts/self/detail';
+ const account = createAccountDetailWithId();
+ element._cache.set(cacheKey, account);
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(async req => {
+ req.errFn!(new Response(undefined, {status: 403}));
+ return undefined;
+ });
+ assert.isTrue(element._cache.has(cacheKey));
+
+ await element.getAccount();
+ assert.isTrue(stub.called);
+ assert.isFalse(element._cache.has(cacheKey));
+ });
+
+ test('getAccount when resp is successful updates cache', async () => {
+ const cacheKey = '/accounts/self/detail';
+ const account = createAccountDetailWithId();
+ const stub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(async () => {
+ element._cache.set(cacheKey, account);
+ return undefined;
+ });
+ assert.isFalse(element._cache.has(cacheKey));
+
+ await element.getAccount();
+ assert.isTrue(stub.called);
+ assert.equal(element._cache.get(cacheKey), account);
+ });
+
+ const preferenceSetup = function (testJSON: unknown, loggedIn: boolean) {
+ sinon
+ .stub(element, 'getLoggedIn')
+ .callsFake(() => Promise.resolve(loggedIn));
+ sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .callsFake(() => Promise.resolve(testJSON as ParsedJSON));
+ };
+
+ test('getPreferences returns correctly logged in', async () => {
+ const testJSON = {diff_view: 'SIDE_BY_SIDE'};
+ const loggedIn = true;
+
+ preferenceSetup(testJSON, loggedIn);
+
+ const obj = await element.getPreferences();
+ assert.equal(obj!.diff_view, 'SIDE_BY_SIDE');
+ });
+
+ test('getPreferences returns correctly on larger screens logged in', async () => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = true;
+
+ preferenceSetup(testJSON, loggedIn);
+
+ const obj = await element.getPreferences();
+ assert.equal(obj!.diff_view, 'UNIFIED_DIFF');
+ });
+
+ test('getPreferences returns correctly on larger screens no login', async () => {
+ const testJSON = {diff_view: 'UNIFIED_DIFF'};
+ const loggedIn = false;
+
+ preferenceSetup(testJSON, loggedIn);
+
+ const obj = await element.getPreferences();
+ assert.equal(obj!.diff_view, 'SIDE_BY_SIDE');
+ });
+
+ test('savPreferences normalizes download scheme', () => {
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves(new Response());
+ element.savePreferences({download_scheme: 'HTTP'});
+ assert.isTrue(sendStub.called);
+ assert.equal(
+ (sendStub.lastCall.args[0].body as Partial<PreferencesInfo>)
+ .download_scheme,
+ 'http'
+ );
+ });
+
+ test('getDiffPreferences returns correct defaults', async () => {
+ sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
+
+ const obj = (await element.getDiffPreferences())!;
+ assert.equal(obj.context, 10);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.font_size, 12);
+ assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.show_line_endings, true);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ });
+
+ test('saveDiffPreferences set show_tabs to false', () => {
+ const sendStub = sinon.stub(element._restApiHelper, 'send');
+ element.saveDiffPreferences({
+ show_tabs: false,
+ ignore_whitespace: 'IGNORE_NONE',
+ });
+ assert.isTrue(sendStub.called);
+ assert.equal(
+ (sendStub.lastCall.args[0].body as Partial<DiffPreferenceInput>)
+ .show_tabs,
+ false
+ );
+ });
+
+ test('getEditPreferences returns correct defaults', async () => {
+ sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
+
+ const obj = (await element.getEditPreferences())!;
+ assert.equal(obj.auto_close_brackets, false);
+ assert.equal(obj.cursor_blink_rate, 0);
+ assert.equal(obj.hide_line_numbers, false);
+ assert.equal(obj.hide_top_menu, false);
+ assert.equal(obj.indent_unit, 2);
+ assert.equal(obj.indent_with_tabs, false);
+ assert.equal(obj.key_map_type, 'DEFAULT');
+ assert.equal(obj.line_length, 100);
+ assert.equal(obj.line_wrapping, false);
+ assert.equal(obj.match_brackets, true);
+ assert.equal(obj.show_base, false);
+ assert.equal(obj.show_tabs, true);
+ assert.equal(obj.show_whitespace_errors, true);
+ assert.equal(obj.syntax_highlighting, true);
+ assert.equal(obj.tab_size, 8);
+ assert.equal(obj.theme, 'DEFAULT');
+ });
+
+ test('saveEditPreferences set show_tabs to false', () => {
+ const sendStub = sinon.stub(element._restApiHelper, 'send');
+ element.saveEditPreferences({
+ ...createDefaultEditPrefs(),
+ show_tabs: false,
+ });
+ assert.isTrue(sendStub.called);
+ assert.equal(
+ (sendStub.lastCall.args[0].body as EditPreferencesInfo).show_tabs,
+ false
+ );
+ });
+
+ test('confirmEmail', () => {
+ const sendStub = sinon.spy(element._restApiHelper, 'send');
+ element.confirmEmail('foo');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(sendStub.lastCall.args[0].url, '/config/server/email.confirm');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
+ });
+
+ test('setAccountStatus', async () => {
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves('OOO' as unknown as ParsedJSON);
+ element._cache.set('/accounts/self/detail', createAccountDetailWithId());
+ await element.setAccountStatus('OOO');
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(sendStub.lastCall.args[0].url, '/accounts/self/status');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {status: 'OOO'});
+ assert.deepEqual(
+ element._cache.get('/accounts/self/detail')!.status,
+ 'OOO'
+ );
+ });
+
+ suite('draft comments', () => {
+ test('_sendDiffDraftRequest pending requests tracked', async () => {
+ const obj = element._pendingRequests;
+ sinon
+ .stub(element, '_getChangeURLAndSend')
+ .callsFake(() => mockPromise());
+ assert.notOk(element.hasPendingDiffDrafts());
+
+ element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 1 as PatchSetNum,
+ {}
+ );
+ assert.equal(obj.sendDiffDraft.length, 1);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
+
+ element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 1 as PatchSetNum,
+ {}
+ );
+ assert.equal(obj.sendDiffDraft.length, 2);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
+
+ for (const promise of obj.sendDiffDraft) {
+ (promise as MockPromise<void>).resolve();
+ }
+
+ await element.awaitPendingDiffDrafts();
+ assert.equal(obj.sendDiffDraft.length, 0);
+ assert.isFalse(!!element.hasPendingDiffDrafts());
+ });
+
+ suite('_failForCreate200', () => {
+ test('_sendDiffDraftRequest checks for 200 on create', async () => {
+ const sendPromise = Promise.resolve({} as unknown as ParsedJSON);
+ sinon.stub(element, '_getChangeURLAndSend').returns(sendPromise);
+ const failStub = sinon.stub(element, '_failForCreate200').resolves();
+ await element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 4 as PatchSetNum,
+ {}
+ );
+ assert.isTrue(failStub.calledOnce);
+ assert.isTrue(failStub.calledWithExactly(sendPromise));
+ });
+
+ test('_sendDiffDraftRequest no checks for 200 on non create', async () => {
+ sinon.stub(element, '_getChangeURLAndSend').resolves();
+ const failStub = sinon.stub(element, '_failForCreate200').resolves();
+ await element._sendDiffDraftRequest(
+ HttpMethod.PUT,
+ 123 as NumericChangeId,
+ 4 as PatchSetNum,
+ {
+ id: '123' as UrlEncodedCommentId,
+ }
+ );
+ assert.isFalse(failStub.called);
+ });
+
+ test('_failForCreate200 fails on 200', async () => {
+ const result = new Response(undefined, {
+ status: 200,
+ headers: {
+ 'Set-CoOkiE': 'secret',
+ Innocuous: 'hello',
+ },
+ });
+ const error = (await assertFails(
+ element._failForCreate200(Promise.resolve(result))
+ )) as Error;
+ assert.isOk(error);
+ assert.include(error.message, 'Saving draft resulted in HTTP 200');
+ assert.include(error.message, 'hello');
+ assert.notInclude(error.message, 'secret');
+ });
+
+ test('_failForCreate200 does not fail on 201', () => {
+ const result = new Response(undefined, {status: 201});
+ return element._failForCreate200(Promise.resolve(result));
+ });
+ });
+ });
+
+ test('saveChangeEdit', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const change_num = 1 as NumericChangeId;
+ const file_name = 'index.php';
+ const file_contents = '<?php';
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves([
+ change_num,
+ file_name,
+ file_contents,
+ ] as unknown as ParsedJSON);
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves([
+ change_num,
+ file_name,
+ file_contents,
+ ] as unknown as ParsedJSON);
+ element._cache.set(
+ `/changes/${change_num}/edit/${file_name}`,
+ {} as unknown as ParsedJSON
+ );
+ await element.saveChangeEdit(change_num, file_name, file_contents);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(
+ sendStub.lastCall.args[0].url,
+ '/changes/test~1/edit/' + file_name
+ );
+ assert.equal(sendStub.lastCall.args[0].body, file_contents);
+ });
+
+ test('putChangeCommitMessage', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const change_num = 1 as NumericChangeId;
+ const message = 'this is a commit message';
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves([change_num, message] as unknown as ParsedJSON);
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves([change_num, message] as unknown as ParsedJSON);
+ element._cache.set(
+ `/changes/${change_num}/message`,
+ {} as unknown as ParsedJSON
+ );
+ await element.putChangeCommitMessage(change_num, message);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.PUT);
+ assert.equal(sendStub.lastCall.args[0].url, '/changes/test~1/message');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {
+ message,
+ });
+ });
+
+ test('deleteChangeCommitMessage', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const change_num = 1 as NumericChangeId;
+ const messageId = 'abc' as ChangeMessageId;
+ const sendStub = sinon
+ .stub(element._restApiHelper, 'send')
+ .resolves([change_num, messageId] as unknown as ParsedJSON);
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves([change_num, messageId] as unknown as ParsedJSON);
+ await element.deleteChangeCommitMessage(change_num, messageId);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.DELETE);
+ assert.equal(sendStub.lastCall.args[0].url, '/changes/test~1/messages/abc');
+ });
+
+ test('startWorkInProgress', () => {
+ const sendStub = sinon
+ .stub(element, '_getChangeURLAndSend')
+ .resolves('ok' as unknown as ParsedJSON);
+ element.startWorkInProgress(42 as NumericChangeId);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {});
+
+ element.startWorkInProgress(42 as NumericChangeId, 'revising...');
+ assert.isTrue(sendStub.calledTwice);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.isNotOk(sendStub.lastCall.args[0].patchNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/wip');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {
+ message: 'revising...',
+ });
+ });
+
+ test('deleteComment', async () => {
+ const comment = createComment();
+ const sendStub = sinon
+ .stub(element, '_getChangeURLAndSend')
+ .resolves(comment as unknown as ParsedJSON);
+ const response = await element.deleteComment(
+ 123 as NumericChangeId,
+ 1 as PatchSetNum,
+ '01234' as UrlEncodedCommentId,
+ 'removal reason'
+ );
+ assert.equal(response, comment);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].changeNum, 123 as NumericChangeId);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.equal(sendStub.lastCall.args[0].patchNum, 1 as PatchSetNum);
+ assert.equal(sendStub.lastCall.args[0].endpoint, '/comments/01234/delete');
+ assert.deepEqual(sendStub.lastCall.args[0].body, {
+ reason: 'removal reason',
+ });
+ });
+
+ test('createRepo encodes name', async () => {
+ const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+ await element.createRepo({name: 'x/y' as RepoName});
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy');
+ });
+
+ test('queryChangeFiles', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.queryChangeFiles(42 as NumericChangeId, EDIT, 'test/path.js');
+ assert.equal(fetchStub.lastCall.args[0].changeNum, 42 as NumericChangeId);
+ assert.equal(
+ fetchStub.lastCall.args[0].endpoint,
+ '/files?q=test%2Fpath.js'
+ );
+ assert.equal(fetchStub.lastCall.args[0].revision, EDIT);
+ });
+
+ test('normal use', () => {
+ const defaultQuery = '';
+
+ assert.equal(
+ element._getReposUrl('test', 25).toString(),
+ [false, '/projects/?n=26&S=0&d=&m=test'].toString()
+ );
+
+ assert.equal(
+ element._getReposUrl(undefined, 25).toString(),
+ [false, `/projects/?n=26&S=0&d=&m=${defaultQuery}`].toString()
+ );
+
+ assert.equal(
+ element._getReposUrl('test', 25, 25).toString(),
+ [false, '/projects/?n=26&S=25&d=&m=test'].toString()
+ );
+
+ assert.equal(
+ element._getReposUrl('inname:test', 25, 25).toString(),
+ [true, '/projects/?n=26&S=25&query=inname%3Atest'].toString()
+ );
+ });
+
+ test('invalidateReposCache', () => {
+ const url = '/projects/?n=26&S=0&query=test';
+
+ element._cache.set(url, {} as unknown as ParsedJSON);
+
+ element.invalidateReposCache();
+
+ assert.isUndefined(element._sharedFetchPromises.get(url));
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ test('invalidateAccountsCache', () => {
+ const url = '/accounts/self/detail';
+
+ element._cache.set(url, {} as unknown as ParsedJSON);
+
+ element.invalidateAccountsCache();
+
+ assert.isUndefined(element._sharedFetchPromises.get(url));
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getRepos', () => {
+ const defaultQuery = '';
+ let fetchCacheURLStub: sinon.SinonStub;
+ setup(() => {
+ fetchCacheURLStub = sinon
+ .stub(element._restApiHelper, 'fetchCacheURL')
+ .resolves([] as unknown as ParsedJSON);
+ });
+
+ test('normal use', () => {
+ element.getRepos('test', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=test'
+ );
+
+ element.getRepos(undefined, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ `/projects/?n=26&S=0&d=&m=${defaultQuery}`
+ );
+
+ element.getRepos('test', 25, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=25&d=&m=test'
+ );
+ });
+
+ test('with blank', () => {
+ element.getRepos('test/test', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=test%2Ftest'
+ );
+ });
+
+ test('with hyphen', () => {
+ element.getRepos('foo-bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo-bar'
+ );
+ });
+
+ test('with leading hyphen', () => {
+ element.getRepos('-bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=-bar'
+ );
+ });
+
+ test('with trailing hyphen', () => {
+ element.getRepos('foo-bar-', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo-bar-'
+ );
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo_bar'
+ );
+ });
+
+ test('with underscore', () => {
+ element.getRepos('foo_bar', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=foo_bar'
+ );
+ });
+
+ test('hyphen only', () => {
+ element.getRepos('-', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&d=&m=-'
+ );
+ });
+
+ test('using query', () => {
+ element.getRepos('description:project', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/?n=26&S=0&query=description%3Aproject'
+ );
+ });
+ });
+
+ test('_getGroupsUrl normal use', () => {
+ assert.equal(element._getGroupsUrl('test', 25), '/groups/?n=26&S=0&m=test');
+
+ assert.equal(element._getGroupsUrl('', 25), '/groups/?n=26&S=0');
+
+ assert.equal(
+ element._getGroupsUrl('test', 25, 25),
+ '/groups/?n=26&S=25&m=test'
+ );
+ });
+
+ test('invalidateGroupsCache', () => {
+ const url = '/groups/?n=26&S=0&m=test';
+
+ element._cache.set(url, {} as unknown as ParsedJSON);
+
+ element.invalidateGroupsCache();
+
+ assert.isUndefined(element._sharedFetchPromises.get(url));
+
+ assert.isFalse(element._cache.has(url));
+ });
+
+ suite('getGroups', () => {
+ let fetchCacheURLStub: sinon.SinonStub;
+ setup(() => {
+ fetchCacheURLStub = sinon.stub(element._restApiHelper, 'fetchCacheURL');
+ });
+
+ test('normal use', () => {
+ element.getGroups('test', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&m=test'
+ );
+
+ element.getGroups('', 25);
+ assert.equal(fetchCacheURLStub.lastCall.args[0].url, '/groups/?n=26&S=0');
+
+ element.getGroups('test', 25, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&m=test'
+ );
+ });
+
+ test('regex', () => {
+ element.getGroups('^test.*', 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=0&r=%5Etest.*'
+ );
+
+ element.getGroups('^test.*', 25, 25);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/groups/?n=26&S=25&r=%5Etest.*'
+ );
+ });
+ });
+
+ test('gerrit auth is used', () => {
+ const fetchStub = stubAuth('fetch').resolves();
+ element._restApiHelper.fetchJSON({url: 'foo'});
+ assert(fetchStub.called);
+ });
+
+ test('getSuggestedAccounts does not return fetchJSON', async () => {
+ const fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
+ const accts = await element.getSuggestedAccounts('');
+ assert.isFalse(fetchJSONSpy.called);
+ assert.equal(accts!.length, 0);
+ });
+
+ test('fetchJSON gets called by getSuggestedAccounts', async () => {
+ const fetchJSONStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
+ await element.getSuggestedAccounts('own');
+ assert.deepEqual(fetchJSONStub.lastCall.args[0].params, {
+ q: 'own',
+ o: 'DETAILS',
+ });
+ });
+
+ suite('getChangeDetail', () => {
+ suite('change detail options', () => {
+ let changeDetailStub: sinon.SinonStub;
+ setup(() => {
+ changeDetailStub = sinon
+ .stub(element, '_getChangeDetail')
+ .resolves({...createChange(), _number: 123 as NumericChangeId});
+ });
+
+ test('signed pushes disabled', async () => {
+ sinon.stub(element, 'getConfig').resolves({
+ ...createServerInfo(),
+ receive: {enable_signed_push: undefined},
+ });
+ const change = await element.getChangeDetail(123 as NumericChangeId);
+ assert.strictEqual(123, change!._number);
+ const options = changeDetailStub.firstCall.args[1];
+ assert.isNotOk(
+ parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
+ );
+ });
+
+ test('signed pushes enabled', async () => {
+ sinon.stub(element, 'getConfig').resolves({
+ ...createServerInfo(),
+ receive: {enable_signed_push: 'true'},
+ });
+ const change = await element.getChangeDetail(123 as NumericChangeId);
+ assert.strictEqual(123, change!._number);
+ const options = changeDetailStub.firstCall.args[1];
+ assert.ok(
+ parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
+ );
+ });
+ });
+
+ test('GrReviewerUpdatesParser.parse is used', async () => {
+ const changeInfo = createParsedChange();
+ const parseStub = sinon
+ .stub(GrReviewerUpdatesParser, 'parse')
+ .resolves(changeInfo);
+ const result = await element.getChangeDetail(42 as NumericChangeId);
+ assert.isTrue(parseStub.calledOnce);
+ assert.equal(result, changeInfo);
+ });
+
+ test('_getChangeDetail passes params to ETags decorator', async () => {
+ const changeNum = 4321 as NumericChangeId;
+ element._projectLookup[changeNum] = Promise.resolve('test' as RepoName);
+ const expectedUrl = `${window.CANONICAL_PATH}/changes/test~4321/detail?O=516714`;
+ const optionsStub = sinon.stub(element._etags, 'getOptions');
+ const collectStub = sinon.stub(element._etags, 'collect');
+ await element._getChangeDetail(changeNum, '516714');
+ assert.isTrue(optionsStub.calledWithExactly(expectedUrl));
+ assert.equal(collectStub.lastCall.args[0], expectedUrl);
+ });
+
+ test('_getChangeDetail calls errFn on 500', async () => {
+ const errFn = sinon.stub();
+ sinon.stub(element, 'getChangeActionURL').resolves('');
+ sinon
+ .stub(element._restApiHelper, 'fetchRawJSON')
+ .resolves(new Response(undefined, {status: 500}));
+ await element._getChangeDetail(123 as NumericChangeId, '516714', errFn);
+ assert.isTrue(errFn.called);
+ });
+
+ test('_getChangeDetail populates _projectLookup', async () => {
+ sinon.stub(element, 'getChangeActionURL').resolves('');
+ sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ new Response(')]}\'{"_number":1,"project":"test"}', {
+ status: 200,
+ })
+ );
+ await element._getChangeDetail(1 as NumericChangeId, '516714');
+ assert.equal(Object.keys(element._projectLookup).length, 1);
+ const project = await element._projectLookup[1];
+ assert.equal(project, 'test' as RepoName);
+ });
+
+ suite('_getChangeDetail ETag cache', () => {
+ let requestUrl: string;
+ let mockResponseSerial: string;
+ let collectSpy: sinon.SinonSpy;
+
+ setup(() => {
+ requestUrl = '/foo/bar';
+ const mockResponse = {foo: 'bar', baz: 42};
+ mockResponseSerial = JSON_PREFIX + JSON.stringify(mockResponse);
+ sinon.stub(element._restApiHelper, 'urlWithParams').returns(requestUrl);
+ sinon.stub(element, 'getChangeActionURL').resolves(requestUrl);
+ collectSpy = sinon.spy(element._etags, 'collect');
+ });
+
+ test('contributes to cache', async () => {
+ const getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
+ sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ new Response(mockResponseSerial, {
+ status: 200,
+ })
+ );
+
+ await element._getChangeDetail(123 as NumericChangeId, '516714');
+ assert.isFalse(getPayloadSpy.called);
+ assert.isTrue(collectSpy.calledOnce);
+ const cachedResponse = element._etags.getCachedPayload(requestUrl);
+ assert.equal(cachedResponse, mockResponseSerial);
+ });
+
+ test('uses cache on HTTP 304', async () => {
+ const getPayloadStub = sinon.stub(element._etags, 'getCachedPayload');
+ getPayloadStub.returns(mockResponseSerial);
+ sinon.stub(element._restApiHelper, 'fetchRawJSON').resolves(
+ new Response(undefined, {
+ status: 304,
+ })
+ );
+
+ await element._getChangeDetail(123 as NumericChangeId, '');
+ assert.isFalse(collectSpy.called);
+ assert.isTrue(getPayloadStub.calledOnce);
+ });
+ });
+ });
+
+ test('setInProjectLookup', async () => {
+ await element.setInProjectLookup(
+ 555 as NumericChangeId,
+ 'project' as RepoName
+ );
+ const project = await element.getFromProjectLookup(555 as NumericChangeId);
+ assert.deepEqual(project, 'project' as RepoName);
+ });
+
+ suite('getFromProjectLookup', () => {
+ test('getChange succeeds, no project', async () => {
+ sinon.stub(element, 'getChange').resolves(null);
+ const val = await element.getFromProjectLookup(555 as NumericChangeId);
+ assert.strictEqual(val, undefined);
+ });
+
+ test('getChange succeeds with project', async () => {
+ sinon
+ .stub(element, 'getChange')
+ .resolves({...createChange(), project: 'project' as RepoName});
+ const projectLookup = element.getFromProjectLookup(
+ 555 as NumericChangeId
+ );
+ const val = await projectLookup;
+ assert.equal(val, 'project' as RepoName);
+ assert.deepEqual(element._projectLookup, {'555': projectLookup});
+ });
+ });
+
+ suite('getChanges populates _projectLookup', () => {
+ test('multiple queries', async () => {
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves([
+ [
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ ],
+ [{_number: 3, project: 'test/test'}],
+ ] as unknown as ParsedJSON);
+ // When opt_query instanceof Array, fetchJSON returns
+ // Array<Array<Object>>.
+ await element.getChangesForMultipleQueries(undefined, []);
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ const project1 = await element.getFromProjectLookup(1 as NumericChangeId);
+ assert.equal(project1, 'test' as RepoName);
+ const project2 = await element.getFromProjectLookup(2 as NumericChangeId);
+ assert.equal(project2, 'test' as RepoName);
+ const project3 = await element.getFromProjectLookup(3 as NumericChangeId);
+ assert.equal(project3, 'test/test' as RepoName);
+ });
+
+ test('no query', async () => {
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves([
+ {_number: 1, project: 'test'},
+ {_number: 2, project: 'test'},
+ {_number: 3, project: 'test/test'},
+ ] as unknown as ParsedJSON);
+
+ // When opt_query !instanceof Array, fetchJSON returns Array<Object>.
+ await element.getChanges();
+ assert.equal(Object.keys(element._projectLookup).length, 3);
+ const project1 = await element.getFromProjectLookup(1 as NumericChangeId);
+ assert.equal(project1, 'test' as RepoName);
+ const project2 = await element.getFromProjectLookup(2 as NumericChangeId);
+ assert.equal(project2, 'test' as RepoName);
+ const project3 = await element.getFromProjectLookup(3 as NumericChangeId);
+ assert.equal(project3, 'test/test' as RepoName);
+ });
+ });
+
+ test('getDetailedChangesWithActions', async () => {
+ const c1 = createChange();
+ c1._number = 1 as NumericChangeId;
+ const c2 = createChange();
+ c2._number = 2 as NumericChangeId;
+ const getChangesStub = sinon
+ .stub(element, 'getChanges')
+ .callsFake((changesPerPage, query, offset, options) => {
+ assert.isUndefined(changesPerPage);
+ assert.strictEqual(query, 'change:1 OR change:2');
+ assert.isUndefined(offset);
+ assert.strictEqual(options, EXPECTED_QUERY_OPTIONS);
+ return Promise.resolve([]);
+ });
+ await element.getDetailedChangesWithActions([c1._number, c2._number]);
+ assert.isTrue(getChangesStub.calledOnce);
+ });
+
+ test('_getChangeURLAndFetch', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const fetchStub = sinon
+ .stub(element._restApiHelper, 'fetchJSON')
+ .resolves();
+ const req = {
+ changeNum: 1 as NumericChangeId,
+ endpoint: '/test',
+ revision: 1 as RevisionId,
+ };
+ await element._getChangeURLAndFetch(req);
+ assert.equal(
+ fetchStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test'
+ );
+ });
+
+ test('_getChangeURLAndSend', async () => {
+ element._projectLookup = {1: Promise.resolve('test' as RepoName)};
+ const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+
+ const req = {
+ changeNum: 1 as NumericChangeId,
+ method: HttpMethod.POST,
+ patchNum: 1 as PatchSetNum,
+ endpoint: '/test',
+ };
+ await element._getChangeURLAndSend(req);
+ assert.isTrue(sendStub.calledOnce);
+ assert.equal(sendStub.lastCall.args[0].method, HttpMethod.POST);
+ assert.equal(
+ sendStub.lastCall.args[0].url,
+ '/changes/test~1/revisions/1/test'
+ );
+ });
+
+ suite('reading responses', () => {
+ test('_readResponsePayload', async () => {
+ const mockObject = {foo: 'bar', baz: 'foo'} as unknown as ParsedJSON;
+ const serial = JSON_PREFIX + JSON.stringify(mockObject);
+ const response = new Response(serial);
+ const payload = await readResponsePayload(response);
+ assert.deepEqual(payload.parsed, mockObject);
+ assert.equal(payload.raw, serial);
+ });
+
+ test('_parsePrefixedJSON', () => {
+ const obj = {x: 3, y: {z: 4}, w: 23} as unknown as ParsedJSON;
+ const serial = JSON_PREFIX + JSON.stringify(obj);
+ const result = parsePrefixedJSON(serial);
+ assert.deepEqual(result, obj);
+ });
+ });
+
+ test('setChangeTopic', async () => {
+ const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
+ await element.setChangeTopic(123 as NumericChangeId, 'foo-bar');
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'});
+ });
+
+ test('setChangeHashtag', async () => {
+ const sendSpy = sinon.spy(element, '_getChangeURLAndSend');
+ await element.setChangeHashtag(123 as NumericChangeId, {
+ add: ['foo-bar' as Hashtag],
+ });
+ assert.isTrue(sendSpy.calledOnce);
+ assert.sameDeepMembers(
+ (sendSpy.lastCall.args[0].body! as HashtagsInput).add!,
+ ['foo-bar']
+ );
+ });
+
+ test('generateAccountHttpPassword', async () => {
+ const sendSpy = sinon.spy(element._restApiHelper, 'send');
+ await element.generateAccountHttpPassword();
+ assert.isTrue(sendSpy.calledOnce);
+ assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
+ });
+
+ suite('getChangeFiles', () => {
+ test('patch only', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ const range = {basePatchNum: PARENT, patchNum: 2 as RevisionPatchSetNum};
+ await element.getChangeFiles(123 as NumericChangeId, range);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(
+ fetchStub.lastCall.args[0].revision,
+ 2 as RevisionPatchSetNum
+ );
+ assert.isNotOk(fetchStub.lastCall.args[0].params);
+ });
+
+ test('simple range', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ const range = {
+ basePatchNum: 4 as BasePatchSetNum,
+ patchNum: 5 as RevisionPatchSetNum,
+ };
+ await element.getChangeFiles(123 as NumericChangeId, range);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
+ });
+
+ test('parent index', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ const range = {
+ basePatchNum: -3 as BasePatchSetNum,
+ patchNum: 5 as RevisionPatchSetNum,
+ };
+ await element.getChangeFiles(123 as NumericChangeId, range);
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
+ assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
+ });
+ });
+
+ suite('getDiff', () => {
+ test('patchOnly', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.getDiff(
+ 123 as NumericChangeId,
+ PARENT,
+ 2 as PatchSetNum,
+ 'foo/bar.baz'
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 2 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
+ });
+
+ test('simple range', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.getDiff(
+ 123 as NumericChangeId,
+ 4 as PatchSetNum,
+ 5 as PatchSetNum,
+ 'foo/bar.baz'
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
+ assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
+ });
+
+ test('parent index', async () => {
+ const fetchStub = sinon.stub(element, '_getChangeURLAndFetch').resolves();
+ await element.getDiff(
+ 123 as NumericChangeId,
+ -3 as PatchSetNum,
+ 5 as PatchSetNum,
+ 'foo/bar.baz'
+ );
+ assert.isTrue(fetchStub.calledOnce);
+ assert.equal(fetchStub.lastCall.args[0].revision, 5 as RevisionId);
+ assert.isOk(fetchStub.lastCall.args[0].params);
+ assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
+ assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
+ });
+ });
+
+ test('getDashboard', () => {
+ const fetchCacheURLStub = sinon.stub(
+ element._restApiHelper,
+ 'fetchCacheURL'
+ );
+ element.getDashboard(
+ 'gerrit/project' as RepoName,
+ 'default:main' as DashboardId
+ );
+ assert.isTrue(fetchCacheURLStub.calledOnce);
+ assert.equal(
+ fetchCacheURLStub.lastCall.args[0].url,
+ '/projects/gerrit%2Fproject/dashboards/default%3Amain'
+ );
+ });
+
+ test('getFileContent', async () => {
+ sinon.stub(element, '_getChangeURLAndSend').resolves(
+ new Response(undefined, {
+ status: 200,
+ headers: {
+ 'X-FYI-Content-Type': 'text/java',
+ },
+ }) as unknown as ParsedJSON
+ );
+
+ sinon
+ .stub(element, 'getResponseObject')
+ .resolves('new content' as unknown as ParsedJSON);
+
+ const edit = await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ 'EDIT' as PatchSetNum
+ );
+
+ assert.deepEqual(edit, {
+ content: 'new content',
+ type: 'text/java',
+ ok: true,
+ });
+
+ const normal = await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ '3' as PatchSetNum
+ );
+ assert.deepEqual(normal, {
+ content: 'new content',
+ type: 'text/java',
+ ok: true,
+ });
+ });
+
+ test('getFileContent suppresses 404s', async () => {
+ const res404 = new Response(undefined, {status: 404});
+ const res500 = new Response(undefined, {status: 500});
+ const spy = sinon.spy();
+ addListenerForTest(document, 'server-error', spy);
+ const authStub = sinon.stub(authService, 'fetch').resolves(res404);
+ sinon.stub(element, '_changeBaseURL').resolves('');
+ await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ 1 as PatchSetNum
+ );
+ await waitEventLoop();
+ assert.isFalse(spy.called);
+ authStub.reset();
+ authStub.resolves(res500);
+ await element.getFileContent(
+ 1 as NumericChangeId,
+ 'tst/path',
+ 1 as PatchSetNum
+ );
+ assert.isTrue(spy.called);
+ assert.notEqual(spy.lastCall.args[0].detail.response.status, 404);
+ });
+
+ test('getChangeFilesOrEditFiles is edit-sensitive', async () => {
+ const getChangeFilesStub = sinon
+ .stub(element, 'getChangeFiles')
+ .resolves({});
+ const getChangeEditFilesStub = sinon
+ .stub(element, 'getChangeEditFiles')
+ .resolves({files: {}});
+
+ await element.getChangeOrEditFiles(1 as NumericChangeId, {
+ basePatchNum: PARENT,
+ patchNum: EDIT,
+ });
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isFalse(getChangeFilesStub.called);
+ await element.getChangeOrEditFiles(1 as NumericChangeId, {
+ basePatchNum: PARENT,
+ patchNum: 1 as RevisionPatchSetNum,
+ });
+ assert.isTrue(getChangeEditFilesStub.calledOnce);
+ assert.isTrue(getChangeFilesStub.calledOnce);
+ });
+
+ test('_fetch forwards request and logs', async () => {
+ const logStub = sinon.stub(element._restApiHelper, '_logCall');
+ const response = new Response(undefined, {status: 404});
+ const url = 'my url';
+ const fetchOptions = {method: 'DELETE'};
+ sinon.stub(authService, 'fetch').resolves(response);
+ const startTime = 123;
+ sinon.stub(Date, 'now').returns(startTime);
+ const req = {url, fetchOptions};
+ await element._restApiHelper.fetch(req);
+ assert.isTrue(logStub.calledOnce);
+ assert.isTrue(logStub.calledWith(req, startTime, response.status));
+ });
+
+ test('_logCall only reports requests with anonymized URLss', async () => {
+ sinon.stub(Date, 'now').returns(200);
+ const handler = sinon.stub();
+ addListenerForTest(document, 'gr-rpc-log', handler);
+
+ element._restApiHelper._logCall({url: 'url'}, 100, 200);
+ assert.isFalse(handler.called);
+
+ element._restApiHelper._logCall(
+ {url: 'url', anonymizedUrl: 'not url'},
+ 100,
+ 200
+ );
+ await waitEventLoop();
+ assert.isTrue(handler.calledOnce);
+ });
+
+ test('ported comment errors do not trigger error dialog', () => {
+ const change = createChange();
+ const handler = sinon.stub();
+ addListenerForTest(document, 'server-error', handler);
+ sinon.stub(element._restApiHelper, 'fetchJSON').resolves({
+ ok: false,
+ } as unknown as ParsedJSON);
+
+ element.getPortedComments(change._number, CURRENT);
+
+ assert.isFalse(handler.called);
+ });
+
+ test('ported drafts are not requested user is not logged in', () => {
+ const change = createChange();
+ sinon.stub(element, 'getLoggedIn').resolves(false);
+ const getChangeURLAndFetchStub = sinon.stub(
+ element,
+ '_getChangeURLAndFetch'
+ );
+
+ element.getPortedDrafts(change._number, CURRENT);
+
+ assert.isFalse(getChangeURLAndFetchStub.called);
+ });
+
+ test('saveChangeStarred', async () => {
+ sinon.stub(element, 'getFromProjectLookup').resolves('test' as RepoName);
+ const sendStub = sinon.stub(element._restApiHelper, 'send').resolves();
+
+ await element.saveChangeStarred(123 as NumericChangeId, true);
+ assert.isTrue(sendStub.calledOnce);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: HttpMethod.PUT,
+ url: '/accounts/self/starred.changes/test~123',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+
+ await element.saveChangeStarred(456 as NumericChangeId, false);
+ assert.isTrue(sendStub.calledTwice);
+ assert.deepEqual(sendStub.lastCall.args[0], {
+ method: HttpMethod.DELETE,
+ url: '/accounts/self/starred.changes/test~456',
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ });
+ });
+});
diff --git a/polygerrit-ui/app/services/highlight/highlight-service.ts b/polygerrit-ui/app/services/highlight/highlight-service.ts
index bfaa263..d10d875 100644
--- a/polygerrit-ui/app/services/highlight/highlight-service.ts
+++ b/polygerrit-ui/app/services/highlight/highlight-service.ts
@@ -3,6 +3,7 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {define} from '../../models/dependency';
import {
SyntaxWorkerRequest,
SyntaxWorkerInit,
@@ -39,6 +40,8 @@
*/
const CODE_MAX_LENGTH = 25 * CODE_MAX_LINES;
+export const highlightServiceToken =
+ define<HighlightService>('highlight-service');
/**
* Service for syntax highlighting. Maintains some HighlightJS workers doing
* their job in the background.
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index f8bc778..edde7a4 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable} from 'rxjs';
-import {Finalizable} from '../registry';
import {
NumericChangeId,
RevisionPatchSetNum,
@@ -12,6 +11,7 @@
} from '../../types/common';
import {Model} from '../../models/model';
import {select} from '../../utils/observable-util';
+import {define} from '../../models/dependency';
export enum GerritView {
ADMIN = 'admin',
@@ -36,7 +36,8 @@
basePatchNum?: BasePatchSetNum;
}
-export class RouterModel extends Model<RouterState> implements Finalizable {
+export const routerModelToken = define<RouterModel>('router-model');
+export class RouterModel extends Model<RouterState> {
readonly routerView$: Observable<GerritView | undefined> = select(
this.state$,
state => state.view
diff --git a/polygerrit-ui/app/services/service-worker-installer.ts b/polygerrit-ui/app/services/service-worker-installer.ts
index 53cd325..ffc5be2 100644
--- a/polygerrit-ui/app/services/service-worker-installer.ts
+++ b/polygerrit-ui/app/services/service-worker-installer.ts
@@ -12,11 +12,14 @@
import {UserModel} from '../models/user/user-model';
import {AccountDetailInfo} from '../api/rest-api';
import {until} from '../utils/async-util';
+import {LifeCycle} from '../constants/reporting';
+import {ReportingService} from './gr-reporting/gr-reporting';
/** Type of incoming messages for ServiceWorker. */
export enum ServiceWorkerMessageType {
TRIGGER_NOTIFICATIONS = 'TRIGGER_NOTIFICATIONS',
USER_PREFERENCE_CHANGE = 'USER_PREFERENCE_CHANGE',
+ REPORTING = 'REPORTING',
}
export const TRIGGER_NOTIFICATION_UPDATES_MS = 5 * 60 * 1000;
@@ -30,6 +33,7 @@
constructor(
private readonly flagsService: FlagsService,
+ private readonly reportingService: ReportingService,
private readonly userModel: UserModel
) {
if (!this.flagsService.isEnabled(KnownExperimentId.PUSH_NOTIFICATIONS)) {
@@ -74,8 +78,19 @@
}
await registerServiceWorker('/service-worker.js');
const permission = await Notification.requestPermission();
+ this.reportingService.reportLifeCycle(LifeCycle.NOTIFICATION_PERMISSION, {
+ permission,
+ });
if (this.isPermitted(permission)) this.startTriggerTimer();
this.initialized = true;
+ // Assumption: service worker will send event only to 1 client.
+ navigator.serviceWorker.onmessage = event => {
+ if (event.data?.type === ServiceWorkerMessageType.REPORTING) {
+ this.reportingService.reportLifeCycle(LifeCycle.SERVICE_WORKER_UPDATE, {
+ eventName: event.data.eventName as string | undefined,
+ });
+ }
+ };
}
areNotificationsEnabled() {
diff --git a/polygerrit-ui/app/services/service-worker-installer_test.ts b/polygerrit-ui/app/services/service-worker-installer_test.ts
index e8fd233..a036289 100644
--- a/polygerrit-ui/app/services/service-worker-installer_test.ts
+++ b/polygerrit-ui/app/services/service-worker-installer_test.ts
@@ -9,14 +9,17 @@
import {assert} from '@open-wc/testing';
import {createDefaultPreferences} from '../constants/constants';
import {waitUntilObserved} from '../test/test-utils';
+import {testResolver} from '../test/common-test-setup';
+import {userModelToken} from '../models/user/user-model';
suite('service worker installer tests', () => {
test('init', async () => {
const registerStub = sinon.stub(window.navigator.serviceWorker, 'register');
const flagsService = getAppContext().flagsService;
- const userModel = getAppContext().userModel;
+ const reportingService = getAppContext().reportingService;
+ const userModel = testResolver(userModelToken);
sinon.stub(flagsService, 'isEnabled').returns(true);
- new ServiceWorkerInstaller(flagsService, userModel);
+ new ServiceWorkerInstaller(flagsService, reportingService, userModel);
const prefs = {
...createDefaultPreferences(),
allow_browser_notifications: true,
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
index 5b38a8a..0a5e0a4 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -15,6 +15,8 @@
import {getAppContext} from '../app-context';
import {pressKey} from '../../test/test-utils';
import {assert} from '@open-wc/testing';
+import {testResolver} from '../../test/common-test-setup';
+import {userModelToken} from '../../models/user/user-model';
const KEY_A: Binding = {key: 'a'};
@@ -23,7 +25,7 @@
setup(() => {
service = new ShortcutsService(
- getAppContext().userModel,
+ testResolver(userModelToken),
getAppContext().reportingService
);
});
diff --git a/polygerrit-ui/app/services/storage/gr-storage_impl.ts b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
index 0caffbc..c177ddc 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_impl.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
@@ -6,6 +6,7 @@
import {StorageLocation, StorageObject, StorageService} from './gr-storage';
import {Finalizable} from '../registry';
import {NumericChangeId} from '../../types/common';
+import {define} from '../../models/dependency';
export const DURATION_DAY = 24 * 60 * 60 * 1000;
@@ -16,6 +17,8 @@
CLEANUP_PREFIXES_MAX_AGE_MAP.set('draft', DURATION_DAY);
CLEANUP_PREFIXES_MAX_AGE_MAP.set('editablecontent', DURATION_DAY);
+export const storageServiceToken = define<StorageService>('storage-service');
+
export class GrStorageService implements StorageService, Finalizable {
private lastCleanup = 0;
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 306747b..31462b5 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -11,7 +11,6 @@
import {
createTestAppContext,
createTestDependencies,
- Creator,
} from './test-app-context-init';
import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {_testOnlyResetGrRestApiSharedObjects} from '../services/gr-rest-api/gr-rest-api-impl';
@@ -39,6 +38,7 @@
} from '../models/dependency';
import * as sinon from 'sinon';
import '../styles/themes/app-theme.ts';
+import {Creator} from '../services/app-context-init';
declare global {
interface Window {
@@ -129,6 +129,10 @@
_testOnlyResetGrRestApiSharedObjects();
});
+export function removeRequestDependencyListener() {
+ document.removeEventListener('request-dependency', resolveDependency);
+}
+
// Very simple function to catch unexpected elements in documents body.
// It can't catch everything, but in most cases it is enough.
function checkChildAllowed(element: Element) {
@@ -189,7 +193,7 @@
removeThemeStyles();
cancelAllTasks();
cleanUpStorage();
- document.removeEventListener('request-dependency', resolveDependency);
+ removeRequestDependencyListener();
injectedDependencies.clear();
// Reset state
for (const f of finalizers) {
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 0693570..a0b6130 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -6,7 +6,6 @@
// Init app context before any other imports
import {create, Registry, Finalizable} from '../services/registry';
-import {DependencyToken} from '../models/dependency';
import {assertIsDefined} from '../utils/common-util';
import {AppContext} from '../services/app-context';
import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
@@ -15,58 +14,17 @@
import {GrAuthMock} from '../services/gr-auth/gr-auth_mock';
import {FlagsServiceImplementation} from '../services/flags/flags_impl';
import {EventEmitter} from '../services/gr-event-interface/gr-event-interface_impl';
-import {ChangeModel, changeModelToken} from '../models/change/change-model';
-import {FilesModel, filesModelToken} from '../models/change/files-model';
-import {ChecksModel, checksModelToken} from '../models/checks/checks-model';
import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
-import {UserModel} from '../models/user/user-model';
-import {
- CommentsModel,
- commentsModelToken,
-} from '../models/comments/comments-model';
-import {RouterModel} from '../services/router/router-model';
-import {
- ShortcutsService,
- shortcutsServiceToken,
-} from '../services/shortcuts/shortcuts-service';
-import {ConfigModel, configModelToken} from '../models/config/config-model';
-import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
import {PluginsModel} from '../models/plugins/plugins-model';
import {MockHighlightService} from '../services/highlight/highlight-service-mock';
-import {
- AccountsModel,
- accountsModelToken,
-} from '../models/accounts-model/accounts-model';
-import {
- DashboardViewModel,
- dashboardViewModelToken,
-} from '../models/views/dashboard';
-import {
- SettingsViewModel,
- settingsViewModelToken,
-} from '../models/views/settings';
-import {GrRouter, routerToken} from '../elements/core/gr-router/gr-router';
-import {AdminViewModel, adminViewModelToken} from '../models/views/admin';
-import {
- AgreementViewModel,
- agreementViewModelToken,
-} from '../models/views/agreement';
-import {ChangeViewModel, changeViewModelToken} from '../models/views/change';
-import {DiffViewModel, diffViewModelToken} from '../models/views/diff';
-import {
- DocumentationViewModel,
- documentationViewModelToken,
-} from '../models/views/documentation';
-import {EditViewModel, editViewModelToken} from '../models/views/edit';
-import {GroupViewModel, groupViewModelToken} from '../models/views/group';
-import {PluginViewModel, pluginViewModelToken} from '../models/views/plugin';
-import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
-import {SearchViewModel, searchViewModelToken} from '../models/views/search';
+import {createAppDependencies, Creator} from '../services/app-context-init';
import {navigationToken} from '../elements/core/gr-navigation/gr-navigation';
+import {DependencyToken} from '../models/dependency';
+import {storageServiceToken} from '../services/storage/gr-storage_impl';
+import {highlightServiceToken} from '../services/highlight/highlight-service';
export function createTestAppContext(): AppContext & Finalizable {
const appRegistry: Registry<AppContext> = {
- routerModel: (_ctx: Partial<AppContext>) => new RouterModel(),
flagsService: (_ctx: Partial<AppContext>) =>
new FlagsServiceImplementation(),
reportingService: (_ctx: Partial<AppContext>) => grReportingMock,
@@ -80,91 +38,17 @@
assertIsDefined(ctx.reportingService, 'reportingService');
return new GrJsApiInterface(ctx.reportingService);
},
- storageService: (_ctx: Partial<AppContext>) => grStorageMock,
- userModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new UserModel(ctx.restApiService);
- },
- accountsModel: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.restApiService, 'restApiService');
- return new AccountsModel(ctx.restApiService);
- },
- shortcutsService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.userModel, 'userModel');
- assertIsDefined(ctx.flagsService, 'flagsService');
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new ShortcutsService(ctx.userModel, ctx.reportingService);
- },
pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
- highlightService: (ctx: Partial<AppContext>) => {
- assertIsDefined(ctx.reportingService, 'reportingService');
- return new MockHighlightService(ctx.reportingService);
- },
};
return create<AppContext>(appRegistry);
}
-export type Creator<T> = () => T & Finalizable;
-
-// Test dependencies are provides as creator functions to ensure that they are
-// not created if a test doesn't depend on them. E.g. don't create a
-// change-model in change-model_test.ts because it creates one in the test
-// after setting up stubs.
export function createTestDependencies(
appContext: AppContext,
resolver: <T>(token: DependencyToken<T>) => T
): Map<DependencyToken<unknown>, Creator<unknown>> {
- const dependencies = new Map<DependencyToken<unknown>, Creator<unknown>>();
- const browserModel = () => new BrowserModel(appContext.userModel);
- dependencies.set(browserModelToken, browserModel);
-
- const adminViewModelCreator = () => new AdminViewModel();
- dependencies.set(adminViewModelToken, adminViewModelCreator);
- const agreementViewModelCreator = () => new AgreementViewModel();
- dependencies.set(agreementViewModelToken, agreementViewModelCreator);
- const changeViewModelCreator = () => new ChangeViewModel();
- dependencies.set(changeViewModelToken, changeViewModelCreator);
- const dashboardViewModelCreator = () => new DashboardViewModel();
- dependencies.set(dashboardViewModelToken, dashboardViewModelCreator);
- const diffViewModelCreator = () => new DiffViewModel();
- dependencies.set(diffViewModelToken, diffViewModelCreator);
- const documentationViewModelCreator = () => new DocumentationViewModel();
- dependencies.set(documentationViewModelToken, documentationViewModelCreator);
- const editViewModelCreator = () => new EditViewModel();
- dependencies.set(editViewModelToken, editViewModelCreator);
- const groupViewModelCreator = () => new GroupViewModel();
- dependencies.set(groupViewModelToken, groupViewModelCreator);
- const pluginViewModelCreator = () => new PluginViewModel();
- dependencies.set(pluginViewModelToken, pluginViewModelCreator);
- const repoViewModelCreator = () => new RepoViewModel();
- dependencies.set(repoViewModelToken, repoViewModelCreator);
- const searchViewModelCreator = () =>
- new SearchViewModel(appContext.restApiService, appContext.userModel, () =>
- resolver(navigationToken)
- );
- dependencies.set(searchViewModelToken, searchViewModelCreator);
- const settingsViewModelCreator = () => new SettingsViewModel();
- dependencies.set(settingsViewModelToken, settingsViewModelCreator);
-
- const routerCreator = () =>
- new GrRouter(
- appContext.reportingService,
- appContext.routerModel,
- appContext.restApiService,
- resolver(adminViewModelToken),
- resolver(agreementViewModelToken),
- resolver(changeViewModelToken),
- resolver(dashboardViewModelToken),
- resolver(diffViewModelToken),
- resolver(documentationViewModelToken),
- resolver(editViewModelToken),
- resolver(groupViewModelToken),
- resolver(pluginViewModelToken),
- resolver(repoViewModelToken),
- resolver(searchViewModelToken),
- resolver(settingsViewModelToken)
- );
- dependencies.set(routerToken, routerCreator);
+ const dependencies = createAppDependencies(appContext, resolver);
+ dependencies.set(storageServiceToken, () => grStorageMock);
dependencies.set(navigationToken, () => {
return {
setUrl: () => {},
@@ -172,55 +56,9 @@
finalize: () => {},
};
});
-
- const changeModelCreator = () =>
- new ChangeModel(
- appContext.routerModel,
- appContext.restApiService,
- appContext.userModel
- );
- dependencies.set(changeModelToken, changeModelCreator);
-
- const accountsModelCreator = () =>
- new AccountsModel(appContext.restApiService);
- dependencies.set(accountsModelToken, accountsModelCreator);
-
- const commentsModelCreator = () =>
- new CommentsModel(
- appContext.routerModel,
- resolver(changeModelToken),
- resolver(accountsModelToken),
- appContext.restApiService,
- appContext.reportingService
- );
- dependencies.set(commentsModelToken, commentsModelCreator);
-
- const filesModelCreator = () =>
- new FilesModel(
- resolver(changeModelToken),
- resolver(commentsModelToken),
- appContext.restApiService
- );
- dependencies.set(filesModelToken, filesModelCreator);
-
- const configModelCreator = () =>
- new ConfigModel(resolver(changeModelToken), appContext.restApiService);
- dependencies.set(configModelToken, configModelCreator);
-
- const checksModelCreator = () =>
- new ChecksModel(
- appContext.routerModel,
- resolver(changeViewModelToken),
- resolver(changeModelToken),
- appContext.reportingService,
- appContext.pluginsModel
- );
-
- dependencies.set(checksModelToken, checksModelCreator);
-
- const shortcutServiceCreator = () =>
- new ShortcutsService(appContext.userModel, appContext.reportingService);
- dependencies.set(shortcutsServiceToken, shortcutServiceCreator);
-
+ dependencies.set(
+ highlightServiceToken,
+ () => new MockHighlightService(appContext.reportingService)
+ );
return dependencies;
}
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index d6ad434..d7b178a 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -9,16 +9,13 @@
import {getAppContext} from '../services/app-context';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {SinonSpy, SinonStub} from 'sinon';
-import {StorageService} from '../services/storage/gr-storage';
import {AuthService} from '../services/gr-auth/gr-auth';
import {ReportingService} from '../services/gr-reporting/gr-reporting';
-import {UserModel} from '../models/user/user-model';
import {queryAndAssert, query} from '../utils/common-util';
import {FlagsService} from '../services/flags/flags';
-import {Key, Modifier} from '../utils/dom-util';
+import {Key, Modifier, whenVisible} from '../utils/dom-util';
import {Observable} from 'rxjs';
import {filter, take, timeout} from 'rxjs/operators';
-import {HighlightService} from '../services/highlight/highlight-service';
import {assert} from '@open-wc/testing';
export {query, queryAll, queryAndAssert} from '../utils/common-util';
@@ -109,24 +106,6 @@
return sinon.spy(getAppContext().restApiService, method);
}
-export function stubUsers<K extends keyof UserModel>(method: K) {
- return sinon.stub(getAppContext().userModel, method);
-}
-
-export function stubHighlightService<K extends keyof HighlightService>(
- method: K
-) {
- return sinon.stub(getAppContext().highlightService, method);
-}
-
-export function stubStorage<K extends keyof StorageService>(method: K) {
- return sinon.stub(getAppContext().storageService, method);
-}
-
-export function spyStorage<K extends keyof StorageService>(method: K) {
- return sinon.spy(getAppContext().storageService, method);
-}
-
export function stubAuth<K extends keyof AuthService>(method: K) {
return sinon.stub(getAppContext().authService, method);
}
@@ -220,6 +199,12 @@
});
}
+export async function waitUntilVisible(element: Element): Promise<void> {
+ return new Promise(resolve => {
+ whenVisible(element, () => resolve());
+ });
+}
+
export function waitUntilCalled(stub: SinonStub | SinonSpy, name: string) {
return waitUntil(() => stub.called, `${name} was not called`);
}
diff --git a/polygerrit-ui/app/workers/service-worker-class.ts b/polygerrit-ui/app/workers/service-worker-class.ts
index 218744d..ee85c0e 100644
--- a/polygerrit-ui/app/workers/service-worker-class.ts
+++ b/polygerrit-ui/app/workers/service-worker-class.ts
@@ -133,6 +133,7 @@
// TODO(milutin): Add gerrit host icon
this.ctx.registration.showNotification(change.subject, {body, data});
+ this.sendReport('notify about 1 change');
}
private showNotificationForDashboard(numOfChangesToNotifyAbout: number) {
@@ -140,6 +141,7 @@
const dashboardUrl = createDashboardUrl({});
const data = {url: `${self.location.origin}${dashboardUrl}`};
this.ctx.registration.showNotification(title, {data});
+ this.sendReport(`notify about ${numOfChangesToNotifyAbout} changes`);
}
// private but used in test
@@ -154,6 +156,7 @@
const prevLatestUpdateTimestampMs = this.latestUpdateTimestampMs;
this.latestUpdateTimestampMs = Date.now();
await this.saveState();
+ this.sendReport('polling');
const changes = await this.getLatestAttentionSetChanges();
const latestAttentionChanges = filterAttentionChangesAfter(
changes,
@@ -173,4 +176,19 @@
const changes = payload.parsed as unknown as ParsedChangeInfo[] | undefined;
return changes ?? [];
}
+
+ /**
+ * Send report event to 1 client (last focused one). The client will use
+ * gr-reporting service to send event to metric event collectors.
+ */
+ async sendReport(eventName: string) {
+ const clientsArr = await this.ctx.clients.matchAll({type: 'window'});
+ const lastFocusedClient = clientsArr?.[0];
+ if (!lastFocusedClient) return;
+
+ lastFocusedClient.postMessage({
+ type: ServiceWorkerMessageType.REPORTING,
+ eventName,
+ });
+ }
}
diff --git a/tools/BUILD b/tools/BUILD
index 3665bc2..e25dcc5 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -428,6 +428,7 @@
"-Xep:WrongOneof:ERROR",
"-Xep:XorPower:ERROR",
"-Xep:ZoneIdOfZ:ERROR",
+ "-Xlint:unchecked",
],
packages = ["error_prone_packages"],
)