Merge "Do not reload page after clicking Cancel in apply-fix dialog"
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 2bf19ec..7a5bc6f 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
@@ -156,6 +156,7 @@
   ThreadListModifiedEvent,
   TabState,
   EventType,
+  CloseFixPreviewEvent,
 } from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrMessagesList} from '../gr-messages-list/gr-messages-list';
@@ -678,7 +679,7 @@
       this._handleCommitMessageCancel()
     );
     this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
-    this.addEventListener('close-fix-preview', () => this._onCloseFixPreview());
+    this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
     window.addEventListener('scroll', this.handleScroll);
     document.addEventListener('visibilitychange', this.handleVisibilityChange);
 
@@ -744,8 +745,8 @@
     this.$.applyFixDialog.open(e);
   }
 
-  _onCloseFixPreview() {
-    this._reload();
+  _onCloseFixPreview(e: CloseFixPreviewEvent) {
+    if (e.detail.fixApplied) this._reload();
   }
 
   _handleToggleDiffMode(e: CustomKeyboardEvent) {
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 716d142..0bf9f1c 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
@@ -37,12 +37,14 @@
 import {isRobot} from '../../../utils/comment-util';
 import {OpenFixPreviewEvent} from '../../../types/events';
 import {appContext} from '../../../services/app-context';
-import {fireEvent} from '../../../utils/event-util';
+import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
 import {ParsedChangeInfo} from '../../../types/types';
+import {GrButton} from '../../shared/gr-button/gr-button';
 
 export interface GrApplyFixDialog {
   $: {
     applyFixOverlay: GrOverlay;
+    nextFix: GrButton;
   };
 }
 
@@ -168,7 +170,7 @@
         }
       })
       .catch(err => {
-        this._close();
+        this._close(false);
         throw err;
       });
   }
@@ -186,7 +188,7 @@
     if (e) {
       e.stopPropagation();
     }
-    this._close();
+    this._close(false);
   }
 
   addOneTo(_selectedFixIdx: number) {
@@ -225,12 +227,12 @@
     return _selectedFixIdx === fixSuggestions.length - 1;
   }
 
-  _close() {
+  _close(fixApplied: boolean) {
     this._currentFix = undefined;
     this._currentPreviews = [];
     this._isApplyFixLoading = false;
 
-    fireEvent(this, 'close-fix-preview');
+    fireCloseFixPreview(this, fixApplied);
     this.$.applyFixOverlay.close();
   }
 
@@ -282,7 +284,7 @@
             EditPatchSetNum,
             patchNum as BasePatchSetNum
           );
-          this._close();
+          this._close(true);
         }
         this._isApplyFixLoading = false;
       });
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
deleted file mode 100644
index d78961c..0000000
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
+++ /dev/null
@@ -1,272 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-apply-fix-dialog.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-apply-fix-dialog');
-
-suite('gr-apply-fix-dialog tests', () => {
-  let element;
-
-  const ROBOT_COMMENT_WITH_TWO_FIXES = {
-    robot_id: 'robot_1',
-    fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}],
-  };
-
-  const ROBOT_COMMENT_WITH_ONE_FIX = {
-    robot_id: 'robot_1',
-    fix_suggestions: [{fix_id: 'fix_1'}],
-  };
-
-  setup(() => {
-    element = basicFixture.instantiate();
-    element.changeNum = '1';
-    element._patchNum = 2;
-    element.change = {
-      _number: '1',
-      project: 'project',
-      revisions: {
-        abcd: {_number: 1},
-        efgh: {_number: 2},
-      },
-      current_revision: 'efgh',
-    };
-    element.prefs = {
-      font_size: 12,
-      line_length: 100,
-      tab_size: 4,
-    };
-  });
-
-  suite('dialog open', () => {
-    setup(() => {
-      stubRestApi('getRobotCommentFixPreview')
-          .returns(Promise.resolve({
-            f1: {
-              meta_a: {},
-              meta_b: {},
-              content: [
-                {
-                  ab: ['loqlwkqll'],
-                },
-                {
-                  b: ['qwqqsqw'],
-                },
-                {
-                  ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
-                },
-              ],
-            },
-            f2: {
-              meta_a: {},
-              meta_b: {},
-              content: [
-                {
-                  ab: ['eqweqweqwex'],
-                },
-                {
-                  b: ['zassdasd'],
-                },
-                {
-                  ab: ['zassdasd', 'dasdasda', 'asdasdad'],
-                },
-              ],
-            },
-          }));
-      sinon.stub(element.$.applyFixOverlay, 'open')
-          .returns(Promise.resolve());
-    });
-
-    test('dialog opens fetch and sets previews', done => {
-      element.open({detail: {patchNum: 2,
-        comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
-          .then(() => {
-            assert.equal(element._currentFix.fix_id, 'fix_1');
-            assert.equal(element._currentPreviews.length, 2);
-            assert.equal(element._robotId, 'robot_1');
-            const button = element.shadowRoot.querySelector(
-                '#applyFixDialog').shadowRoot.querySelector('#confirm');
-            assert.isFalse(button.hasAttribute('disabled'));
-            assert.equal(button.getAttribute('title'), '');
-            done();
-          });
-    });
-
-    test('tooltip is hidden if apply fix is loading', done => {
-      element.open({detail: {patchNum: 2,
-        comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
-          .then(() => {
-            element._isApplyFixLoading = true;
-            const button = element.shadowRoot.querySelector(
-                '#applyFixDialog').shadowRoot.querySelector('#confirm');
-            assert.isTrue(button.hasAttribute('disabled'));
-            assert.equal(button.getAttribute('title'), '');
-            done();
-          });
-    });
-
-    test('apply fix button is disabled on older patchset', done => {
-      element.change = {
-        _number: '1',
-        project: 'project',
-        revisions: {
-          abcd: {_number: 1},
-          efgh: {_number: 2},
-        },
-        current_revision: 'abcd',
-      };
-      element.open({detail: {patchNum: 2,
-        comment: ROBOT_COMMENT_WITH_ONE_FIX}})
-          .then(() => {
-            flush(() => {
-              const button = element.shadowRoot.querySelector(
-                  '#applyFixDialog').shadowRoot.querySelector('#confirm');
-              assert.isTrue(button.hasAttribute('disabled'));
-              assert.equal(button.getAttribute('title'),
-                  'Fix can only be applied to the latest patchset');
-              done();
-            });
-          });
-    });
-  });
-
-  test('next button state updated when suggestions changed', done => {
-    stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
-    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
-    element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
-        .then(() => assert.isTrue(element.$.nextFix.disabled))
-        .then(() =>
-          element.open({detail: {patchNum: 2,
-            comment: ROBOT_COMMENT_WITH_TWO_FIXES}}))
-        .then(() => {
-          assert.isFalse(element.$.nextFix.disabled);
-          done();
-        });
-  });
-
-  test('preview endpoint throws error should reset dialog', async () => {
-    stubRestApi('getRobotCommentFixPreview').returns(
-        Promise.reject(new Error('backend error')));
-    element.open({detail: {patchNum: 2,
-      comment: ROBOT_COMMENT_WITH_TWO_FIXES}});
-    await flush();
-    assert.equal(element._currentFix, undefined);
-  });
-
-  test('apply fix button should call apply ' +
-  'and navigate to change view', () => {
-    const stub = stubRestApi('applyFixSuggestion').returns(
-        Promise.resolve({ok: true}));
-    sinon.stub(GerritNav, 'navigateToChange');
-    element._currentFix = {fix_id: '123'};
-
-    return element._handleApplyFix().then(() => {
-      assert.isTrue(stub.calledWithExactly('1', 2, '123'));
-      assert.isTrue(GerritNav.navigateToChange.calledWithExactly({
-        _number: '1',
-        project: 'project',
-        revisions: {
-          abcd: {_number: 1},
-          efgh: {_number: 2},
-        },
-        current_revision: 'efgh',
-      }, 'edit', 2));
-
-      // reset gr-apply-fix-dialog and close
-      assert.equal(element._currentFix, undefined);
-      assert.equal(element._currentPreviews.length, 0);
-    });
-  });
-
-  test('should not navigate to change view if incorect reponse', done => {
-    const stub = stubRestApi('applyFixSuggestion').returns(Promise.resolve({}));
-    sinon.stub(GerritNav, 'navigateToChange');
-    element._currentFix = {fix_id: '123'};
-
-    element._handleApplyFix().then(() => {
-      assert.isTrue(stub.calledWithExactly('1', 2, '123'));
-      assert.isTrue(GerritNav.navigateToChange.notCalled);
-
-      assert.equal(element._isApplyFixLoading, false);
-      done();
-    });
-  });
-
-  test('select fix forward and back of multiple suggested fixes', done => {
-    stubRestApi('getRobotCommentFixPreview')
-        .returns(Promise.resolve({
-          f1: {
-            meta_a: {},
-            meta_b: {},
-            content: [
-              {
-                ab: ['loqlwkqll'],
-              },
-              {
-                b: ['qwqqsqw'],
-              },
-              {
-                ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
-              },
-            ],
-          },
-          f2: {
-            meta_a: {},
-            meta_b: {},
-            content: [
-              {
-                ab: ['eqweqweqwex'],
-              },
-              {
-                b: ['zassdasd'],
-              },
-              {
-                ab: ['zassdasd', 'dasdasda', 'asdasdad'],
-              },
-            ],
-          },
-        }));
-    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
-
-    element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}})
-        .then(() => {
-          element._onNextFixClick();
-          assert.equal(element._currentFix.fix_id, 'fix_2');
-          element._onPrevFixClick();
-          assert.equal(element._currentFix.fix_id, 'fix_1');
-          done();
-        });
-  });
-
-  test('server-error should throw for failed apply call', async () => {
-    stubRestApi('applyFixSuggestion').returns(
-        Promise.reject(new Error('backend error')));
-    sinon.stub(GerritNav, 'navigateToChange');
-    element._currentFix = {fix_id: '123'};
-    let expectedError;
-    await element._handleApplyFix().catch(e => {
-      expectedError = e;
-    });
-    assert.isOk(expectedError);
-    assert.isFalse(GerritNav.navigateToChange.called);
-  });
-});
-
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
new file mode 100644
index 0000000..d3d7615
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -0,0 +1,376 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-apply-fix-dialog';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {GrApplyFixDialog} from './gr-apply-fix-dialog';
+import {
+  BasePatchSetNum,
+  EditPatchSetNum,
+  PatchSetNum,
+  RobotId,
+  RobotRunId,
+  Timestamp,
+  UrlEncodedCommentId,
+} from '../../../types/common';
+import {
+  createFixSuggestionInfo,
+  createParsedChange,
+  createRevisions,
+  getCurrentRevision,
+} from '../../../test/test-data-generators';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {DiffInfo} from '../../../types/diff';
+import {UIRobot} from '../../../utils/comment-util';
+import {
+  CloseFixPreviewEventDetail,
+  EventType,
+  OpenFixPreviewEventDetail,
+} from '../../../types/events';
+import {GrButton} from '../../shared/gr-button/gr-button';
+
+const basicFixture = fixtureFromElement('gr-apply-fix-dialog');
+
+suite('gr-apply-fix-dialog tests', () => {
+  let element: GrApplyFixDialog;
+
+  const ROBOT_COMMENT_WITH_TWO_FIXES: UIRobot = {
+    id: '1' as UrlEncodedCommentId,
+    updated: '2018-02-08 18:49:18.000000000' as Timestamp,
+    robot_id: 'robot_1' as RobotId,
+    robot_run_id: 'run_1' as RobotRunId,
+    properties: {},
+    fix_suggestions: [
+      createFixSuggestionInfo('fix_1'),
+      createFixSuggestionInfo('fix_2'),
+    ],
+  };
+
+  const ROBOT_COMMENT_WITH_ONE_FIX: UIRobot = {
+    id: '2' as UrlEncodedCommentId,
+    updated: '2018-02-08 18:49:18.000000000' as Timestamp,
+    robot_id: 'robot_1' as RobotId,
+    robot_run_id: 'run_1' as RobotRunId,
+    properties: {},
+    fix_suggestions: [createFixSuggestionInfo('fix_1')],
+  };
+
+  function getConfirmButton(): GrButton {
+    return queryAndAssert(
+      queryAndAssert(element, '#applyFixDialog'),
+      '#confirm'
+    );
+  }
+
+  setup(() => {
+    element = basicFixture.instantiate();
+    const change = {
+      ...createParsedChange(),
+      revisions: createRevisions(2),
+      current_revision: getCurrentRevision(1),
+    };
+    element.changeNum = change._number;
+    element._patchNum = change.revisions[change.current_revision]._number;
+    element.change = change;
+    element.prefs = {
+      ...createDefaultDiffPrefs(),
+      font_size: 12,
+      line_length: 100,
+      tab_size: 4,
+    };
+  });
+
+  suite('dialog open', () => {
+    setup(() => {
+      const diffInfo1: DiffInfo = {
+        meta_a: {
+          name: 'f1',
+          content_type: 'text',
+          lines: 10,
+        },
+        meta_b: {
+          name: 'f1',
+          content_type: 'text',
+          lines: 12,
+        },
+        content: [
+          {
+            ab: ['loqlwkqll'],
+          },
+          {
+            b: ['qwqqsqw'],
+          },
+          {
+            ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'],
+          },
+        ],
+        change_type: 'MODIFIED',
+        intraline_status: 'OK',
+      };
+
+      const diffInfo2: DiffInfo = {
+        meta_a: {
+          name: 'f2',
+          content_type: 'text',
+          lines: 10,
+        },
+        meta_b: {
+          name: 'f2',
+          content_type: 'text',
+          lines: 12,
+        },
+        content: [
+          {
+            ab: ['eqweqweqwex'],
+          },
+          {
+            b: ['zassdasd'],
+          },
+          {
+            ab: ['zassdasd', 'dasdasda', 'asdasdad'],
+          },
+        ],
+        change_type: 'MODIFIED',
+        intraline_status: 'OK',
+      };
+
+      stubRestApi('getRobotCommentFixPreview').returns(
+        Promise.resolve({
+          f1: diffInfo1,
+          f2: diffInfo2,
+        })
+      );
+      sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+    });
+
+    test('dialog opens fetch and sets previews', async () => {
+      await element.open(
+        new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+          detail: {
+            patchNum: 2 as PatchSetNum,
+            comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+          },
+        })
+      );
+      assert.equal(element._currentFix!.fix_id, 'fix_1');
+      assert.equal(element._currentPreviews.length, 2);
+      assert.equal(element._robotId, 'robot_1' as RobotId);
+      const button = getConfirmButton();
+      assert.isFalse(button.hasAttribute('disabled'));
+      assert.equal(button.getAttribute('title'), '');
+    });
+
+    test('tooltip is hidden if apply fix is loading', async () => {
+      await element.open(
+        new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+          detail: {
+            patchNum: 2 as PatchSetNum,
+            comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+          },
+        })
+      );
+      element._isApplyFixLoading = true;
+      const button = getConfirmButton();
+      assert.isTrue(button.hasAttribute('disabled'));
+      assert.equal(button.getAttribute('title'), '');
+    });
+
+    test('apply fix button is disabled on older patchset', async () => {
+      element.change = element.change = {
+        ...createParsedChange(),
+        revisions: createRevisions(2),
+        current_revision: getCurrentRevision(0),
+      };
+      await element.open(
+        new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+          detail: {
+            patchNum: 2 as PatchSetNum,
+            comment: ROBOT_COMMENT_WITH_ONE_FIX,
+          },
+        })
+      );
+      await flush();
+      const button = getConfirmButton();
+      assert.isTrue(button.hasAttribute('disabled'));
+      assert.equal(
+        button.getAttribute('title'),
+        'Fix can only be applied to the latest patchset'
+      );
+    });
+  });
+
+  test('next button state updated when suggestions changed', async () => {
+    stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
+    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+    await element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_ONE_FIX,
+        },
+      })
+    );
+    assert.isTrue(element.$.nextFix.disabled);
+    await element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+        },
+      })
+    );
+    assert.isFalse(element.$.nextFix.disabled);
+  });
+
+  test('preview endpoint throws error should reset dialog', async () => {
+    stubRestApi('getRobotCommentFixPreview').returns(
+      Promise.reject(new Error('backend error'))
+    );
+    element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+        },
+      })
+    );
+    await flush();
+    assert.equal(element._currentFix, undefined);
+  });
+
+  test('apply fix button should call apply, navigate to change view and fire close', async () => {
+    const applyFixSuggestionStub = stubRestApi('applyFixSuggestion').returns(
+      Promise.resolve(new Response(null, {status: 200}))
+    );
+    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+    element._currentFix = createFixSuggestionInfo('123');
+
+    const closeFixPreviewEventSpy = sinon.spy();
+    // Element is recreated after each test, removeEventListener isn't required
+    element.addEventListener(
+      EventType.CLOSE_FIX_PREVIEW,
+      closeFixPreviewEventSpy
+    );
+    await element._handleApplyFix(new CustomEvent('confirm'));
+
+    sinon.assert.calledOnceWithExactly(
+      applyFixSuggestionStub,
+      element.change!._number,
+      2 as PatchSetNum,
+      '123'
+    );
+    sinon.assert.calledWithExactly(
+      navigateToChangeStub,
+      element.change!,
+      EditPatchSetNum,
+      element.change!.revisions[2]._number as BasePatchSetNum
+    );
+
+    sinon.assert.calledOnceWithExactly(
+      closeFixPreviewEventSpy,
+      new CustomEvent<CloseFixPreviewEventDetail>(EventType.CLOSE_FIX_PREVIEW, {
+        detail: {
+          fixApplied: true,
+        },
+      })
+    );
+
+    // reset gr-apply-fix-dialog and close
+    assert.equal(element._currentFix, undefined);
+    assert.equal(element._currentPreviews.length, 0);
+  });
+
+  test('should not navigate to change view if incorect reponse', async () => {
+    const applyFixSuggestionStub = stubRestApi('applyFixSuggestion').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(
+      applyFixSuggestionStub,
+      element.change!._number,
+      2 as PatchSetNum,
+      'fix_123'
+    );
+    assert.isTrue(navigateToChangeStub.notCalled);
+
+    assert.equal(element._isApplyFixLoading, false);
+  });
+
+  test('select fix forward and back of multiple suggested fixes', async () => {
+    sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
+
+    await element.open(
+      new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
+        detail: {
+          patchNum: 2 as PatchSetNum,
+          comment: ROBOT_COMMENT_WITH_TWO_FIXES,
+        },
+      })
+    );
+    element._onNextFixClick(new CustomEvent('click'));
+    assert.equal(element._currentFix!.fix_id, 'fix_2');
+    element._onPrevFixClick(new CustomEvent('click'));
+    assert.equal(element._currentFix!.fix_id, 'fix_1');
+  });
+
+  test('server-error should throw for failed apply call', async () => {
+    stubRestApi('applyFixSuggestion').returns(
+      Promise.reject(new Error('backend error'))
+    );
+    const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+    element._currentFix = createFixSuggestionInfo('fix_123');
+
+    const closeFixPreviewEventSpy = sinon.spy();
+    // Element is recreated after each test, removeEventListener isn't required
+    element.addEventListener(
+      EventType.CLOSE_FIX_PREVIEW,
+      closeFixPreviewEventSpy
+    );
+
+    let expectedError;
+    await element._handleApplyFix(new CustomEvent('click')).catch(e => {
+      expectedError = e;
+    });
+    assert.isOk(expectedError);
+    assert.isFalse(navigateToChangeStub.called);
+    sinon.assert.notCalled(closeFixPreviewEventSpy);
+  });
+
+  test('onCancel fires close with correct parameters', () => {
+    const closeFixPreviewEventSpy = sinon.spy();
+    // Element is recreated after each test, removeEventListener isn't required
+    element.addEventListener(
+      EventType.CLOSE_FIX_PREVIEW,
+      closeFixPreviewEventSpy
+    );
+    element.onCancel(new CustomEvent('cancel'));
+    sinon.assert.calledOnceWithExactly(
+      closeFixPreviewEventSpy,
+      new CustomEvent<CloseFixPreviewEventDetail>(EventType.CLOSE_FIX_PREVIEW, {
+        detail: {
+          fixApplied: false,
+        },
+      })
+    );
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 24a7d78..874f572 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -72,6 +72,7 @@
 declare global {
   interface HTMLElementEventMap {
     'diff-context-expanded': CustomEvent<DiffContextExpandedEventDetail>;
+    'content-load-needed': CustomEvent<ContentLoadNeededEventDetail>;
   }
 }
 
@@ -511,14 +512,14 @@
             end_line: lastRange.right.end_line,
           },
         };
-        fire<ContentLoadNeededEventDetail>(button, 'content-load-needed', {
+        fire(button, 'content-load-needed', {
           lineRange,
         });
       });
     } else {
       button.addEventListener('click', e => {
         e.stopPropagation();
-        fire<DiffContextExpandedEventDetail>(button, 'diff-context-expanded', {
+        fire(button, 'diff-context-expanded', {
           groups,
           section,
           numLines,
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 3bfe743..778e58d 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -65,6 +65,8 @@
   RelatedChangeAndCommitInfo,
   SubmittedTogetherInfo,
   RelatedChangesInfo,
+  FixSuggestionInfo,
+  FixId,
 } from '../types/common';
 import {
   AccountsVisibility,
@@ -614,3 +616,11 @@
     non_visible_changes: 0,
   };
 }
+
+export function createFixSuggestionInfo(fixId = 'fix_1'): FixSuggestionInfo {
+  return {
+    fix_id: fixId as FixId,
+    description: `Fix ${fixId}`,
+    replacements: [],
+  };
+}
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 11155c1..7bde788 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -32,6 +32,7 @@
   MOVED_LINK_CLICKED = 'moved-link-clicked',
   NETWORK_ERROR = 'network-error',
   OPEN_FIX_PREVIEW = 'open-fix-preview',
+  CLOSE_FIX_PREVIEW = 'close-fix-preview',
   PAGE_ERROR = 'page-error',
   RELOAD = 'reload',
   REPLY = 'reply',
@@ -54,6 +55,7 @@
     'iron-announce': IronAnnounceEvent;
     'moved-link-clicked': MovedLinkClickedEvent;
     'open-fix-preview': OpenFixPreviewEvent;
+    'close-fix-preview': CloseFixPreviewEvent;
     /* prettier-ignore */
     'reload': ReloadEvent;
     /* prettier-ignore */
@@ -133,6 +135,11 @@
 }
 export type OpenFixPreviewEvent = CustomEvent<OpenFixPreviewEventDetail>;
 
+export interface CloseFixPreviewEventDetail {
+  fixApplied: boolean;
+}
+export type CloseFixPreviewEvent = CustomEvent<CloseFixPreviewEventDetail>;
+
 export interface PageErrorEventDetail {
   response?: Response;
 }
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 080955f..da075f1 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -20,15 +20,8 @@
 import {
   DialogChangeEventDetail,
   EventType,
-  IronAnnounceEventDetail,
-  NetworkErrorEventDetail,
-  PageErrorEventDetail,
-  ServerErrorEventDetail,
-  ShowAlertEventDetail,
   SwitchTabEventDetail,
   TabState,
-  ThreadListModifiedDetail,
-  TitleChangeEventDetail,
 } from '../types/events';
 
 export function fireEvent(target: EventTarget, type: string) {
@@ -40,6 +33,34 @@
   );
 }
 
+type HTMLElementEventDetailType<
+  K extends keyof HTMLElementEventMap
+> = HTMLElementEventMap[K] extends CustomEvent<infer DT>
+  ? unknown extends DT
+    ? never
+    : DT
+  : never;
+
+type DocumentEventDetailType<
+  K extends keyof DocumentEventMap
+> = DocumentEventMap[K] extends CustomEvent<infer DT>
+  ? unknown extends DT
+    ? never
+    : DT
+  : never;
+
+export function fire<K extends keyof DocumentEventMap>(
+  target: Document,
+  type: K,
+  detail: DocumentEventDetailType<K>
+): void;
+
+export function fire<K extends keyof HTMLElementEventMap>(
+  target: EventTarget,
+  type: K,
+  detail: HTMLElementEventDetailType<K>
+): void;
+
 export function fire<T>(target: EventTarget, type: string, detail: T) {
   target.dispatchEvent(
     new CustomEvent<T>(type, {
@@ -51,27 +72,27 @@
 }
 
 export function fireAlert(target: EventTarget, message: string) {
-  fire<ShowAlertEventDetail>(target, EventType.SHOW_ALERT, {message});
+  fire(target, EventType.SHOW_ALERT, {message});
 }
 
 export function firePageError(response?: Response | null) {
   if (response === null) response = undefined;
-  fire<PageErrorEventDetail>(document, EventType.PAGE_ERROR, {response});
+  fire(document, EventType.PAGE_ERROR, {response});
 }
 
 export function fireServerError(response: Response, request?: FetchRequest) {
-  fire<ServerErrorEventDetail>(document, EventType.SERVER_ERROR, {
+  fire(document, EventType.SERVER_ERROR, {
     response,
     request,
   });
 }
 
 export function fireNetworkError(error: Error) {
-  fire<NetworkErrorEventDetail>(document, EventType.NETWORK_ERROR, {error});
+  fire(document, EventType.NETWORK_ERROR, {error});
 }
 
 export function fireTitleChange(target: EventTarget, title: string) {
-  fire<TitleChangeEventDetail>(target, EventType.TITLE_CHANGE, {title});
+  fire(target, EventType.TITLE_CHANGE, {title});
 }
 
 // TODO(milutin) - remove once new gr-dialog will do it out of the box
@@ -80,11 +101,11 @@
   target: EventTarget,
   detail: DialogChangeEventDetail
 ) {
-  fire<DialogChangeEventDetail>(target, EventType.DIALOG_CHANGE, detail);
+  fire(target, EventType.DIALOG_CHANGE, detail);
 }
 
 export function fireIronAnnounce(target: EventTarget, text: string) {
-  fire<IronAnnounceEventDetail>(target, EventType.IRON_ANNOUNCE, {text});
+  fire(target, EventType.IRON_ANNOUNCE, {text});
 }
 
 export function fireThreadListModifiedEvent(
@@ -92,7 +113,7 @@
   rootId: UrlEncodedCommentId,
   path: string
 ) {
-  fire<ThreadListModifiedDetail>(target, EventType.THREAD_LIST_MODIFIED, {
+  fire(target, EventType.THREAD_LIST_MODIFIED, {
     rootId,
     path,
   });
@@ -105,7 +126,11 @@
   tabState?: TabState
 ) {
   const detail: SwitchTabEventDetail = {tab, scrollIntoView, tabState};
-  fire<SwitchTabEventDetail>(target, EventType.SHOW_PRIMARY_TAB, detail);
+  fire(target, EventType.SHOW_PRIMARY_TAB, detail);
+}
+
+export function fireCloseFixPreview(target: EventTarget, fixApplied: boolean) {
+  fire(target, EventType.CLOSE_FIX_PREVIEW, {fixApplied});
 }
 
 export function waitForEventOnce<K extends keyof HTMLElementEventMap>(