Repository: electron/fiddle
Pull request #657 (2021-04-14 09:10:24)

test: add test coverage for AddThemeDialog
This PR improves test coverage for `AddThemeDialog`, which was sorely lacking.

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

Coverage increased (+1.04%) to 95.132% when pulling **a20ca3ad932bf776566f643ac9d9ac1e3764251f on improve-some-coverage** into **196c00a460bbb6fafff49885557e6e4e3a490ea3 on master**.


Patch:
diff --git a/src/renderer/binary.ts b/src/renderer/binary.ts
index 6cb82f19..3ccee0df 100644
--- a/src/renderer/binary.ts
+++ b/src/renderer/binary.ts
@@ -203,7 +203,7 @@ async function download(ver: RunnableVersion): Promise<string> {
     const percent = Math.round(progress.percent * 100) / 100;
     if (ver.downloadProgress !== percent) {
       ver.downloadProgress = percent;
-      console.debug(`Binary: Version ${version} download progress: ${percent}`);
+      console.info(`Binary: Version ${version} download progress: ${percent}`);
     }
   };
 
diff --git a/src/renderer/components/dialog-add-theme.tsx b/src/renderer/components/dialog-add-theme.tsx
index cf63ddd1..47278b19 100644
--- a/src/renderer/components/dialog-add-theme.tsx
+++ b/src/renderer/components/dialog-add-theme.tsx
@@ -1,6 +1,6 @@
 import { Button, Dialog, FileInput } from '@blueprintjs/core';
 import { shell } from 'electron';
-import * as fsType from 'fs-extra';
+import * as fs from 'fs-extra';
 import { observer } from 'mobx-react';
 import * as MonacoType from 'monaco-editor';
 import * as path from 'path';
@@ -43,19 +43,19 @@ export class AddThemeDialog extends React.Component<
   }
 
   /**
-   * Handles a change of the file input
+   * Handles a change of the file input.
    *
    * @param {React.ChangeEvent<HTMLInputElement>} event
    */
   public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
     const { files } = event.target as any;
-    const file = files && files[0] ? files[0] : undefined;
+    const file = files?.[0] ;
 
     this.setState({ file });
   }
 
   /**
-   * Handles the submission of the dialog
+   * Handles the submission of the dialog.
    *
    * @returns {Promise<void>}
    */
@@ -66,10 +66,11 @@ export class AddThemeDialog extends React.Component<
     const defaultTheme = !!appState.theme
       ? await getTheme(appState.theme)
       : defaultDark;
+
     if (!file) return;
 
     try {
-      const editor = fsType.readJSONSync(file.path);
+      const editor = fs.readJSONSync(file.path);
       if (!editor.base && !editor.rules)
         throw Error('File does not match specifications'); // has to have these attributes
       defaultTheme.editor = editor as Partial<MonacoType.editor.IStandaloneThemeData>;
@@ -79,9 +80,9 @@ export class AddThemeDialog extends React.Component<
     } catch (error) {
       appState.setGenericDialogOptions({
         type: GenericDialogType.warning,
-        label: `Error: ${error}, please pick a different file.`,
+        label: `${error}, please pick a different file.`,
       });
-      appState.isGenericDialogShowing = true;
+      appState.toggleGenericDialog();
       return;
     }
 
@@ -92,11 +93,14 @@ export class AddThemeDialog extends React.Component<
   public async createNewThemeFromMonaco(
     name: string,
     newTheme: LoadedFiddleTheme,
-  ): Promise<boolean> {
-    if (!name) return false;
+  ): Promise<void> {
+    if (!name) {
+      throw new Error(`Filename ${name} not found`);
+    }
+
     const themePath = path.join(THEMES_PATH, `${name}`);
 
-    await fsType.outputJSON(
+    await fs.outputJSON(
       themePath,
       {
         ...newTheme,
@@ -107,7 +111,6 @@ export class AddThemeDialog extends React.Component<
 
     this.props.appState.setTheme(themePath);
     shell.showItemInFolder(themePath);
-    return true;
   }
 
   get buttons() {


Test patch:
diff --git a/tests/renderer/components/__snapshots__/dialog-add-theme-spec.tsx.snap b/tests/renderer/components/__snapshots__/dialog-add-theme-spec.tsx.snap
new file mode 100644
index 00000000..6085d6b0
--- /dev/null
+++ b/tests/renderer/components/__snapshots__/dialog-add-theme-spec.tsx.snap
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AddThemeDialog component renders 1`] = `
+<Blueprint3.Dialog
+  canOutsideClickClose={true}
+  className="dialog-add-version"
+  isOpen={false}
+  onClose={[Function]}
+  title="Add theme"
+>
+  <div
+    className="bp3-dialog-body"
+  >
+    <Blueprint3.FileInput
+      hasSelection={false}
+      inputProps={
+        Object {
+          "accept": ".json",
+        }
+      }
+      onInputChange={[Function]}
+      text="Select the Monaco file..."
+    />
+    <br />
+  </div>
+  <div
+    className="bp3-dialog-footer"
+  >
+    <div
+      className="bp3-dialog-footer-actions"
+    >
+      <Blueprint3.Button
+        disabled={false}
+        icon="add"
+        key="submit"
+        onClick={[Function]}
+        text="Add"
+      />
+      <Blueprint3.Button
+        icon="cross"
+        key="cancel"
+        onClick={[Function]}
+        text="Cancel"
+      />
+    </div>
+  </div>
+</Blueprint3.Dialog>
+`;
diff --git a/tests/renderer/components/__snapshots__/settings-general-block-accelerators-spec.tsx.snap b/tests/renderer/components/__snapshots__/settings-general-block-accelerators-spec.tsx.snap
new file mode 100644
index 00000000..b26f494c
--- /dev/null
+++ b/tests/renderer/components/__snapshots__/settings-general-block-accelerators-spec.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BlockAcceleratorsSettings component renders 1`] = `
+<div>
+  <h4>
+    Block Keyboard Shortcuts
+  </h4>
+  <Blueprint3.Callout>
+    <Blueprint3.FormGroup
+      label="Any keyboard shortcuts checked below will be disabled."
+    >
+      <Blueprint3.Checkbox
+        checked={false}
+        label="Save"
+        onChange={[Function]}
+        value="save"
+      />
+      <Blueprint3.Checkbox
+        checked={false}
+        label="Save as"
+        onChange={[Function]}
+        value="saveAs"
+      />
+    </Blueprint3.FormGroup>
+  </Blueprint3.Callout>
+</div>
+`;
diff --git a/tests/renderer/components/dialog-add-theme-spec.tsx b/tests/renderer/components/dialog-add-theme-spec.tsx
new file mode 100644
index 00000000..2d466d41
--- /dev/null
+++ b/tests/renderer/components/dialog-add-theme-spec.tsx
@@ -0,0 +1,160 @@
+import { shell } from 'electron';
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import * as fs from 'fs-extra';
+
+import { AddThemeDialog } from '../../../src/renderer/components/dialog-add-theme';
+import { overridePlatform, resetPlatform } from '../../utils';
+import {
+  defaultLight,
+  LoadedFiddleTheme,
+} from '../../../src/renderer/themes-defaults';
+
+jest.mock('../../../src/renderer/ipc');
+jest.mock('../../../src/renderer/binary');
+
+describe('AddThemeDialog component', () => {
+  let store: any;
+
+  beforeAll(() => {
+    // We render the buttons different depending on the
+    // platform, so let' have a uniform platform for unit tests
+    overridePlatform('darwin');
+  });
+
+  afterAll(() => {
+    resetPlatform();
+  });
+
+  beforeEach(() => {
+    store = {
+      setTheme: jest.fn(),
+      setGenericDialogOptions: jest.fn(),
+      toggleGenericDialog: jest.fn(),
+    };
+  });
+
+  it('renders', () => {
+    const wrapper = shallow(<AddThemeDialog appState={store} />);
+
+    wrapper.setState({
+      file: '/test/file',
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  describe('createNewThemeFromMonaco()', () => {
+    it('handles invalid input', async () => {
+      const wrapper = shallow(<AddThemeDialog appState={store} />);
+      const instance: AddThemeDialog = wrapper.instance() as any;
+
+      try {
+        await instance.createNewThemeFromMonaco('', {} as LoadedFiddleTheme);
+      } catch (err) {
+        expect(err.message).toEqual(`Filename  not found`);
+        expect(fs.outputJSON).toHaveBeenCalledTimes(0);
+        expect(store.setTheme).toHaveBeenCalledTimes(0);
+        expect(shell.showItemInFolder).toHaveBeenCalledTimes(0);
+      }
+    });
+
+    it('handles valid input', async () => {
+      const wrapper = shallow(<AddThemeDialog appState={store} />);
+      const instance: AddThemeDialog = wrapper.instance() as any;
+      wrapper.setState({ file: '/test/file' });
+
+      (fs.outputJSON as jest.Mock).mockResolvedValue({});
+
+      await instance.createNewThemeFromMonaco('testingLight', defaultLight);
+
+      expect(fs.outputJSON).toHaveBeenCalled();
+
+      const args = (fs.outputJSON as jest.Mock).mock.calls[0];
+      expect(args[0].includes(`.electron-fiddle`)).toBe(true);
+      expect(args[1].name).toEqual('testingLight');
+      expect(args[1].common).toBeDefined();
+      expect(args[1].file).toBeDefined();
+
+      const themePath = '~/.electron-fiddle/themes/testingLight';
+      expect(store.setTheme).toHaveBeenCalledWith(themePath);
+      expect(shell.showItemInFolder).toHaveBeenCalledWith(themePath);
+    });
+  });
+
+  describe('onSubmit()', () => {
+    it('does nothing if there is no file currently set', async () => {
+      const wrapper = shallow(<AddThemeDialog appState={store} />);
+      const instance: AddThemeDialog = wrapper.instance() as any;
+
+      instance.createNewThemeFromMonaco = jest.fn();
+      instance.onClose = jest.fn();
+
+      await instance.onSubmit();
+
+      expect(fs.readJSONSync).toHaveBeenCalledTimes(0);
+      expect(instance.createNewThemeFromMonaco).toHaveBeenCalledTimes(0);
+      expect(instance.onClose).toHaveBeenCalledTimes(0);
+    });
+
+    it('loads a theme if a file is currently set', async () => {
+      const wrapper = shallow(<AddThemeDialog appState={store} />);
+      const instance: AddThemeDialog = wrapper.instance() as any;
+
+      wrapper.setState({ file: '/test/file' });
+
+      (fs.readJSONSync as jest.Mock).mockReturnValue(defaultLight.editor);
+
+      instance.createNewThemeFromMonaco = jest.fn();
+      instance.onClose = jest.fn();
+
+      await instance.onSubmit();
+
+      expect(fs.readJSONSync).toHaveBeenCalledTimes(1);
+      expect(instance.createNewThemeFromMonaco).toHaveBeenCalledTimes(1);
+      expect(instance.onClose).toHaveBeenCalledTimes(1);
+    });
+
+    it('shows an error dialog for a malformed theme', async () => {
+      const wrapper = shallow(<AddThemeDialog appState={store} />);
+      const instance: AddThemeDialog = wrapper.instance() as any;
+
+      wrapper.setState({ file: '/test/file' });
+
+      (fs.readJSONSync as jest.Mock).mockReturnValue({});
+
+      instance.onClose = jest.fn();
+
+      await instance.onSubmit();
+
+      expect(fs.readJSONSync).toHaveBeenCalledTimes(1);
+      const error = 'File does not match specifications';
+      expect(store.setGenericDialogOptions).toHaveBeenCalledWith({
+        label: `Error: ${error}, please pick a different file.`,
+        type: 'warning',
+      });
+      expect(store.toggleGenericDialog).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('onChangeFile()', () => {
+    it('handles valid input', () => {
+      const wrapper = shallow(<AddThemeDialog appState={store} />);
+
+      const files = ['one', 'two'];
+      (wrapper.instance() as any).onChangeFile({
+        target: { files },
+      });
+      expect(wrapper.state('file')).toBe(files[0]);
+    });
+
+    it('handles no input', () => {
+      const wrapper = shallow(<AddThemeDialog appState={store} />);
+
+      (wrapper.instance() as any).onChangeFile({
+        target: { files: null },
+      });
+      expect(wrapper.state('file')).toBeUndefined();
+    });
+  });
+});
diff --git a/tests/renderer/components/settings-general-block-accelerators.tsx b/tests/renderer/components/settings-general-block-accelerators-spec.tsx
similarity index 68%
rename from tests/renderer/components/settings-general-block-accelerators.tsx
rename to tests/renderer/components/settings-general-block-accelerators-spec.tsx
index 30bc5c4d..344f8bc1 100644
--- a/tests/renderer/components/settings-general-block-accelerators.tsx
+++ b/tests/renderer/components/settings-general-block-accelerators-spec.tsx
@@ -8,7 +8,11 @@ describe('BlockAcceleratorsSettings component', () => {
   let store: any;
 
   beforeEach(() => {
-    store = {};
+    store = {
+      acceleratorsToBlock: [],
+      removeAcceleratorToBlock: jest.fn(),
+      addAcceleratorToBlock: jest.fn(),
+    };
   });
 
   it('renders', () => {
@@ -20,17 +24,22 @@ describe('BlockAcceleratorsSettings component', () => {
     it('handles a new selection', async () => {
       const wrapper = shallow(<BlockAcceleratorsSettings appState={store} />);
       const instance = wrapper.instance() as any;
+
       await instance.handleBlockAcceleratorChange({
         currentTarget: { checked: false, value: BlockableAccelerator.save },
       });
 
-      expect(store.acceleratorsToBlock).toBe([]);
+      expect(store.removeAcceleratorToBlock).toHaveBeenCalledWith(
+        BlockableAccelerator.save,
+      );
 
       await instance.handleBlockAcceleratorChange({
-        currentTarget: { checked: false, value: BlockableAccelerator.save },
+        currentTarget: { checked: true, value: BlockableAccelerator.save },
       });
 
-      expect(store.acceleratorsToBlock).toBe([BlockableAccelerator.save]);
+      expect(store.addAcceleratorToBlock).toHaveBeenCalledWith(
+        BlockableAccelerator.save,
+      );
     });
   });
 });