Merge "Replace `GerritNav.navigateToChange()` by new `setUrl()` service"
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index 6305292..cee0fa4 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -10,7 +10,7 @@
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-select/gr-select';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   RepoName,
   BranchName,
@@ -28,6 +28,7 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
 import {resolve} from '../../../models/dependency';
+import {createChangeUrl} from '../../../models/views/change';
 
 const SUGGESTIONS_LIMIT = 15;
 const REF_PREFIX = 'refs/heads/';
@@ -71,6 +72,8 @@
 
   private readonly configModel = resolve(this, configModelToken);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     this.query = (input: string) => this.getRepoBranchesSuggestions(input);
@@ -223,9 +226,9 @@
         this.baseChange,
         this.baseCommit || undefined
       )
-      .then(changeCreated => {
-        if (!changeCreated) return;
-        GerritNav.navigateToChange(changeCreated);
+      .then(change => {
+        if (!change) return;
+        this.getNavigation().setUrl(createChangeUrl({change}));
       });
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index d47926b..21ab184 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -5,7 +5,7 @@
  */
 import '../gr-access-section/gr-access-section';
 import {encodeURL, getBaseUrl, singleDecodeURL} from '../../../utils/url-util';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {toSortedPermissionsArray} from '../../../utils/access-util';
 import {
   RepoName,
@@ -41,6 +41,8 @@
 import {assertIsDefined} from '../../../utils/common-util';
 import {ValueChangedEvent} from '../../../types/events';
 import {ifDefined} from 'lit/directives/if-defined.js';
+import {resolve} from '../../../models/dependency';
+import {createChangeUrl} from '../../../models/views/change';
 
 const NOTHING_TO_SAVE = 'No changes to save.';
 
@@ -109,6 +111,8 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     this.query = () => this.getInheritFromSuggestions();
@@ -692,7 +696,7 @@
     return this.restApiService
       .setRepoAccessRightsForReview(this.repo, obj)
       .then(change => {
-        GerritNav.navigateToChange(change);
+        this.getNavigation().setUrl(createChangeUrl({change}));
       })
       .finally(() => {
         this.modified = false;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
index 9fd2a54..85d5c21 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-repo-access';
 import {GrRepoAccess} from './gr-repo-access';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {toSortedPermissionsArray} from '../../../utils/access-util';
 import {
   addListenerForTest,
@@ -32,6 +32,7 @@
 import {GrPermission} from '../gr-permission/gr-permission';
 import {createChange} from '../../../test/test-data-generators';
 import {fixture, html, assert} from '@open-wc/testing';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-repo-access tests', () => {
   let element: GrRepoAccess;
@@ -1440,7 +1441,7 @@
       stubRestApi('getRepoAccessRights').returns(
         Promise.resolve(JSON.parse(JSON.stringify(accessRes)))
       );
-      const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+      const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
       let resolver: (value: Response | PromiseLike<Response>) => void;
       const saveStub = stubRestApi('setRepoAccessRights').returns(
         new Promise(r => (resolver = r))
@@ -1459,7 +1460,7 @@
       resolver!({status: 200} as Response);
       await element.updateComplete;
       assert.isTrue(saveStub.called);
-      assert.isTrue(navigateToChangeStub.notCalled);
+      assert.isTrue(setUrlStub.notCalled);
     });
 
     test('handleSaveForReview', async () => {
@@ -1490,7 +1491,7 @@
       stubRestApi('getRepoAccessRights').returns(
         Promise.resolve(JSON.parse(JSON.stringify(accessRes)))
       );
-      const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+      const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
       let resolver: (value: ChangeInfo | PromiseLike<ChangeInfo>) => void;
       const saveForReviewStub = stubRestApi(
         'setRepoAccessRightsForReview'
@@ -1511,8 +1512,9 @@
       resolver!(createChange());
       await element.updateComplete;
       assert.isTrue(saveForReviewStub.called);
+      assert.isTrue(setUrlStub.called);
       assert.isTrue(
-        navigateToChangeStub.lastCall.calledWithExactly(createChange())
+        setUrlStub.lastCall.args?.[0]?.includes(`${createChange()._number}`)
       );
     });
   });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 195070f..69b5a1f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -15,7 +15,7 @@
 import '../gr-change-list-column-requirements-summary/gr-change-list-column-requirements-summary';
 import '../gr-change-list-column-requirement/gr-change-list-column-requirement';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {getDisplayName} from '../../../utils/display-name-util';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
@@ -114,6 +114,8 @@
 
   private readonly getBulkActionsModel = resolve(this, bulkActionsModelToken);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     subscribe(
@@ -645,7 +647,7 @@
     // gr-change-list-item such as account links, which will bubble through
     // without triggering this extra navigation.
     if (this.change && e.composedPath()[0] === this) {
-      GerritNav.navigateToChange(this.change);
+      this.getNavigation().setUrl(createChangeUrl({change: this.change}));
     }
   };
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index 5e9d040..cec1dee 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -32,7 +32,7 @@
   TopicName,
 } from '../../../types/common';
 import {StandardLabels} from '../../../utils/label-util';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import './gr-change-list-item';
 import {GrChangeListItem} from './gr-change-list-item';
 import {
@@ -45,6 +45,7 @@
 } from '../../../models/bulk-actions/bulk-actions-model';
 import {createTestAppContext} from '../../../test/test-app-context-init';
 import {ColumnNames} from '../../../constants/constants';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-change-list-item tests', () => {
   const account = createAccountWithId();
@@ -309,7 +310,7 @@
   });
 
   test('clicking item navigates to change', async () => {
-    const navStub = sinon.stub(GerritNav);
+    const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
 
     element.change = change;
     await element.updateComplete;
@@ -317,7 +318,8 @@
     element.click();
     await element.updateComplete;
 
-    assert.isTrue(navStub.navigateToChange.calledWithExactly(change));
+    assert.isTrue(setUrlStub.called);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/a/test/repo/+/42');
   });
 
   test('renders', async () => {
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 937276d..0ac9903 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
@@ -7,7 +7,7 @@
 import '../gr-repo-header/gr-repo-header';
 import '../gr-user-header/gr-user-header';
 import {page} from '../../../utils/page-wrapper-utils';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   AccountDetailInfo,
   AccountId,
@@ -30,6 +30,7 @@
 } from '../../../models/views/search';
 import {resolve} from '../../../models/dependency';
 import {subscribe} from '../../lit/subscription-controller';
+import {createChangeUrl} from '../../../models/views/change';
 
 const LOOKUP_QUERY_PATTERNS: RegExp[] = [
   /^\s*i?[0-9a-f]{7,40}\s*$/i, // CHANGE_ID
@@ -111,6 +112,8 @@
 
   private readonly getViewModel = resolve(this, searchViewModelToken);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     this.addEventListener('next-page', () => this.handleNextPage());
@@ -302,11 +305,10 @@
       if (this.query && changes.length === 1) {
         for (const queryPattern of LOOKUP_QUERY_PATTERNS) {
           if (this.query.match(queryPattern)) {
-            // "Back"/"Forward" buttons work correctly only with
-            // opt_redirect options
-            GerritNav.navigateToChange(changes[0], {
-              redirect: true,
-            });
+            // "Back"/"Forward" buttons work correctly only with replaceUrl()
+            this.getNavigation().replaceUrl(
+              createChangeUrl({change: changes[0]})
+            );
             return;
           }
         }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
index f88f668..860f8a3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
@@ -7,9 +7,8 @@
 import './gr-change-list-view';
 import {GrChangeListView} from './gr-change-list-view';
 import {page} from '../../../utils/page-wrapper-utils';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
-  mockPromise,
   query,
   stubRestApi,
   queryAndAssert,
@@ -24,6 +23,8 @@
 } from '../../../api/rest-api';
 import {fixture, html, waitUntil, assert} from '@open-wc/testing';
 import {GerritView} from '../../../services/router/router-model';
+import {testResolver} from '../../../test/common-test-setup';
+import {SinonStub} from 'sinon';
 
 const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
 const COMMIT_HASH = '12345678';
@@ -275,7 +276,10 @@
   });
 
   suite('query based navigation', () => {
-    setup(() => {});
+    let replaceUrlStub: SinonStub;
+    setup(() => {
+      replaceUrlStub = sinon.stub(testResolver(navigationToken), 'replaceUrl');
+    });
 
     teardown(async () => {
       await element.updateComplete;
@@ -285,82 +289,52 @@
     test('Searching for a change ID redirects to change', async () => {
       const change = {...createChange(), _number: 1 as NumericChangeId};
       sinon.stub(element, 'getChanges').returns(Promise.resolve([change]));
-      const promise = mockPromise();
-      sinon.stub(GerritNav, 'navigateToChange').callsFake((url, opt) => {
-        assert.equal(url, change);
-        // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
-        assert.isTrue(opt!.redirect);
-        promise.resolve();
-      });
 
-      element.viewState = {
-        view: GerritView.SEARCH,
-        query: CHANGE_ID,
-        offset: '',
-      };
-      await promise;
+      element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
+      await element.updateComplete;
+
+      assert.isTrue(replaceUrlStub.called);
+      assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
     });
 
     test('Searching for a change num redirects to change', async () => {
       const change = {...createChange(), _number: 1 as NumericChangeId};
       sinon.stub(element, 'getChanges').returns(Promise.resolve([change]));
-      const promise = mockPromise();
-      sinon.stub(GerritNav, 'navigateToChange').callsFake((url, opt) => {
-        assert.equal(url, change);
-        // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
-        assert.isTrue(opt!.redirect);
-        promise.resolve();
-      });
 
-      element.viewState = {view: GerritView.SEARCH, query: '1', offset: ''};
-      await promise;
+      element.viewState = {view: GerritView.SEARCH, query: '1'};
+      await element.updateComplete;
+
+      assert.isTrue(replaceUrlStub.called);
+      assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
     });
 
     test('Commit hash redirects to change', async () => {
       const change = {...createChange(), _number: 1 as NumericChangeId};
       sinon.stub(element, 'getChanges').returns(Promise.resolve([change]));
-      const promise = mockPromise();
-      sinon.stub(GerritNav, 'navigateToChange').callsFake((url, opt) => {
-        assert.equal(url, change);
-        // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
-        assert.isTrue(opt!.redirect);
-        promise.resolve();
-      });
 
-      element.viewState = {
-        view: GerritView.SEARCH,
-        query: COMMIT_HASH,
-        offset: '',
-      };
-      await promise;
+      element.viewState = {view: GerritView.SEARCH, query: COMMIT_HASH};
+      await element.updateComplete;
+
+      assert.isTrue(replaceUrlStub.called);
+      assert.equal(replaceUrlStub.lastCall.firstArg, '/c/test-project/+/1');
     });
 
     test('Searching for an invalid change ID searches', async () => {
       sinon.stub(element, 'getChanges').returns(Promise.resolve([]));
-      const stub = sinon.stub(GerritNav, 'navigateToChange');
 
-      element.viewState = {
-        view: GerritView.SEARCH,
-        query: CHANGE_ID,
-        offset: '',
-      };
+      element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
       await element.updateComplete;
 
-      assert.isFalse(stub.called);
+      assert.isFalse(replaceUrlStub.called);
     });
 
     test('Change ID with multiple search results searches', async () => {
       sinon.stub(element, 'getChanges').returns(Promise.resolve(undefined));
-      const stub = sinon.stub(GerritNav, 'navigateToChange');
 
-      element.viewState = {
-        view: GerritView.SEARCH,
-        query: CHANGE_ID,
-        offset: '',
-      };
+      element.viewState = {view: GerritView.SEARCH, query: CHANGE_ID};
       await element.updateComplete;
 
-      assert.isFalse(stub.called);
+      assert.isFalse(replaceUrlStub.called);
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 343dee2..fb7b354 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -9,7 +9,7 @@
 import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import {getAppContext} from '../../../services/app-context';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
@@ -34,6 +34,8 @@
 import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
 import {Execution} from '../../../constants/reporting';
 import {ValueChangedEvent} from '../../../types/events';
+import {resolve} from '../../../models/dependency';
+import {createChangeUrl} from '../../../models/views/change';
 
 export interface ChangeListSection {
   countLabel?: string;
@@ -143,6 +145,8 @@
 
   private readonly shortcuts = new ShortcutController(this);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   private cursor = new GrCursorManager();
 
   constructor() {
@@ -388,7 +392,7 @@
 
   private async openChange() {
     const change = await this.changeForIndex(this.selectedIndex);
-    if (change) GerritNav.navigateToChange(change);
+    if (change) this.getNavigation().setUrl(createChangeUrl({change}));
   }
 
   private nextPage() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
index dbb436f..a15da85 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-change-list';
 import {GrChangeList, computeRelativeIndex} from './gr-change-list';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   pressKey,
   query,
@@ -31,6 +31,7 @@
 import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
 import {fixture, assert} from '@open-wc/testing';
 import {html} from 'lit';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-change-list basic tests', () => {
   let element: GrChangeList;
@@ -224,16 +225,11 @@
     assert.equal(element.selectedIndex, 2);
     assert.isTrue(elementItems[2].selected);
 
-    const navStub = sinon.stub(GerritNav, 'navigateToChange');
+    const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
     assert.equal(element.selectedIndex, 2);
     pressKey(element, Key.ENTER);
-    await waitUntil(() => navStub.callCount >= 1);
-    await element.updateComplete;
-    assert.deepEqual(
-      navStub.lastCall.args[0],
-      {...createChange(), _number: 2 as NumericChangeId},
-      'Should navigate to /c/2/'
-    );
+    await waitUntil(() => setUrlStub.callCount >= 1);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/2');
 
     pressKey(element, 'k');
     await element.updateComplete;
@@ -241,15 +237,11 @@
 
     assert.equal(element.selectedIndex, 1);
 
-    const prevCount = navStub.callCount;
+    const prevCount = setUrlStub.callCount;
     pressKey(element, Key.ENTER);
 
-    await waitUntil(() => navStub.callCount > prevCount);
-    assert.deepEqual(
-      navStub.lastCall.args[0],
-      {...createChange(), _number: 1 as NumericChangeId},
-      'Should navigate to /c/1/'
-    );
+    await waitUntil(() => setUrlStub.callCount > prevCount);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/1');
 
     pressKey(element, 'k');
     pressKey(element, 'k');
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 9d2c458..c5a007e 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
@@ -110,6 +110,7 @@
 import {Interaction} from '../../../constants/reporting';
 import {rootUrl} from '../../../utils/url-util';
 import {createSearchUrl} from '../../../models/views/search';
+import {createChangeUrl} from '../../../models/views/change';
 
 const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
 const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -1845,14 +1846,18 @@
           this.waitForChangeReachable(revertChangeInfo._number)
             .then(() => this.setReviewOnRevert(revertChangeInfo._number))
             .then(() => {
-              GerritNav.navigateToChange(revertChangeInfo);
+              this.getNavigation().setUrl(
+                createChangeUrl({change: revertChangeInfo})
+              );
             });
           break;
         }
         case RevisionActions.CHERRYPICK: {
           const cherrypickChangeInfo: ChangeInfo = obj as unknown as ChangeInfo;
           this.waitForChangeReachable(cherrypickChangeInfo._number).then(() => {
-            GerritNav.navigateToChange(cherrypickChangeInfo);
+            this.getNavigation().setUrl(
+              createChangeUrl({change: cherrypickChangeInfo})
+            );
           });
           break;
         }
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 5ae2733..1b98c5d 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
@@ -5,10 +5,7 @@
  */
 import '../../../test/common-test-setup';
 import './gr-change-actions';
-import {
-  GerritNav,
-  navigationToken,
-} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {
   createAccountWithId,
@@ -2460,7 +2457,6 @@
           sendStub = stubRestApi('executeChangeAction').returns(
             Promise.resolve(new Response())
           );
-          sinon.stub(GerritNav, 'navigateToChange');
         });
 
         test('change action', async () => {
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 75199d2..574991e 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
@@ -37,7 +37,10 @@
 import {GrEditConstants} from '../../edit/gr-edit-constants';
 import {pluralize} from '../../../utils/string-util';
 import {querySelectorAll, windowLocationReload} from '../../../utils/dom-util';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+  GerritNav,
+  navigationToken,
+} from '../../core/gr-navigation/gr-navigation';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
@@ -586,6 +589,8 @@
 
   private readonly shortcutsController = new ShortcutController(this);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     this.setupListeners();
@@ -2096,8 +2101,8 @@
    * anymore. The app element makes sure that an obsolete change view is not
    * shown anymore, so if the change view is still and doing some update to
    * itself, then that is not dangerous. But for example it should not call
-   * navigateToChange() anymore. That would very likely cause erroneous
-   * behavior.
+   * the navigation service's set/replaceUrl() methods anymore. That would very
+   * likely cause erroneous behavior.
    */
   private isChangeObsolete() {
     // While this.changeNum is undefined the change view is fresh and has just
@@ -2513,9 +2518,9 @@
       fireAlert(this, 'Base is already selected.');
       return;
     }
-    GerritNav.navigateToChange(this.change, {
-      patchNum: this.patchRange.patchNum,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({change: this.change, patchNum: this.patchRange.patchNum})
+    );
   }
 
   // Private but used in tests.
@@ -2527,9 +2532,12 @@
       fireAlert(this, 'Left is already base.');
       return;
     }
-    GerritNav.navigateToChange(this.change, {
-      patchNum: this.patchRange.basePatchNum as RevisionPatchSetNum,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({
+        change: this.change,
+        patchNum: this.patchRange.basePatchNum as RevisionPatchSetNum,
+      })
+    );
   }
 
   // Private but used in tests.
@@ -2541,10 +2549,13 @@
       fireAlert(this, 'Latest is already selected.');
       return;
     }
-    GerritNav.navigateToChange(this.change, {
-      patchNum: latestPatchNum,
-      basePatchNum: this.patchRange.basePatchNum,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({
+        change: this.change,
+        patchNum: latestPatchNum,
+        basePatchNum: this.patchRange.basePatchNum,
+      })
+    );
   }
 
   // Private but used in tests.
@@ -2556,10 +2567,13 @@
       fireAlert(this, 'Right is already latest.');
       return;
     }
-    GerritNav.navigateToChange(this.change, {
-      patchNum: latestPatchNum,
-      basePatchNum: this.patchRange.patchNum as BasePatchSetNum,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({
+        change: this.change,
+        patchNum: latestPatchNum,
+        basePatchNum: this.patchRange.patchNum as BasePatchSetNum,
+      })
+    );
   }
 
   // Private but used in tests.
@@ -2574,7 +2588,9 @@
       fireAlert(this, 'Already diffing base against latest.');
       return;
     }
-    GerritNav.navigateToChange(this.change, {patchNum: latestPatchNum});
+    this.getNavigation().setUrl(
+      createChangeUrl({change: this.change, patchNum: latestPatchNum})
+    );
   }
 
   private handleToggleChangeStar() {
@@ -2900,9 +2916,9 @@
   loadData(isLocationChange?: boolean, clearPatchset?: boolean) {
     if (this.isChangeObsolete()) return Promise.resolve();
     if (clearPatchset && this.change) {
-      GerritNav.navigateToChange(this.change, {
-        forceReload: true,
-      });
+      this.getNavigation().setUrl(
+        createChangeUrl({change: this.change, forceReload: true})
+      );
       return Promise.resolve();
     }
     this.loading = true;
@@ -3261,7 +3277,8 @@
     );
 
     if (editInfo) {
-      GerritNav.navigateToChange(this.change, {patchNum: EDIT});
+      const url = createChangeUrl({change: this.change, patchNum: EDIT});
+      this.getNavigation().setUrl(url);
       return;
     }
 
@@ -3274,20 +3291,26 @@
     ) {
       patchNum = this.patchRange.patchNum;
     }
-    GerritNav.navigateToChange(this.change, {
-      patchNum,
-      isEdit: true,
-      forceReload: true,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({
+        change: this.change,
+        patchNum,
+        edit: true,
+        forceReload: true,
+      })
+    );
   }
 
   private handleStopEditTap() {
     assertIsDefined(this.change, 'change');
     assertIsDefined(this.patchRange, 'patchRange');
-    GerritNav.navigateToChange(this.change, {
-      patchNum: this.patchRange.patchNum,
-      forceReload: true,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({
+        change: this.change,
+        patchNum: this.patchRange.patchNum,
+        forceReload: true,
+      })
+    );
   }
 
   private resetReplyOverlayFocusStops() {
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 0e6fb71..79a9dfe 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
@@ -19,7 +19,10 @@
 } from '../../../constants/constants';
 import {GrEditConstants} from '../../edit/gr-edit-constants';
 import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+  GerritNav,
+  navigationToken,
+} from '../../core/gr-navigation/gr-navigation';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {EventType, PluginApi} from '../../../api/plugin';
 import {
@@ -100,13 +103,11 @@
 import {GrCopyLinks} from '../gr-copy-links/gr-copy-links';
 import {ChangeViewState} from '../../../models/views/change';
 import {rootUrl} from '../../../utils/url-util';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-change-view tests', () => {
   let element: GrChangeView;
-
-  let navigateToChangeStub: SinonStubbedMember<
-    typeof GerritNav.navigateToChange
-  >;
+  let setUrlStub: sinon.SinonStub;
 
   const ROBOT_COMMENTS_LIMIT = 10;
 
@@ -331,7 +332,7 @@
   setup(async () => {
     // Since pluginEndpoints are global, must reset state.
     _testOnly_resetEndpoints();
-    navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+    setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
 
     stubRestApi('getConfig').returns(
       Promise.resolve({
@@ -585,10 +586,8 @@
       basePatchNum: 1 as BasePatchSetNum,
     };
     element.handleDiffAgainstBase();
-    assert(navigateToChangeStub.called);
-    const args = navigateToChangeStub.getCall(0).args;
-    assert.equal(args[0], element.change);
-    assert.equal(args[1]!.patchNum, 3 as RevisionPatchSetNum);
+    assert.isTrue(setUrlStub.called);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/3');
   });
 
   test('handleDiffAgainstLatest', () => {
@@ -601,11 +600,8 @@
       patchNum: 3 as RevisionPatchSetNum,
     };
     element.handleDiffAgainstLatest();
-    assert(navigateToChangeStub.called);
-    const args = navigateToChangeStub.getCall(0).args;
-    assert.equal(args[0], element.change);
-    assert.equal(args[1]!.patchNum, 10 as RevisionPatchSetNum);
-    assert.equal(args[1]!.basePatchNum, 1 as BasePatchSetNum);
+    assert.isTrue(setUrlStub.called);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1..10');
   });
 
   test('handleDiffBaseAgainstLeft', () => {
@@ -618,10 +614,8 @@
       basePatchNum: 1 as BasePatchSetNum,
     };
     element.handleDiffBaseAgainstLeft();
-    assert(navigateToChangeStub.called);
-    const args = navigateToChangeStub.getCall(0).args;
-    assert.equal(args[0], element.change);
-    assert.equal(args[1]!.patchNum, 1 as RevisionPatchSetNum);
+    assert.isTrue(setUrlStub.called);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
   });
 
   test('handleDiffRightAgainstLatest', () => {
@@ -634,10 +628,8 @@
       patchNum: 3 as RevisionPatchSetNum,
     };
     element.handleDiffRightAgainstLatest();
-    assert(navigateToChangeStub.called);
-    const args = navigateToChangeStub.getCall(0).args;
-    assert.equal(args[1]!.patchNum, 10 as RevisionPatchSetNum);
-    assert.equal(args[1]!.basePatchNum, 3 as BasePatchSetNum);
+    assert.isTrue(setUrlStub.called);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/3..10');
   });
 
   test('handleDiffBaseAgainstLatest', () => {
@@ -650,10 +642,8 @@
       patchNum: 3 as RevisionPatchSetNum,
     };
     element.handleDiffBaseAgainstLatest();
-    assert(navigateToChangeStub.called);
-    const args = navigateToChangeStub.getCall(0).args;
-    assert.equal(args[1]!.patchNum, 10 as RevisionPatchSetNum);
-    assert.isNotOk(args[1]!.basePatchNum);
+    assert.isTrue(setUrlStub.called);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/10');
   });
 
   test('toggle attention set status', async () => {
@@ -1710,7 +1700,7 @@
     });
 
     await element.loadData(true);
-    assert.isFalse(navigateToChangeStub.called);
+    assert.isFalse(setUrlStub.called);
     assert.isTrue(reloadStub.called);
   });
 
@@ -2290,7 +2280,6 @@
         assertIsDefined(element.actions);
         element.actions.dispatchEvent(new CustomEvent('edit-tap'));
       };
-      navigateToChangeStub.restore();
 
       element.change = {
         ...createChangeViewChange(),
@@ -2299,13 +2288,6 @@
     });
 
     test('edit exists in revisions', async () => {
-      const promise = mockPromise();
-      sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
-        assert.equal(args.length, 2);
-        assert.equal(args[1]!.patchNum, EDIT); // patchNum
-        promise.resolve();
-      });
-
       assertIsDefined(element.change);
       const newChange = {...element.change};
       newChange.revisions.rev2 = createRevision(EDIT);
@@ -2313,18 +2295,11 @@
       await element.updateComplete;
 
       fireEdit();
-      await promise;
+      assert.isTrue(setUrlStub.called);
+      assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/edit');
     });
 
     test('no edit exists in revisions, non-latest patchset', async () => {
-      const promise = mockPromise();
-      sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
-        assert.equal(args.length, 2);
-        assert.equal(args[1]!.patchNum, 1 as RevisionPatchSetNum); // patchNum
-        assert.equal(args[1]!.isEdit, true); // opt_isEdit
-        promise.resolve();
-      });
-
       assertIsDefined(element.change);
       const newChange = {...element.change};
       newChange.revisions.rev2 = createRevision(2);
@@ -2333,19 +2308,14 @@
       await element.updateComplete;
 
       fireEdit();
-      await promise;
+      assert.isTrue(setUrlStub.called);
+      assert.equal(
+        setUrlStub.lastCall.firstArg,
+        '/c/test-project/+/42/1,edit?forceReload=true'
+      );
     });
 
     test('no edit exists in revisions, latest patchset', async () => {
-      const promise = mockPromise();
-      sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
-        assert.equal(args.length, 2);
-        // No patch should be specified when patchNum == latest.
-        assert.isNotOk(args[1]!.patchNum); // patchNum
-        assert.equal(args[1]!.isEdit, true); // opt_isEdit
-        promise.resolve();
-      });
-
       assertIsDefined(element.change);
       const newChange = {...element.change};
       newChange.revisions.rev2 = createRevision(2);
@@ -2354,7 +2324,11 @@
       await element.updateComplete;
 
       fireEdit();
-      await promise;
+      assert.isTrue(setUrlStub.called);
+      assert.equal(
+        setUrlStub.lastCall.firstArg,
+        '/c/test-project/+/42,edit?forceReload=true'
+      );
     });
   });
 
@@ -2366,19 +2340,17 @@
     assertIsDefined(element.metadata);
     assertIsDefined(element.actions);
     sinon.stub(element.metadata, 'computeLabelNames');
-    navigateToChangeStub.restore();
-    const promise = mockPromise();
-    sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
-      assert.equal(args.length, 2);
-      assert.equal(args[1]!.patchNum, 1 as RevisionPatchSetNum); // patchNum
-      promise.resolve();
-    });
 
     element.patchRange = {patchNum: 1 as RevisionPatchSetNum};
     element.actions.dispatchEvent(
       new CustomEvent('stop-edit-tap', {bubbles: false})
     );
-    await promise;
+
+    assert.isTrue(setUrlStub.called);
+    assert.equal(
+      setUrlStub.lastCall.firstArg,
+      '/c/test-project/+/42/1?forceReload=true'
+    );
   });
 
   suite('plugin endpoints', () => {
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 bbebf10..832738b 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
@@ -11,7 +11,7 @@
 import '../../shared/gr-icon/gr-icon';
 import '../gr-commit-info/gr-commit-info';
 import {FilesExpandedState} from '../gr-file-list-constants';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {computeLatestPatchNum, PatchSet} from '../../../utils/patch-set-util';
 import {property, customElement, query, state} from 'lit/decorators.js';
 import {
@@ -41,6 +41,7 @@
 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';
 
 @customElement('gr-file-list-header')
 export class GrFileListHeader extends LitElement {
@@ -124,6 +125,8 @@
 
   private readonly userModel = getAppContext().userModel;
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     subscribe(
@@ -421,7 +424,9 @@
     ) {
       return;
     }
-    GerritNav.navigateToChange(this.change, {patchNum, basePatchNum});
+    this.getNavigation().setUrl(
+      createChangeUrl({change: this.change, patchNum, basePatchNum})
+    );
   }
 
   private handlePrefsTap(e: Event) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
index 030934e..7b79893 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-file-list-header';
 import {FilesExpandedState} from '../gr-file-list-constants';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {createChange, createRevision} from '../../../test/test-data-generators';
 import {
   isVisible,
@@ -22,13 +22,13 @@
   PARENT,
   PatchSetNum,
   PatchSetNumber,
-  RevisionPatchSetNum,
 } from '../../../types/common';
 import {ChangeInfo, ChangeStatus} from '../../../api/rest-api';
 import {PatchSet} from '../../../utils/patch-set-util';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
 import {fixture, html, assert} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-file-list-header tests', () => {
   let element: GrFileListHeader;
@@ -238,8 +238,8 @@
     assert.equal(getComputedStyle(collapseBtn).display, 'none');
   });
 
-  test('navigateToChange called when range select changes', async () => {
-    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+  test('setUrl called when range select changes', async () => {
+    const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
     element.basePatchNum = 1 as BasePatchSetNum;
     element.patchNum = 2 as PatchSetNum;
     await element.updateComplete;
@@ -249,13 +249,8 @@
     } as CustomEvent);
     await element.updateComplete;
 
-    assert.equal(navigateToChangeStub.callCount, 1);
-    assert.isTrue(
-      navigateToChangeStub.lastCall.calledWithExactly(change, {
-        patchNum: 3 as RevisionPatchSetNum,
-        basePatchNum: 1 as BasePatchSetNum,
-      })
-    );
+    assert.equal(setUrlStub.callCount, 1);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1..3');
   });
 
   test('class is applied to file list on old patch set', () => {
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 d2b840f..5daa3d2 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
@@ -21,7 +21,10 @@
 import {asyncForeach} from '../../../utils/async-util';
 import {FilesExpandedState} from '../gr-file-list-constants';
 import {diffFilePaths, pluralize} from '../../../utils/string-util';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+  GerritNav,
+  navigationToken,
+} from '../../core/gr-navigation/gr-navigation';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {getAppContext} from '../../../services/app-context';
@@ -76,6 +79,7 @@
 import {HtmlPatched} from '../../../utils/lit-util';
 import {createDiffUrl} from '../../../models/views/diff';
 import {createEditUrl} from '../../../models/views/edit';
+import {createChangeUrl} from '../../../models/views/change';
 
 export const DEFAULT_NUM_FILES_SHOWN = 200;
 
@@ -304,6 +308,8 @@
 
   shortcutsController = new ShortcutController(this);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   // private but used in test
   fileCursor = new GrCursorManager();
 
@@ -2169,10 +2175,13 @@
 
   private handleShowParent1(): void {
     if (!this.change || !this.patchRange) return;
-    GerritNav.navigateToChange(this.change, {
-      patchNum: this.patchRange.patchNum,
-      basePatchNum: -1 as BasePatchSetNum, // Parent 1
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({
+        change: this.change,
+        patchNum: this.patchRange.patchNum,
+        basePatchNum: -1 as BasePatchSetNum, // Parent 1
+      })
+    );
   }
 
   private computeFilesShown(): NormalizedFileInfo[] {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 04766bd..e11822f 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -38,7 +38,7 @@
 import {LABEL_TITLE_SCORE_PATTERN} from '../gr-message-scores/gr-message-scores';
 import {getAppContext} from '../../../services/app-context';
 import {pluralize} from '../../../utils/string-util';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   computeAllPatchSets,
   computeLatestPatchNum,
@@ -48,6 +48,8 @@
 import {assertIsDefined} from '../../../utils/common-util';
 import {when} from 'lit/directives/when.js';
 import {FormattedReviewerUpdateInfo} from '../../../types/types';
+import {resolve} from '../../../models/dependency';
+import {createChangeUrl} from '../../../models/views/change';
 
 const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
 const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
@@ -127,6 +129,8 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   // for COMMENTS_AUTOCLOSE logging purposes only
   readonly uid = performance.now().toString(36) + Math.random().toString(36);
 
@@ -623,7 +627,9 @@
       patchNum = computeLatestPatchNum(computeAllPatchSets(this.change))!;
       basePatchNum = computePredecessor(patchNum)!;
     }
-    GerritNav.navigateToChange(this.change, {patchNum, basePatchNum});
+    this.getNavigation().setUrl(
+      createChangeUrl({change: this.change, patchNum, basePatchNum})
+    );
     // stop propagation to stop message expansion
     e.stopPropagation();
   }
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index c55f380..34292d6 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -5,7 +5,7 @@
  */
 import '../../../test/common-test-setup';
 import './gr-message';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   createAccountWithIdNameAndEmail,
   createChange,
@@ -25,11 +25,9 @@
 import {GrMessage} from './gr-message';
 import {
   AccountId,
-  BasePatchSetNum,
   ChangeMessageId,
   EmailAddress,
   NumericChangeId,
-  PARENT,
   RevisionPatchSetNum,
   ReviewInputTag,
   Timestamp,
@@ -41,9 +39,10 @@
 } from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {CommentSide} from '../../../constants/constants';
-import {SinonStubbedMember} from 'sinon';
+import {SinonStub} from 'sinon';
 import {html} from 'lit';
 import {fixture, assert} from '@open-wc/testing';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-message tests', () => {
   let element: GrMessage;
@@ -424,10 +423,10 @@
     });
 
     suite('uploaded patchset X message navigates to X - 1 vs  X', () => {
-      let navStub: SinonStubbedMember<typeof GerritNav.navigateToChange>;
+      let setUrlStub: SinonStub;
       setup(() => {
         element.change = {...createChange(), revisions: createRevisions(4)};
-        navStub = sinon.stub(GerritNav, 'navigateToChange');
+        setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
       });
 
       test('Patchset 1 navigates to Base', () => {
@@ -436,12 +435,9 @@
           message: 'Uploaded patch set 1.',
         };
         element.handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(
-          navStub.calledWithExactly(element.change!, {
-            patchNum: 1 as RevisionPatchSetNum,
-            basePatchNum: PARENT,
-          })
-        );
+
+        assert.isTrue(setUrlStub.calledOnce);
+        assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
       });
 
       test('Patchset X navigates to X vs X - 1', () => {
@@ -450,23 +446,20 @@
           message: 'Uploaded patch set 2.',
         };
         element.handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(
-          navStub.calledWithExactly(element.change!, {
-            patchNum: 2 as RevisionPatchSetNum,
-            basePatchNum: 1 as BasePatchSetNum,
-          })
-        );
+
+        assert.isTrue(setUrlStub.calledOnce);
+        assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1..2');
 
         element.message = {
           ...createChangeMessage(),
           message: 'Uploaded patch set 200.',
         };
         element.handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(
-          navStub.calledWithExactly(element.change!, {
-            patchNum: 200 as RevisionPatchSetNum,
-            basePatchNum: 199 as BasePatchSetNum,
-          })
+
+        assert.isTrue(setUrlStub.calledTwice);
+        assert.equal(
+          setUrlStub.lastCall.firstArg,
+          '/c/test-project/+/42/199..200'
         );
       });
 
@@ -476,12 +469,9 @@
           message: 'Commit message updated.',
         };
         element.handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(
-          navStub.calledWithExactly(element.change!, {
-            patchNum: 4 as RevisionPatchSetNum,
-            basePatchNum: 3 as BasePatchSetNum,
-          })
-        );
+
+        assert.isTrue(setUrlStub.calledOnce);
+        assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/3..4');
       });
 
       test('Merged patchset change message', () => {
@@ -490,12 +480,9 @@
           message: 'abcd↵3 is the latest approved patch-set.↵abc',
         };
         element.handleViewPatchsetDiff(new MouseEvent('click'));
-        assert.isTrue(
-          navStub.calledWithExactly(element.change!, {
-            patchNum: 4 as RevisionPatchSetNum,
-            basePatchNum: 3 as BasePatchSetNum,
-          })
-        );
+
+        assert.isTrue(setUrlStub.calledOnce);
+        assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/3..4');
       });
     });
 
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 2db3fd2..69a7b1d 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -6,13 +6,10 @@
 import {
   BasePatchSetNum,
   ChangeInfo,
-  RepoName,
   RevisionPatchSetNum,
 } from '../../../types/common';
 import {ParsedChangeInfo} from '../../../types/types';
-import {createRepoUrl} from '../../../models/views/repo';
 import {createDiffUrl} from '../../../models/views/diff';
-import {createChangeUrl} from '../../../models/views/change';
 import {define} from '../../../models/dependency';
 
 const uninitialized = () => {
@@ -26,15 +23,6 @@
 
 export type NavigateCallback = (target: string, redirect?: boolean) => void;
 
-interface NavigateToChangeParams {
-  patchNum?: RevisionPatchSetNum;
-  basePatchNum?: BasePatchSetNum;
-  isEdit?: boolean;
-  redirect?: boolean;
-  forceReload?: boolean;
-  openReplyDialog?: boolean;
-}
-
 export const navigationToken = define<NavigationService>('navigation');
 
 export interface NavigationService {
@@ -81,40 +69,6 @@
 
   /**
    * @param basePatchNum The string PARENT can be used for none.
-   * @param redirect redirect to a change - if true, the current
-   *     location (i.e. page which makes redirect) is not added to a history.
-   *     I.e. back/forward buttons skip current location
-   * @param forceReload Some views are smart about how to handle the reload
-   *     of the view. In certain cases we want to force the view to reload
-   *     and re-render everything.
-   */
-  navigateToChange(
-    change: Pick<ChangeInfo, '_number' | 'project'>,
-    options: NavigateToChangeParams = {}
-  ) {
-    const {
-      patchNum,
-      basePatchNum,
-      isEdit,
-      forceReload,
-      redirect,
-      openReplyDialog,
-    } = options;
-    this._navigate(
-      createChangeUrl({
-        change,
-        patchNum,
-        basePatchNum,
-        edit: isEdit,
-        forceReload,
-        openReplyDialog,
-      }),
-      redirect
-    );
-  },
-
-  /**
-   * @param basePatchNum The string PARENT can be used for none.
    */
   navigateToDiff(
     change: ChangeInfo | ParsedChangeInfo,
@@ -144,11 +98,4 @@
     }
     this._navigate(relativeUrl);
   },
-
-  /**
-   * Navigate to a repo settings page.
-   */
-  navigateToRepo(repo: RepoName) {
-    this._navigate(createRepoUrl({repo}));
-  },
 };
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 f2fa44c..262f1fd 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
@@ -8,7 +8,7 @@
 import '../../shared/gr-icon/gr-icon';
 import '../../shared/gr-overlay/gr-overlay';
 import '../../../embed/diff/gr-diff/gr-diff';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {
   NumericChangeId,
   EDIT,
@@ -31,6 +31,8 @@
 import {sharedStyles} from '../../../styles/shared-styles';
 import {subscribe} from '../../lit/subscription-controller';
 import {assert} from '../../../utils/common-util';
+import {resolve} from '../../../models/dependency';
+import {createChangeUrl} from '../../../models/views/change';
 
 interface FilePreview {
   filepath: string;
@@ -79,6 +81,8 @@
 
   private readonly userModel = getAppContext().userModel;
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     subscribe(
@@ -338,10 +342,13 @@
       );
     }
     if (res && res.ok) {
-      GerritNav.navigateToChange(change, {
-        patchNum: EDIT,
-        basePatchNum: patchNum as BasePatchSetNum,
-      });
+      this.getNavigation().setUrl(
+        createChangeUrl({
+          change,
+          patchNum: EDIT,
+          basePatchNum: patchNum as BasePatchSetNum,
+        })
+      );
       this.close(true);
     }
     this.isApplyFixLoading = false;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index 47ac64e..b6e4a95 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -5,10 +5,10 @@
  */
 import '../../../test/common-test-setup';
 import './gr-apply-fix-dialog';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
 import {GrApplyFixDialog} from './gr-apply-fix-dialog';
-import {BasePatchSetNum, EDIT, PatchSetNum} from '../../../types/common';
+import {PatchSetNum} from '../../../types/common';
 import {
   createFixSuggestionInfo,
   createParsedChange,
@@ -24,9 +24,12 @@
 } from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {fixture, html, assert} from '@open-wc/testing';
+import {SinonStub} from 'sinon';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-apply-fix-dialog tests', () => {
   let element: GrApplyFixDialog;
+  let setUrlStub: SinonStub;
 
   const TWO_FIXES: OpenFixPreviewEventDetail = {
     patchNum: 2 as PatchSetNum,
@@ -58,6 +61,7 @@
   }
 
   setup(async () => {
+    setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
     element = await fixture<GrApplyFixDialog>(
       html`<gr-apply-fix-dialog></gr-apply-fix-dialog>`
     );
@@ -238,7 +242,6 @@
     const applyRobotFixSuggestionStub = stubRestApi(
       'applyRobotFixSuggestion'
     ).returns(Promise.resolve(new Response(null, {status: 200})));
-    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
     element.currentFix = createFixSuggestionInfo('123');
 
     const closeFixPreviewEventSpy = sinon.spy();
@@ -247,6 +250,7 @@
       EventType.CLOSE_FIX_PREVIEW,
       closeFixPreviewEventSpy
     );
+
     await element.handleApplyFix(new CustomEvent('confirm'));
 
     sinon.assert.calledOnceWithExactly(
@@ -255,10 +259,8 @@
       2 as PatchSetNum,
       '123'
     );
-    sinon.assert.calledWithExactly(navigateToChangeStub, element.change!, {
-      patchNum: EDIT,
-      basePatchNum: element.change!.revisions[2]._number as BasePatchSetNum,
-    });
+    assert.isTrue(setUrlStub.called);
+    assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/2..edit');
 
     sinon.assert.calledOnceWithExactly(
       closeFixPreviewEventSpy,
@@ -268,7 +270,6 @@
         },
       })
     );
-
     // reset gr-apply-fix-dialog and close
     assert.equal(element.currentFix, undefined);
     assert.equal(element.currentPreviews.length, 0);
@@ -278,18 +279,17 @@
     const applyRobotFixSuggestionStub = stubRestApi(
       'applyRobotFixSuggestion'
     ).returns(Promise.resolve(new Response(null, {status: 500})));
-    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
     element.currentFix = createFixSuggestionInfo('fix_123');
 
     await element.handleApplyFix(new CustomEvent('confirm'));
+
     sinon.assert.calledWithExactly(
       applyRobotFixSuggestionStub,
       element.change!._number,
       2 as PatchSetNum,
       'fix_123'
     );
-    assert.isTrue(navigateToChangeStub.notCalled);
-
+    assert.isFalse(setUrlStub.called);
     assert.equal(element.isApplyFixLoading, false);
   });
 
@@ -307,7 +307,6 @@
     stubRestApi('applyRobotFixSuggestion').returns(
       Promise.reject(new Error('backend error'))
     );
-    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
     element.currentFix = createFixSuggestionInfo('fix_123');
 
     const closeFixPreviewEventSpy = sinon.spy();
@@ -322,7 +321,7 @@
       expectedError = e;
     });
     assert.isOk(expectedError);
-    assert.isFalse(navigateToChangeStub.called);
+    assert.isFalse(setUrlStub.called);
     sinon.assert.notCalled(closeFixPreviewEventSpy);
   });
 
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 27bfc7d..295d4b9 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
@@ -21,7 +21,10 @@
 import '../gr-patch-range-select/gr-patch-range-select';
 import '../../change/gr-download-dialog/gr-download-dialog';
 import '../../shared/gr-overlay/gr-overlay';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+  GerritNav,
+  navigationToken,
+} from '../../core/gr-navigation/gr-navigation';
 import {getAppContext} from '../../../services/app-context';
 import {
   computeAllPatchSets,
@@ -318,6 +321,8 @@
 
   private readonly shortcutsController = new ShortcutController(this);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   constructor() {
     super();
     this.setupKeyboardShortcuts();
@@ -1536,7 +1541,7 @@
       );
       if (!comment) {
         fireAlert(this, 'comment not found');
-        GerritNav.navigateToChange(this.change);
+        this.getNavigation().setUrl(createChangeUrl({change: this.change}));
         return;
       }
       this.getChangeModel().updatePath(comment.path);
@@ -1834,11 +1839,14 @@
   ) {
     if (!change) return;
     const range = this.getChangeUrlRange(patchRange, revisions);
-    GerritNav.navigateToChange(change, {
-      patchNum: range.patchNum,
-      basePatchNum: range.basePatchNum,
-      openReplyDialog: !!openReplyDialog,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({
+        change,
+        patchNum: range.patchNum,
+        basePatchNum: range.basePatchNum,
+        openReplyDialog: !!openReplyDialog,
+      })
+    );
   }
 
   // Private but used in tests
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 1b93560..134f23e 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
@@ -5,7 +5,10 @@
  */
 import '../../../test/common-test-setup';
 import './gr-diff-view';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+  GerritNav,
+  navigationToken,
+} from '../../core/gr-navigation/gr-navigation';
 import {
   ChangeStatus,
   DiffViewMode,
@@ -70,6 +73,7 @@
 import {Key} from '../../../utils/dom-util';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {createEditUrl} from '../../../models/views/edit';
+import {testResolver} from '../../../test/common-test-setup';
 
 function createComment(
   id: string,
@@ -92,6 +96,7 @@
     let clock: SinonFakeTimers;
     let diffCommentsStub;
     let getDiffRestApiStub: SinonStub;
+    let setUrlStub: SinonStub;
 
     function getFilesFromFileList(fileList: string[]): Files {
       const changeFilesByPath = fileList.reduce((files, path) => {
@@ -105,6 +110,7 @@
     }
 
     setup(async () => {
+      setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
       stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
       stubRestApi('getLoggedIn').returns(Promise.resolve(false));
       stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
@@ -651,13 +657,10 @@
       await element.updateComplete;
 
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
 
       pressKey(element, 'u');
-      assert(
-        changeNavStub.lastCall.calledWith(element.change),
-        'Should navigate to /c/42/'
-      );
+      assert.isTrue(setUrlStub.calledOnce);
+      assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
       await element.updateComplete;
 
       pressKey(element, ']');
@@ -706,10 +709,8 @@
       assert.isTrue(element.loading);
 
       pressKey(element, '[');
-      assert(
-        changeNavStub.lastCall.calledWith(element.change),
-        'Should navigate to /c/42/'
-      );
+      assert.isTrue(setUrlStub.calledTwice);
+      assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
       await element.updateComplete;
       assert.isTrue(element.loading);
 
@@ -790,7 +791,6 @@
 
     test('moveToNextCommentThread navigates to next file', async () => {
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      const diffChangeStub = sinon.stub(element, 'navigateToChange');
       assertIsDefined(element.cursor);
       sinon.stub(element.cursor, 'isAtEnd').returns(true);
       element.changeNum = 42 as NumericChangeId;
@@ -834,7 +834,7 @@
       pressKey(element, 'N');
       await element.updateComplete;
 
-      assert.isTrue(diffChangeStub.called);
+      assert.isTrue(setUrlStub.calledOnce);
     });
 
     test('shift+x shortcut toggles all diff context', async () => {
@@ -966,17 +966,12 @@
     });
 
     test('A fires an error event when not logged in', async () => {
-      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
       element.loggedIn = false;
       const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       pressKey(element, 'a');
       await element.updateComplete;
-      assert.isTrue(
-        changeNavStub.notCalled,
-        'The `a` keyboard shortcut ' +
-          'should only work when the user is logged in.'
-      );
+      assert.isFalse(setUrlStub.calledOnce);
       assert.isTrue(loggedInErrorSpy.called);
     });
 
@@ -994,19 +989,15 @@
           b: createRevision(5),
         },
       };
-      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
       element.loggedIn = true;
       const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       pressKey(element, 'a');
       await element.updateComplete;
-      assert(
-        changeNavStub.lastCall.calledWithExactly(element.change, {
-          patchNum: 10 as RevisionPatchSetNum,
-          basePatchNum: 5 as BasePatchSetNum,
-          openReplyDialog: true,
-        }),
-        'Should navigate to /c/42/5..10'
+      assert.isTrue(setUrlStub.calledOnce);
+      assert.equal(
+        setUrlStub.lastCall.firstArg,
+        '/c/test-project/+/42/5..10?openReplyDialog=true'
       );
       assert.isFalse(loggedInErrorSpy.called);
     });
@@ -1025,19 +1016,15 @@
           b: createRevision(2),
         },
       };
-      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
       element.loggedIn = true;
       const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       pressKey(element, 'a');
       await element.updateComplete;
-      assert(
-        changeNavStub.lastCall.calledWithExactly(element.change, {
-          patchNum: 1 as RevisionPatchSetNum,
-          basePatchNum: PARENT,
-          openReplyDialog: true,
-        }),
-        'Should navigate to /c/42/1'
+      assert.isTrue(setUrlStub.calledOnce);
+      assert.equal(
+        setUrlStub.lastCall.firstArg,
+        '/c/test-project/+/42/1?openReplyDialog=true'
       );
       assert.isFalse(loggedInErrorSpy.called);
     });
@@ -1064,17 +1051,10 @@
       element.path = 'glados.txt';
 
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
 
       pressKey(element, 'u');
-      assert(
-        changeNavStub.lastCall.calledWithExactly(element.change, {
-          patchNum: 10 as RevisionPatchSetNum,
-          basePatchNum: 5 as BasePatchSetNum,
-          openReplyDialog: false,
-        }),
-        'Should navigate to /c/42/5..10'
-      );
+      assert.isTrue(setUrlStub.calledOnce);
+      assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
 
       pressKey(element, ']');
       assert.isTrue(element.loading);
@@ -1120,14 +1100,8 @@
 
       pressKey(element, '[');
       assert.isTrue(element.loading);
-      assert(
-        changeNavStub.lastCall.calledWithExactly(element.change, {
-          patchNum: 10 as RevisionPatchSetNum,
-          basePatchNum: 5 as BasePatchSetNum,
-          openReplyDialog: false,
-        }),
-        'Should navigate to /c/42/5..10'
-      );
+      assert.isTrue(setUrlStub.calledTwice);
+      assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
 
       assertIsDefined(element.downloadOverlay);
       const downloadOverlayStub = sinon
@@ -1159,17 +1133,10 @@
       element.path = 'glados.txt';
 
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
-      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
 
       pressKey(element, 'u');
-      assert(
-        changeNavStub.lastCall.calledWithExactly(element.change, {
-          patchNum: 1 as RevisionPatchSetNum,
-          basePatchNum: PARENT,
-          openReplyDialog: false,
-        }),
-        'Should navigate to /c/42/1'
-      );
+      assert.isTrue(setUrlStub.calledOnce);
+      assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
 
       pressKey(element, ']');
       assert(
@@ -1210,17 +1177,10 @@
       );
       element.path = 'chell.go';
 
-      changeNavStub.reset();
+      setUrlStub.reset();
       pressKey(element, '[');
-      assert(
-        changeNavStub.lastCall.calledWithExactly(element.change, {
-          patchNum: 1 as RevisionPatchSetNum,
-          basePatchNum: PARENT,
-          openReplyDialog: false,
-        }),
-        'Should navigate to /c/42/1'
-      );
-      assert.isTrue(changeNavStub.calledOnce);
+      assert.isTrue(setUrlStub.calledOnce);
+      assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
     });
 
     test('edit should redirect to edit page', async () => {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 71a2c91..ee83e80 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -21,6 +21,7 @@
 import {fixture, html, assert} from '@open-wc/testing';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import '../../shared/gr-dialog/gr-dialog';
+import {waitForEventOnce} from '../../../utils/event-util';
 
 suite('gr-edit-controls tests', () => {
   let element: GrEditControls;
@@ -507,15 +508,13 @@
   });
 
   suite('save file upload', () => {
-    let navStub: sinon.SinonStub;
     let fileStub: sinon.SinonStub;
 
     setup(() => {
-      navStub = sinon.stub(GerritNav, 'navigateToChange');
       fileStub = stubRestApi('saveFileUploadChangeEdit');
     });
 
-    test('handleUploadConfirm', () => {
+    test('handleUploadConfirm', async () => {
       fileStub.returns(Promise.resolve({ok: true}));
 
       element.change = {
@@ -535,9 +534,13 @@
         current_revision: 'efgh' as CommitId,
       };
 
-      element.handleUploadConfirm('test.php', 'base64').then(() => {
-        assert.isTrue(navStub.calledWithExactly(1 as NumericChangeId));
-      });
+      element.handleUploadConfirm('test.php', 'base64');
+
+      assert.isTrue(fileStub.calledOnce);
+      assert.equal(fileStub.lastCall.args[0], 1);
+      assert.equal(fileStub.lastCall.args[1], 'test.php');
+      assert.equal(fileStub.lastCall.args[2], 'base64');
+      await waitForEventOnce(element, 'reload');
     });
   });
 
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 4cbde8f..0b1a2de 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
@@ -8,7 +8,7 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-editable-label/gr-editable-label';
 import '../gr-default-editor/gr-default-editor';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {computeTruncatedPath} from '../../../utils/path-list-util';
 import {
   EditPreferencesInfo,
@@ -32,6 +32,7 @@
 import {changeModelToken} from '../../../models/change/change-model';
 import {ShortcutController} from '../../lit/shortcut-controller';
 import {editViewModelToken, EditViewState} from '../../../models/views/edit';
+import {createChangeUrl} from '../../../models/views/change';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const SAVING_MESSAGE = 'Saving changes...';
@@ -94,6 +95,8 @@
 
   private readonly getEditViewModel = resolve(this, editViewModelToken);
 
+  private readonly getNavigation = resolve(this, navigationToken);
+
   private readonly shortcuts = new ShortcutController(this);
 
   // Tests use this so needs to be non private
@@ -328,7 +331,7 @@
       this,
       'Change edits cannot be created if change is merged or abandoned. Redirected to non edit mode.'
     );
-    GerritNav.navigateToChange(this.change);
+    this.getNavigation().setUrl(createChangeUrl({change: this.change}));
   }
 
   private navigateToChangeIfEditType() {
@@ -336,7 +339,7 @@
 
     // Prevent editing binary files
     fireAlert(this, 'You cannot edit binary files within the inline editor.');
-    GerritNav.navigateToChange(this.change);
+    this.getNavigation().setUrl(createChangeUrl({change: this.change}));
   }
 
   // private but used in test
@@ -362,10 +365,9 @@
   // private but used in test
   viewEditInChangeView() {
     if (!this.change) return;
-    GerritNav.navigateToChange(this.change, {
-      isEdit: true,
-      forceReload: true,
-    });
+    this.getNavigation().setUrl(
+      createChangeUrl({change: this.change, edit: true, forceReload: true})
+    );
   }
 
   // private but used in test
@@ -483,7 +485,9 @@
         )
         .then(() => {
           assertIsDefined(this.change, 'change');
-          GerritNav.navigateToChange(this.change, {forceReload: true});
+          this.getNavigation().setUrl(
+            createChangeUrl({change: this.change, forceReload: true})
+          );
         });
     });
   };
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 8bc788c..52581ed 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
@@ -6,7 +6,7 @@
 import '../../../test/common-test-setup';
 import './gr-editor-view';
 import {GrEditorView} from './gr-editor-view';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
 import {HttpMethod} from '../../../constants/constants';
 import {
   mockPromise,
@@ -31,6 +31,7 @@
 import {fixture, html, assert} from '@open-wc/testing';
 import {EventType} from '../../../types/events';
 import {Modifier} from '../../../utils/dom-util';
+import {testResolver} from '../../../test/common-test-setup';
 
 suite('gr-editor-view tests', () => {
   let element: GrEditorView;
@@ -455,10 +456,15 @@
   test('viewEditInChangeView', () => {
     element.change = createChangeViewChange();
     navigateStub.restore();
-    const navStub = sinon.stub(GerritNav, 'navigateToChange');
+    const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
+
     element.viewEditInChangeView();
-    assert.equal(navStub.lastCall.args[1]!.patchNum, undefined);
-    assert.equal(navStub.lastCall.args[1]!.isEdit, true);
+
+    assert.isTrue(setUrlStub.called);
+    assert.equal(
+      setUrlStub.lastCall.firstArg,
+      '/c/test-project/+/42,edit?forceReload=true'
+    );
   });
 
   suite('keyboard shortcuts', () => {
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 5d46210..5cfe670 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -139,7 +139,7 @@
   // but we are not sure whether this was ever 100% working correctly. A
   // major challenge is being able to select PARENT explicitly even if your
   // preference for the default choice is FIRST_PARENT. <gr-file-list-header>
-  // just uses `GerritNav.navigateToChange()` and the router does not have any
+  // just uses `navigation.setUrl()` and the router does not have any
   // way of forcing the basePatchSetNum to stick to PARENT without being
   // altered back to FIRST_PARENT here.
   // See also corresponding TODO in gr-settings-view.