Repository: electron/fiddle
Pull request #622 (2021-03-26 21:01:15)

feat: add an icon for Electron DLs being unzipped
Right now there's a visual wart that during a download the icon goes from 'cloud' (unknown) -> 'cloud-download' (downloading) -> 'cloud' (unzipping) -> 'saved' (saved) and that second shortlived 'cloud' during unzip is a little confusing because for a moment it looks like the download has disappeared. 

This PR uses 'compressed' as the icon during unzip to avoid giving the user that mixed signal.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/38295148/badge)](https://coveralls.io/builds/38295148)

Coverage increased (+0.006%) to 93.314% when pulling **4de850242a6d87fff712e32738094cb1cbae9cb5 on feat/unzipping-icon** into **34017cd4be99cf3f26ea2b644d0c9d16c5f266ed on master**.

ckerr: Since it's a switch on the VersionState, the compiler should also warn us in the future if we add a new state and forget to handle it here :tada: 

Patch:
diff --git a/src/renderer/components/commands-bisect.tsx b/src/renderer/components/commands-bisect.tsx
index fc748298..f02c9aef 100644
--- a/src/renderer/components/commands-bisect.tsx
+++ b/src/renderer/components/commands-bisect.tsx
@@ -87,6 +87,7 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
         <Button
           icon="git-branch"
           text="Bisect"
+          disabled={appState.isAutoBisecting}
           onClick={appState.toggleBisectDialog}
         />
       );
diff --git a/src/renderer/components/commands-version-chooser.tsx b/src/renderer/components/commands-version-chooser.tsx
index f485a8d3..27d11f4c 100644
--- a/src/renderer/components/commands-version-chooser.tsx
+++ b/src/renderer/components/commands-version-chooser.tsx
@@ -14,7 +14,12 @@ export interface VersionChooserProps {
  * download is managed in the state.
  */
 export const VersionChooser = observer((props: VersionChooserProps) => {
-  const { currentElectronVersion, Bisector, setVersion } = props.appState;
+  const {
+    Bisector,
+    currentElectronVersion,
+    isAutoBisecting,
+    setVersion,
+  } = props.appState;
 
   return (
     <ButtonGroup>
@@ -22,7 +27,7 @@ export const VersionChooser = observer((props: VersionChooserProps) => {
         appState={props.appState}
         onVersionSelect={({ version }) => setVersion(version)}
         currentVersion={currentElectronVersion}
-        disabled={!!Bisector}
+        disabled={!!Bisector || isAutoBisecting}
       />
     </ButtonGroup>
   );
diff --git a/src/renderer/components/dialog-bisect.tsx b/src/renderer/components/dialog-bisect.tsx
index a98ddf5d..12647cc4 100644
--- a/src/renderer/components/dialog-bisect.tsx
+++ b/src/renderer/components/dialog-bisect.tsx
@@ -33,6 +33,7 @@ export class BisectDialog extends React.Component<
     super(props);
 
     this.onSubmit = this.onSubmit.bind(this);
+    this.onAuto = this.onAuto.bind(this);
     this.onClose = this.onClose.bind(this);
     this.onBeginSelect = this.onBeginSelect.bind(this);
     this.onEndSelect = this.onEndSelect.bind(this);
@@ -55,25 +56,35 @@ export class BisectDialog extends React.Component<
     this.setState({ endIndex: this.state.allVersions.indexOf(version) });
   }
 
+  getBisectRange(): RunnableVersion[] {
+    const { endIndex, startIndex, allVersions } = this.state;
+    return endIndex !== undefined && startIndex !== undefined
+      ? allVersions.slice(endIndex, startIndex + 1).reverse()
+      : [];
+  }
+
   /**
    * Handles the submission of the dialog
    *
    * @returns {Promise<void>}
    */
   public async onSubmit(): Promise<void> {
-    const { endIndex, startIndex, allVersions } = this.state;
-    const { appState } = this.props;
-
-    if (endIndex === undefined || startIndex === undefined) {
-      return;
+    const range = this.getBisectRange();
+    if (range.length > 1) {
+      const { appState } = this.props;
+      appState.Bisector = new Bisector(range);
+      const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
+      appState.setVersion(initialBisectPivot);
+      this.onClose();
     }
+  }
 
-    const bisectRange = allVersions.slice(endIndex, startIndex + 1).reverse();
-
-    appState.Bisector = new Bisector(bisectRange);
-    const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
-    appState.setVersion(initialBisectPivot);
-    this.onClose();
+  public async onAuto(): Promise<void> {
+    const range = this.getBisectRange();
+    if (range.length > 1) {
+      window.ElectronFiddle.app.runner.autobisect(range);
+      this.onClose();
+    }
   }
 
   /**
@@ -97,6 +108,13 @@ export class BisectDialog extends React.Component<
     return this.state.startIndex > this.state.endIndex;
   }
 
+  /**
+   * Can we autobisect?
+   */
+  get canAuto(): boolean {
+    return this.canSubmit;
+  }
+
   /**
    * Renders the buttons
    */
@@ -109,6 +127,13 @@ export class BisectDialog extends React.Component<
         onClick={this.onSubmit}
         text="Begin"
       />,
+      <Button
+        icon="lab-test"
+        key="auto"
+        disabled={!this.canAuto}
+        onClick={this.onAuto}
+        text="Auto"
+      />,
       <Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
     ];
   }
diff --git a/src/renderer/components/version-select.tsx b/src/renderer/components/version-select.tsx
index d142b13d..7b056937 100644
--- a/src/renderer/components/version-select.tsx
+++ b/src/renderer/components/version-select.tsx
@@ -42,11 +42,16 @@ export function getItemLabel({ source, state, name }: RunnableVersion): string {
  * @returns
  */
 export function getItemIcon({ state }: RunnableVersion) {
-  return state === 'ready'
-    ? 'saved'
-    : state === 'downloading'
-    ? 'cloud-download'
-    : 'cloud';
+  switch (state) {
+    case VersionState.unknown:
+      return 'cloud';
+    case VersionState.ready:
+      return 'saved';
+    case VersionState.downloading:
+      return 'cloud-download';
+    case VersionState.unzipping:
+      return 'compressed';
+  }
 }
 
 /**
diff --git a/src/renderer/runner.ts b/src/renderer/runner.ts
index d3d12046..f8ad6ea6 100644
--- a/src/renderer/runner.ts
+++ b/src/renderer/runner.ts
@@ -60,7 +60,21 @@ export class Runner {
    * @returns {Promise<RunResult>}
    * @memberof Runner
    */
-  public async autobisect(
+  public autobisect(versions: Array<RunnableVersion>): Promise<RunResult> {
+    const { appState } = this;
+    appState.isAutoBisecting = true;
+    const done = () => (appState.isAutoBisecting = false);
+    return this.autobisectImpl(versions).finally(done);
+  }
+
+  /**
+   * Bisect the current fiddle across the specified versions.
+   *
+   * @param {Array<RunnableVersion>} versions - versions to bisect
+   * @returns {Promise<RunResult>}
+   * @memberof Runner
+   */
+  public async autobisectImpl(
     versions: Array<RunnableVersion>,
   ): Promise<RunResult> {
     const { appState } = this;
diff --git a/src/renderer/state.ts b/src/renderer/state.ts
index 915f62b0..d422d7ec 100644
--- a/src/renderer/state.ts
+++ b/src/renderer/state.ts
@@ -147,6 +147,7 @@ export class AppState {
 
   @observable public activeGistAction: GistActionState = GistActionState.none;
   @observable public isRunning = false;
+  @observable public isAutoBisecting = false;
   @observable public isInstallingModules = false;
   @observable public isUnsaved: boolean;
   @observable public isUpdatingElectronVersions = false;


Test patch:
diff --git a/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap b/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap
index 84940e53..3a6a3cb0 100644
--- a/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap
+++ b/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap
@@ -104,7 +104,6 @@ exports[`VersionSelect component renders 1`] = `
         "version": "2.0.2",
       }
     }
-    disabled={false}
     onVersionSelect={[Function]}
   />
 </Blueprint3.ButtonGroup>
diff --git a/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap b/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap
index 015df037..38059a6c 100644
--- a/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap
+++ b/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap
@@ -221,6 +221,13 @@ exports[`BisectDialog component renders 1`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={false}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -439,6 +446,13 @@ exports[`BisectDialog component renders 2`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -664,6 +678,13 @@ exports[`BisectDialog component renders 3`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -896,6 +917,13 @@ exports[`BisectDialog component renders 4`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -1129,6 +1157,13 @@ exports[`BisectDialog component renders 5`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
diff --git a/tests/renderer/components/dialog-bisect-spec.tsx b/tests/renderer/components/dialog-bisect-spec.tsx
index b8f59204..f1994de1 100644
--- a/tests/renderer/components/dialog-bisect-spec.tsx
+++ b/tests/renderer/components/dialog-bisect-spec.tsx
@@ -1,6 +1,10 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { VersionSource, VersionState } from '../../../src/interfaces';
+import {
+  RunResult,
+  VersionSource,
+  VersionState,
+} from '../../../src/interfaces';
 import { Bisector } from '../../../src/renderer/bisect';
 import { BisectDialog } from '../../../src/renderer/components/dialog-bisect';
 import { ElectronReleaseChannel } from '../../../src/renderer/versions';
@@ -137,6 +141,54 @@ describe('BisectDialog component', () => {
     });
   });
 
+  describe('onAuto()', () => {
+    it('initiates autobisect', async () => {
+      // setup: dialog state
+      const wrapper = shallow(<BisectDialog appState={store} />);
+      wrapper.setState({
+        allVersions: generateVersionRange(5),
+        endIndex: 0,
+        startIndex: 4,
+      });
+
+      // setup: the spy
+      const spy = jest
+        .spyOn(window.ElectronFiddle.app.runner, 'autobisect')
+        .mockResolvedValueOnce(RunResult.SUCCESS);
+
+      // click the 'auto' button
+      const instance1: BisectDialog = wrapper.instance() as any;
+      await instance1.onAuto();
+
+      // check the results
+      expect(spy).toHaveBeenCalled();
+
+      // cleanup
+      spy.mockRestore();
+    });
+
+    it('does nothing if endIndex or startIndex are falsy', async () => {
+      const wrapper = shallow(<BisectDialog appState={store} />);
+
+      wrapper.setState({
+        startIndex: undefined,
+        endIndex: 0,
+      });
+      const instance1: BisectDialog = wrapper.instance() as any;
+      await instance1.onAuto();
+      expect(Bisector).not.toHaveBeenCalled();
+
+      wrapper.setState({
+        startIndex: 4,
+        endIndex: undefined,
+      });
+
+      const instance2: BisectDialog = wrapper.instance() as any;
+      await instance2.onAuto();
+      expect(Bisector).not.toHaveBeenCalled();
+    });
+  });
+
   describe('items disabled', () => {
     let instance: BisectDialog;
 
diff --git a/tests/renderer/components/version-select-spec.tsx b/tests/renderer/components/version-select-spec.tsx
index bf359e49..394ba10e 100644
--- a/tests/renderer/components/version-select-spec.tsx
+++ b/tests/renderer/components/version-select-spec.tsx
@@ -16,7 +16,7 @@ import {
 import { ElectronReleaseChannel } from '../../../src/renderer/versions';
 import { mockVersions } from '../../mocks/electron-versions';
 
-const { ready, unknown, downloading } = VersionState;
+const { downloading, ready, unknown, unzipping } = VersionState;
 const { remote, local } = VersionSource;
 
 describe('VersionSelect component', () => {
@@ -133,14 +133,15 @@ describe('VersionSelect component', () => {
 
   describe('getItemIcon()', () => {
     it('returns the correct icon', () => {
-      const vDownloaded = { ...mockVersion1, state: ready };
-      expect(getItemIcon(vDownloaded)).toBe('saved');
-
-      const vDownloading = { ...mockVersion1, state: downloading };
-      expect(getItemIcon(vDownloading)).toBe('cloud-download');
-
-      const vUnknown = { ...mockVersion1, state: unknown };
-      expect(getItemIcon(vUnknown)).toBe('cloud');
+      const icons: Array<{ state: VersionState; expected: string }> = [
+        { state: downloading, expected: 'cloud-download' },
+        { state: ready, expected: 'saved' },
+        { state: unknown, expected: 'cloud' },
+        { state: unzipping, expected: 'compressed' },
+      ];
+      icons.forEach(({ state, expected }) => {
+        expect(getItemIcon({ ...mockVersion1, state })).toBe(expected);
+      });
     });
   });