Add feature to router: Block navigation

We want to use that for optimistic updates. While saving a comment we
don't want the user to leave the page, neither navigate to another page
within the app, nor close the browser tab.

Ultimately this can be used for all ongoing write requests. It is
implemented in a generic way.

Release-Notes: skip
Google-Bug-Id: b/262228572
Change-Id: Iefd3e9ca84730f6ade5b68a9c6364d95150b7b90
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index c4cb465..234bf95 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -11,6 +11,8 @@
   stubRestApi,
   addListenerForTest,
   waitUntilCalled,
+  mockPromise,
+  MockPromise,
 } from '../../../test/test-utils';
 import {GrRouter, routerToken} from './gr-router';
 import {GerritView} from '../../../services/router/router-model';
@@ -254,6 +256,80 @@
     });
   });
 
+  suite('navigation blockers', () => {
+    let clock: sinon.SinonFakeTimers;
+    let redirectStub: sinon.SinonStub;
+    let urlPromise: MockPromise<string>;
+
+    setup(() => {
+      stubRestApi('setInProjectLookup');
+      urlPromise = mockPromise<string>();
+      redirectStub = sinon
+        .stub(router, 'redirect')
+        .callsFake(urlPromise.resolve);
+      router._testOnly_startRouter();
+      clock = sinon.useFakeTimers();
+    });
+
+    test('no blockers: normal redirect', async () => {
+      router.page.show('/settings/agreements');
+      const url = await urlPromise;
+      assert.isTrue(redirectStub.calledOnce);
+      assert.equal(url, '/settings/#Agreements');
+    });
+
+    test('redirect blocked', async () => {
+      const firstAlertPromise = mockPromise<Event>();
+      addListenerForTest(document, 'show-alert', firstAlertPromise.resolve);
+
+      router.blockNavigation('a good reason');
+      router.page.show('/settings/agreements');
+
+      const firstAlert = (await firstAlertPromise) as CustomEvent;
+      assert.equal(
+        firstAlert.detail.message,
+        'Waiting 1 second for navigation blockers to resolve ...'
+      );
+
+      const secondAlertPromise = mockPromise<Event>();
+      addListenerForTest(document, 'show-alert', secondAlertPromise.resolve);
+
+      clock.tick(2000);
+
+      const secondAlert = (await secondAlertPromise) as CustomEvent;
+      assert.equal(
+        secondAlert.detail.message,
+        'Navigation is blocked by: a good reason'
+      );
+
+      assert.isFalse(redirectStub.called);
+    });
+
+    test('redirect blocked, but resolved within one second', async () => {
+      const firstAlertPromise = mockPromise<Event>();
+      addListenerForTest(document, 'show-alert', firstAlertPromise.resolve);
+
+      router.blockNavigation('a good reason');
+      router.page.show('/settings/agreements');
+
+      const firstAlert = (await firstAlertPromise) as CustomEvent;
+      assert.equal(
+        firstAlert.detail.message,
+        'Waiting 1 second for navigation blockers to resolve ...'
+      );
+
+      const secondAlertPromise = mockPromise<Event>();
+      addListenerForTest(document, 'show-alert', secondAlertPromise.resolve);
+
+      clock.tick(500);
+      router.releaseNavigation('a good reason');
+      clock.tick(2000);
+
+      await urlPromise;
+      assert.isTrue(redirectStub.calledOnce);
+    });
+  });
+
   suite('route handlers', () => {
     let redirectStub: sinon.SinonStub;
     let setStateStub: sinon.SinonStub;