Repository: electron/fiddle
Pull request #691 (2021-05-06 19:29:03)

build(deps): bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/lodash/lodash/commit/f299b52f39486275a9e6483b60a410e06520c538"><code>f299b52</code></a> Bump to v4.17.21</li>
<li><a href="https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a"><code>c4847eb</code></a> Improve performance of <code>toNumber</code>, <code>trim</code> and <code>trimEnd</code> on large input strings</li>
<li><a href="https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c"><code>3469357</code></a> Prevent command injection through <code>_.template</code>'s <code>variable</code> option</li>
<li>See full diff in <a href="https://github.com/lodash/lodash/compare/4.17.20...4.17.21">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=lodash&package-manager=npm_and_yarn&previous-version=4.17.20&new-version=4.17.21)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/electron/fiddle/network/alerts).

</details>

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

Coverage remained the same at 95.659% when pulling **ab4a59d738fccf58cc568cc738b863f2b02a54a6 on dependabot/npm_and_yarn/lodash-4.17.21** into **c28b3e19957d7173cd10be1872cb0f87af623034 on master**.


Patch:
diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx
index 95b400e1..c965551e 100644
--- a/src/renderer/app.tsx
+++ b/src/renderer/app.tsx
@@ -6,22 +6,13 @@ import * as MonacoType from 'monaco-editor';
 
 import { ipcRenderer } from 'electron';
 import {
-  DefaultEditorId,
-  EditorId,
   EditorValues,
   GenericDialogType,
   PACKAGE_NAME,
   SetFiddleOptions,
 } from '../interfaces';
 import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
-import { updateEditorLayout } from '../utils/editor-layout';
-import { getEditorValue } from '../utils/editor-value';
 import { getPackageJson, PackageJsonOptions } from '../utils/get-package';
-import {
-  compareEditors,
-  getEmptyContent,
-  isKnownFile,
-} from '../utils/editor-utils';
 import { FileManager } from './file-manager';
 import { RemoteLoader } from './remote-loader';
 import { Runner } from './runner';
@@ -48,7 +39,6 @@ export class App {
 
   constructor() {
     this.getEditorValues = this.getEditorValues.bind(this);
-    this.setEditorValues = this.setEditorValues.bind(this);
 
     this.taskRunner = new TaskRunner(this);
   }
@@ -72,69 +62,16 @@ export class App {
       }
     }
 
-    // Remove all previously created custom editors.
-    this.state.customMosaics = Object.keys(editorValues).filter(
-      (filename: string) => !isKnownFile(filename),
-    ) as EditorId[];
-
-    // If the gist content is empty or matches the empty file output, don't show it.
-    const shouldShow = (id: EditorId, val?: string) => {
-      return !!val && val.length > 0 && val !== getEmptyContent(id);
-    };
-
-    // Sort and display all editors that have content.
-    const visibleEditors: EditorId[] = Object.entries(editorValues)
-      .filter(([id, content]) => shouldShow(id as EditorId, content as string))
-      .map(([id]) => id as DefaultEditorId)
-      .sort(compareEditors);
+    await this.state.editorMosaic.set(editorValues);
 
     this.state.gistId = gistId || '';
     this.state.localPath = filePath;
     this.state.templateName = templateName;
-
-    // Once loaded, we have a "saved" state.
-    await this.state.setVisibleMosaics(visibleEditors);
-    await this.setEditorValues(editorValues);
     this.state.isUnsaved = false;
 
     return true;
   }
 
-  /**
-   * Sets the contents of all editor panes.
-   *
-   * @param {EditorValues} values
-   */
-  public async setEditorValues(values: EditorValues): Promise<void> {
-    const { ElectronFiddle: fiddle } = window;
-
-    if (!fiddle?.app) {
-      throw new Error('Fiddle not ready');
-    }
-
-    // Set content for mosaics.
-    const { closedPanels, editorMosaic } = this.state;
-    for (const [name, content] of Object.entries(values)) {
-      const backup = closedPanels[name];
-      if (backup) {
-        // The editor does not exist, attempt to set it on the backup.
-        // If there's a model, we'll do it on the model. Else, we'll
-        // set the value.
-        if (backup.model) {
-          backup.model.setValue(values[name]!);
-        } else {
-          backup.value = values[name]!;
-        }
-      } else {
-        const editor = editorMosaic.editors.get(name as EditorId);
-        // The editor exists, set the value directly
-        if (editor && editor.getValue() !== content) {
-          editor.setValue(content as string);
-        }
-      }
-    }
-  }
-
   /**
    * Retrieves the contents of all editor panes.
    *
@@ -143,16 +80,7 @@ export class App {
   public async getEditorValues(
     options?: PackageJsonOptions,
   ): Promise<EditorValues> {
-    const { ElectronFiddle: fiddle } = window;
-
-    if (!fiddle?.app) {
-      throw new Error('Fiddle not ready');
-    }
-
-    const values: EditorValues = {};
-    for (const name of this.state.editorMosaic.editors.keys()) {
-      values[name] = getEditorValue(name);
-    }
+    const values = this.state.editorMosaic.values();
 
     if (options && options.include !== false) {
       values[PACKAGE_NAME] = await getPackageJson(this.state, values, options);
@@ -269,7 +197,7 @@ export class App {
    * is resized. This method sets up the listener.
    */
   public setupResizeListener(): void {
-    window.addEventListener('resize', updateEditorLayout);
+    window.addEventListener('resize', this.state.editorMosaic.layout);
   }
 
   /**
diff --git a/src/renderer/components/commands-action-button.tsx b/src/renderer/components/commands-action-button.tsx
index 17edd28f..941ca06c 100644
--- a/src/renderer/components/commands-action-button.tsx
+++ b/src/renderer/components/commands-action-button.tsx
@@ -424,7 +424,7 @@ export class GistActionButton extends React.Component<
   };
 
   private gistFilesList = (values: EditorValues) => {
-    const { customMosaics } = this.props.appState;
+    const { customMosaics } = this.props.appState.editorMosaic;
 
     const filesList = {};
 
diff --git a/src/renderer/components/commands-editors.tsx b/src/renderer/components/commands-editors.tsx
index 3c80be0d..741f9aba 100644
--- a/src/renderer/components/commands-editors.tsx
+++ b/src/renderer/components/commands-editors.tsx
@@ -65,9 +65,14 @@ export class EditorDropdown extends React.Component<
   public renderMenuItems() {
     const { appState } = this.props;
     const result: Array<JSX.Element> = [];
-    const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
+    const visibleMosaics = getVisibleMosaics(
+      appState.editorMosaic.mosaicArrangement,
+    );
 
-    const allEditors = [...DEFAULT_EDITORS, ...appState.customMosaics];
+    const allEditors = [
+      ...DEFAULT_EDITORS,
+      ...appState.editorMosaic.customMosaics,
+    ];
     for (const id of allEditors) {
       const icon = visibleMosaics.includes(id) ? 'eye-open' : 'eye-off';
       const title = getEditorTitle(id);
@@ -81,7 +86,7 @@ export class EditorDropdown extends React.Component<
             id={id}
             onClick={this.onItemClick}
             // Can't hide last editor panel.
-            disabled={appState.mosaicArrangement === id}
+            disabled={appState.editorMosaic.mosaicArrangement === id}
           >
             <MenuItem
               icon={'cross'}
@@ -100,7 +105,7 @@ export class EditorDropdown extends React.Component<
             id={id}
             onClick={this.onItemClick}
             // Can't hide last editor panel.
-            disabled={appState.mosaicArrangement === id}
+            disabled={appState.editorMosaic.mosaicArrangement === id}
           />,
         );
       }
@@ -125,7 +130,7 @@ export class EditorDropdown extends React.Component<
           icon="grid-view"
           key="reset-layout"
           text="Reset Layout"
-          onClick={appState.resetEditorLayout}
+          onClick={appState.editorMosaic.resetEditorLayout}
         />
       </React.Fragment>,
     );
@@ -176,7 +181,7 @@ export class EditorDropdown extends React.Component<
 
       // Also fail if the user tries to create two identical editors.
       if (
-        appState.customMosaics.includes(name) ||
+        appState.editorMosaic.customMosaics.includes(name) ||
         Object.values(DefaultEditorId).includes(name as DefaultEditorId)
       ) {
         appState.setGenericDialogOptions({
@@ -187,8 +192,8 @@ export class EditorDropdown extends React.Component<
 
         appState.toggleGenericDialog();
       } else {
-        appState.customMosaics.push(name);
-        appState.showMosaic(name);
+        appState.editorMosaic.customMosaics.push(name);
+        appState.editorMosaic.showMosaic(name);
       }
     }
   }
@@ -198,20 +203,23 @@ export class EditorDropdown extends React.Component<
     const { appState } = this.props;
 
     console.log(`EditorDropdown: Removing custom editor ${id}`);
-    appState.removeCustomMosaic(id as EditorId);
+    appState.editorMosaic.removeCustomMosaic(id as EditorId);
   }
 
   public onItemClick(event: React.MouseEvent) {
     const { id } = event.currentTarget;
     const { appState } = this.props;
-    const visibleMosaics = getVisibleMosaics(appState.mosaicArrangement);
+    const { editorMosaic } = appState;
+    const visibleMosaics = getVisibleMosaics(
+      appState.editorMosaic.mosaicArrangement,
+    );
 
     if (visibleMosaics.includes(id as EditorId)) {
       console.log(`EditorDropdown: Closing ${id}`);
-      appState.hideAndBackupMosaic(id as EditorId);
+      editorMosaic.hideAndBackupMosaic(id as EditorId);
     } else {
       console.log(`EditorDropdown: Opening ${id}`);
-      appState.showMosaic(id as EditorId);
+      editorMosaic.showMosaic(id as EditorId);
     }
   }
 }
diff --git a/src/renderer/components/editor.tsx b/src/renderer/components/editor.tsx
index a1dafd9b..1b254ba2 100644
--- a/src/renderer/components/editor.tsx
+++ b/src/renderer/components/editor.tsx
@@ -136,7 +136,7 @@ export class Editor extends React.Component<EditorProps> {
     const { appState, id } = this.props;
     const { version } = appState;
 
-    const backup = appState.getAndRemoveEditorValueBackup(id);
+    const backup = appState.editorMosaic.getAndRemoveEditorValueBackup(id);
 
     if (backup) {
       console.log(`Editor: Backup found, restoring state`);
diff --git a/src/renderer/components/editors-non-ideal-state.tsx b/src/renderer/components/editors-non-ideal-state.tsx
index 40d6cb92..f1f199c2 100644
--- a/src/renderer/components/editors-non-ideal-state.tsx
+++ b/src/renderer/components/editors-non-ideal-state.tsx
@@ -15,7 +15,7 @@ export function renderNonIdealState(appState: AppState) {
   const resolveButton = (
     <Button
       text="Open all editors"
-      onClick={() => appState.setVisibleMosaics(allEditors)}
+      onClick={() => appState.editorMosaic.setVisibleMosaics(allEditors)}
     />
   );
 
diff --git a/src/renderer/components/editors-toolbar-button.tsx b/src/renderer/components/editors-toolbar-button.tsx
index 2461a972..115e4b8b 100644
--- a/src/renderer/components/editors-toolbar-button.tsx
+++ b/src/renderer/components/editors-toolbar-button.tsx
@@ -58,7 +58,7 @@ export class RemoveButton extends ToolbarButton {
    */
   public createButton(_mosaicActions: MosaicRootActions<any>) {
     const onClick = () =>
-      this.props.appState.hideAndBackupMosaic(this.props.id);
+      this.props.appState.editorMosaic.hideAndBackupMosaic(this.props.id);
 
     return <Button icon="cross" className="bp3-small" onClick={onClick} />;
   }
diff --git a/src/renderer/components/editors.tsx b/src/renderer/components/editors.tsx
index 9a7ddb31..c089f75b 100644
--- a/src/renderer/components/editors.tsx
+++ b/src/renderer/components/editors.tsx
@@ -1,4 +1,3 @@
-import { reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as MonacoType from 'monaco-editor';
 import * as React from 'react';
@@ -12,7 +11,6 @@ import {
 
 import { EditorId, SetFiddleOptions } from '../../interfaces';
 import { IpcEvents } from '../../ipc-events';
-import { updateEditorLayout } from '../../utils/editor-layout';
 import { getFocusedEditor } from '../../utils/focused-editor';
 import { getAtPath, setAtPath } from '../../utils/js-path';
 import { toggleMonaco } from '../../utils/toggle-monaco';
@@ -45,13 +43,6 @@ interface EditorsState {
 
 @observer
 export class Editors extends React.Component<EditorsProps, EditorsState> {
-  // A reaction: Each time mosaicArrangement is changed, we'll update
-  // the editor layout. That method is itself debounced.
-  public disposeLayoutAutorun = reaction(
-    () => this.props.appState.mosaicArrangement,
-    () => updateEditorLayout(),
-  );
-
   constructor(props: EditorsProps) {
     super(props);
 
@@ -116,7 +107,6 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
   }
 
   public componentWillUnmount() {
-    this.disposeLayoutAutorun();
     this.stopListening();
   }
 
@@ -188,8 +178,8 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
 
     // only show toolbar controls if we have more than 1 visible editor
     // Mosaic arrangement is type string if 1 editor, object otherwise
-    const toolbarControlsMaybe = typeof appState.mosaicArrangement !==
-      'string' && (
+    const toolbarControlsMaybe = typeof appState.editorMosaic
+      .mosaicArrangement !== 'string' && (
       <>
         <MaximizeButton id={id} appState={appState} />
         <RemoveButton id={id} appState={appState} />
@@ -267,7 +257,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
       <Mosaic<EditorId>
         className={`focused__${this.state.focused}`}
         onChange={this.onChange}
-        value={appState.mosaicArrangement}
+        value={appState.editorMosaic.mosaicArrangement}
         zeroStateView={renderNonIdealState(appState)}
         renderTile={this.renderTile}
       />
@@ -280,7 +270,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
    * @param {(MosaicNode<EditorId> | null)} currentNode
    */
   public onChange(currentNode: MosaicNode<EditorId> | null) {
-    this.props.appState.mosaicArrangement = currentNode;
+    this.props.appState.editorMosaic.mosaicArrangement = currentNode;
   }
 
   /**
diff --git a/src/renderer/constants.ts b/src/renderer/constants.ts
index f668a175..607d229b 100644
--- a/src/renderer/constants.ts
+++ b/src/renderer/constants.ts
@@ -2,7 +2,6 @@ import * as path from 'path';
 import { MosaicNode } from 'react-mosaic-component';
 
 import { DefaultEditorId, EditorId } from '../interfaces';
-import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
 
 export const USER_DATA_PATH = window.ElectronFiddle.appPaths.userData;
 export const CONFIG_PATH = path.join(
@@ -32,11 +31,5 @@ export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<EditorId> = {
   },
 };
 
-export const DEFAULT_CLOSED_PANELS: Partial<
-  Record<EditorId, EditorBackup | true>
-> = {
-  'styles.css': getEditorBackup(DefaultEditorId.css),
-};
-
 export const ELECTRON_ORG = 'electron';
 export const ELECTRON_REPO = 'electron';
diff --git a/src/renderer/editor-mosaic.ts b/src/renderer/editor-mosaic.ts
index 30efa2f9..88aa2892 100644
--- a/src/renderer/editor-mosaic.ts
+++ b/src/renderer/editor-mosaic.ts
@@ -1,10 +1,272 @@
 import * as MonacoType from 'monaco-editor';
-import { observable } from 'mobx';
+import { MosaicNode } from 'react-mosaic-component';
+import { action, observable, reaction } from 'mobx';
 
-import { EditorId } from '../interfaces';
+import {
+  createMosaicArrangement,
+  getVisibleMosaics,
+} from '../utils/editors-mosaic-arrangement';
+import { DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
+import {
+  DEFAULT_EDITORS,
+  DefaultEditorId,
+  EditorId,
+  EditorValues,
+} from '../interfaces';
+import { waitForEditorsToMount } from '../utils/editor-mounted';
+import {
+  compareEditors,
+  getEmptyContent,
+  isKnownFile,
+} from '../utils/editor-utils';
 
 export type Editor = MonacoType.editor.IStandaloneCodeEditor;
 
+export interface EditorBackup {
+  value?: string;
+  model?: MonacoType.editor.ITextModel | null;
+  viewState?: MonacoType.editor.ICodeEditorViewState | null;
+}
+
 export class EditorMosaic {
   @observable public readonly editors: Map<EditorId, Editor> = new Map();
+  @observable public customMosaics: EditorId[] = [];
+  @observable
+  public mosaicArrangement: MosaicNode<EditorId> | null = DEFAULT_MOSAIC_ARRANGEMENT;
+  @observable public closedPanels: Record<EditorId, EditorBackup> = {};
+
+  constructor() {
+    for (const name of [
+      'getAndRemoveEditorValueBackup',
+      'hideAndBackupMosaic',
+      'layout',
+      'removeCustomMosaic',
+      'resetEditorLayout',
+      'set',
+      'setVisibleMosaics',
+      'showMosaic',
+      'values',
+    ]) {
+      this[name] = this[name].bind(this);
+    }
+    // A reaction: Each time mosaicArrangement is changed, we'll update
+    // the editor layout. That method is itself debounced.
+    reaction(
+      () => this.mosaicArrangement,
+      () => this.layout(),
+    );
+  }
+
+  /**
+   * Sets the editor value for a given editor. Deletes the value after
+   * accessing it.
+   *
+   * @param {EditorId} id
+   */
+  @action public getAndRemoveEditorValueBackup(
+    id: EditorId,
+  ): EditorBackup | null {
+    const value = this.closedPanels[id];
+    delete this.closedPanels[id];
+    return value;
+  }
+
+  @action public async setVisibleMosaics(visible: Array<EditorId>) {
+    const { editors, mosaicArrangement } = this;
+    const currentlyVisible = getVisibleMosaics(mosaicArrangement);
+
+    for (const id of DEFAULT_EDITORS) {
+      if (!visible.includes(id) && currentlyVisible.includes(id)) {
+        this.closedPanels[id] = this.getEditorBackup(id);
+        // if we have backup, remove active editor
+        editors.delete(id);
+      }
+    }
+
+    const updatedArrangement = createMosaicArrangement(visible);
+    console.log(
+      `State: Setting visible mosaic panels`,
+      visible,
+      updatedArrangement,
+    );
+
+    this.mosaicArrangement = updatedArrangement;
+
+    // after the mosaicArrangement loads, we want to wait for the Mosaic editors to
+    // mount to ensure that we can load content into the editors as soon as they're
+    // declared visible.
+
+    await waitForEditorsToMount(visible);
+  }
+
+  /**
+   * Hides the panel for a given EditorId.
+   *
+   * @param {EditorId} id
+   */
+  @action public hideAndBackupMosaic(id: EditorId) {
+    const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
+    this.setVisibleMosaics(currentlyVisible.filter((v) => v !== id));
+  }
+
+  /**
+   * Removes the panel for a given custom EditorId.
+   *
+   * @param {EditorId} id
+   */
+  @action public removeCustomMosaic(id: EditorId) {
+    this.hideAndBackupMosaic(id);
+    this.editors.delete(id);
+    this.customMosaics = this.customMosaics.filter((mosaic) => mosaic !== id);
+  }
+
+  /**
+   * Shows the editor value for a given editor.
+   *
+   * @param {EditorId} id
+   */
+  @action public showMosaic(id: EditorId) {
+    const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
+    this.setVisibleMosaics([...currentlyVisible, id]);
+  }
+
+  /**
+   * Resets editor view to default layout.
+   *
+   *
+   */
+  @action public resetEditorLayout() {
+    this.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
+  }
+
+  @action public async set(editorValues: EditorValues) {
+    // Remove all previously created custom editors.
+    this.customMosaics = Object.keys(editorValues).filter(
+      (filename: string) => !isKnownFile(filename),
+    ) as EditorId[];
+
+    // If the gist content is empty or matches the empty file output, don't show it.
+    const shouldShow = (id: EditorId, val?: string) => {
+      return !!val && val.length > 0 && val !== getEmptyContent(id);
+    };
+
+    // Sort and display all editors that have content.
+    const visibleEditors: EditorId[] = Object.entries(editorValues)
+      .filter(([id, content]) => shouldShow(id as EditorId, content as string))
+      .map(([id]) => id as DefaultEditorId)
+      .sort(compareEditors);
+
+    // Once loaded, we have a "saved" state.
+    await this.setVisibleMosaics(visibleEditors);
+
+    // Set content for mosaics.
+    for (const [name, value] of Object.entries(editorValues)) {
+      const editor = this.editors.get(name as EditorId);
+      if (editor) {
+        // The editor exists, set the value directly
+        if (editor.getValue() !== value) {
+          editor.setValue(value as string);
+        }
+      } else {
+        // The editor does not exist, attempt to set it on the backup.
+        // If there's a model, we'll do it on the model. Else, we'll
+        // set the value.
+        let backup = this.closedPanels[name];
+        if (!backup) {
+          backup = { value };
+          this.closedPanels[name] = backup;
+        } else if (backup.model) {
+          backup.model.setValue(value);
+        } else {
+          backup.value = value;
+        }
+      }
+    }
+  }
+
+  /**
+   * Retrieves the contents of all editor panes.
+   *
+   * @returns {EditorValues}
+   */
+  public values(): EditorValues {
+    const values: EditorValues = {};
+
+    for (const name of Object.keys(this.closedPanels)) {
+      values[name] = this.getEditorValue(name as EditorId);
+    }
+
+    for (const name of this.editors.keys()) {
+      values[name] = this.getEditorValue(name);
+    }
+
+    return values;
+  }
+
+  /**
+   * Return the value for a given editor
+   *
+   * @param {EditorId} id
+   * @returns {string}
+   */
+  public getEditorValue(id: EditorId): string {
+    const editor = this.editors.get(id);
+    if (editor) return editor.getValue();
+
+    const backup = this.closedPanels[id];
+    if (backup?.value) return backup.value;
+
+    return '';
+  }
+
+  /**
+   * Returns a backup for a given editor
+   *
+   * @param {EditorId} id
+   * @returns {EditorBackup}
+   */
+  public getEditorBackup(id: EditorId): EditorBackup {
+    return {
+      value: this.getEditorValue(id),
+      model: this.getEditorModel(id),
+      viewState: this.getEditorViewState(id),
+    };
+  }
+
+  /**
+   * Returns the view state for a given editor.
+   *
+   * @export
+   * @param {EditorId} id
+   * @returns {(editor.ICodeEditorViewState | null)}
+   */
+  public getEditorViewState(
+    id: EditorId,
+  ): MonacoType.editor.ICodeEditorViewState | null {
+    return this.editors.get(id)?.saveViewState() || null;
+  }
+
+  /**
+   * Return the model for a given editor
+   *
+   * @param {EditorId} id
+   * @returns {editor.ITextModel | null}
+   */
+  public getEditorModel(id: EditorId): MonacoType.editor.ITextModel | null {
+    return this.editors.get(id)?.getModel() || null;
+  }
+
+  private layoutDebounce: any;
+
+  public layout() {
+    const DEBOUNCE_MSEC = 50;
+    if (!this.layoutDebounce) {
+      this.layoutDebounce = setTimeout(() => {
+        for (const editor of this.editors.values()) {
+          editor?.layout?.();
+        }
+        this.layoutDebounce = null;
+      }, DEBOUNCE_MSEC);
+    }
+  }
 }
diff --git a/src/renderer/state.ts b/src/renderer/state.ts
index 4bfc53b8..095ae20a 100644
--- a/src/renderer/state.ts
+++ b/src/renderer/state.ts
@@ -2,12 +2,9 @@ import * as fs from 'fs-extra';
 import * as MonacoType from 'monaco-editor';
 import semver from 'semver';
 import { action, autorun, computed, observable, when } from 'mobx';
-import { MosaicNode } from 'react-mosaic-component';
 
 import {
   BlockableAccelerator,
-  DEFAULT_EDITORS,
-  EditorId,
   ElectronReleaseChannel,
   GenericDialogOptions,
   GenericDialogType,
@@ -22,17 +19,11 @@ import {
   VersionState,
 } from '../interfaces';
 import { IpcEvents } from '../ipc-events';
-import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
-import {
-  createMosaicArrangement,
-  getVisibleMosaics,
-} from '../utils/editors-mosaic-arrangement';
 import { getName } from '../utils/get-name';
 import { normalizeVersion } from '../utils/normalize-version';
 import { removeBinary, setupBinary } from './binary';
 import { Bisector } from './bisect';
 import { EditorMosaic } from './editor-mosaic';
-import { DEFAULT_CLOSED_PANELS, DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
 import { getTemplate, isContentUnchanged } from './content';
 import {
   getLocalTypePathForVersion,
@@ -131,11 +122,8 @@ export class AppState {
     placeholder: '',
   };
   @observable public readonly editorMosaic = new EditorMosaic();
-  @observable public customMosaics: EditorId[] = [];
   @observable public genericDialogLastResult: boolean | null = null;
   @observable public genericDialogLastInput: string | null = null;
-  @observable
-  public mosaicArrangement: MosaicNode<EditorId> | null = DEFAULT_MOSAIC_ARRANGEMENT;
   @observable public templateName: string | undefined;
   @observable public localTypeWatcher: fs.FSWatcher | undefined;
   @observable public Bisector: Bisector | undefined;
@@ -160,11 +148,6 @@ export class AppState {
   @observable public isTourShowing = !localStorage.getItem('hasShownTour');
 
   // -- Editor Values stored when we close the editor ------------------
-  @observable public closedPanels: Record<
-    EditorId,
-    EditorBackup
-  > = DEFAULT_CLOSED_PANELS;
-
   private outputBuffer = '';
   private name: string;
   private readonly defaultVersion: string;
@@ -189,7 +172,6 @@ export class AppState {
     this.toggleSettings = this.toggleSettings.bind(this);
     this.toggleBisectDialog = this.toggleBisectDialog.bind(this);
     this.updateElectronVersions = this.updateElectronVersions.bind(this);
-    this.resetEditorLayout = this.resetEditorLayout.bind(this);
     this.setIsQuitting = this.setIsQuitting.bind(this);
     this.addAcceleratorToBlock = this.addAcceleratorToBlock.bind(this);
     this.removeAcceleratorToBlock = this.removeAcceleratorToBlock.bind(this);
@@ -680,88 +662,6 @@ export class AppState {
     console.warn(error);
   }
 
-  /**
-   * Sets the editor value for a given editor. Deletes the value after
-   * accessing it.
-   *
-   * @param {EditorId} id
-   */
-  @action public getAndRemoveEditorValueBackup(
-    id: EditorId,
-  ): EditorBackup | null {
-    const value = this.closedPanels[id];
-    delete this.closedPanels[id];
-    return value;
-  }
-
-  @action public async setVisibleMosaics(visible: Array<EditorId>) {
-    const { editorMosaic, mosaicArrangement } = this;
-    const currentlyVisible = getVisibleMosaics(mosaicArrangement);
-
-    for (const id of DEFAULT_EDITORS) {
-      if (!visible.includes(id) && currentlyVisible.includes(id)) {
-        this.closedPanels[id] = getEditorBackup(id);
-        // if we have backup, remove active editor
-        editorMosaic.editors.delete(id);
-      }
-    }
-
-    const updatedArrangement = createMosaicArrangement(visible);
-    console.log(
-      `State: Setting visible mosaic panels`,
-      visible,
-      updatedArrangement,
-    );
-
-    this.mosaicArrangement = updatedArrangement;
-
-    // after the mosaicArrangement loads, we want to wait for the Mosaic editors to
-    // mount to ensure that we can load content into the editors as soon as they're
-    // declared visible.
-
-    await waitForEditorsToMount(visible);
-  }
-
-  /**
-   * Hides the panel for a given EditorId.
-   *
-   * @param {EditorId} id
-   */
-  @action public hideAndBackupMosaic(id: EditorId) {
-    const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
-    this.setVisibleMosaics(currentlyVisible.filter((v) => v !== id));
-  }
-
-  /**
-   * Removes the panel for a given custom EditorId.
-   *
-   * @param {EditorId} id
-   */
-  @action public removeCustomMosaic(id: EditorId) {
-    this.hideAndBackupMosaic(id);
-    this.editorMosaic.editors.delete(id);
-    this.customMosaics = this.customMosaics.filter((mosaic) => mosaic !== id);
-  }
-
-  /**
-   * Shows the editor value for a given editor.
-   *
-   * @param {EditorId} id
-   */
-  @action public showMosaic(id: EditorId) {
-    const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
-    this.setVisibleMosaics([...currentlyVisible, id]);
-  }
-
-  /**
-   * Resets editor view to default layout.
-   *
-   *
-   */
-  @action public resetEditorLayout() {
-    this.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
-  }
-
   @action public async addAcceleratorToBlock(acc: BlockableAccelerator) {
     if (!this.acceleratorsToBlock.includes(acc)) {
       this.acceleratorsToBlock = [...this.acceleratorsToBlock, acc];
diff --git a/src/utils/editor-backup.ts b/src/utils/editor-backup.ts
deleted file mode 100644
index 938ed9f2..00000000
--- a/src/utils/editor-backup.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { editor } from 'monaco-editor';
-
-import { EditorId } from '../interfaces';
-import { getEditorModel } from './editor-model';
-import { getEditorValue } from './editor-value';
-import { getEditorViewState } from './editor-viewstate';
-
-export interface EditorBackup {
-  value?: string;
-  model?: editor.ITextModel | null;
-  viewState?: editor.ICodeEditorViewState | null;
-}
-
-/**
- * Returns a backup for a given editor
- *
- * @param {EditorId} id
- * @returns {EditorBackup}
- */
-export function getEditorBackup(id: EditorId): EditorBackup {
-  return {
-    value: getEditorValue(id),
-    model: getEditorModel(id),
-    viewState: getEditorViewState(id),
-  };
-}
diff --git a/src/utils/editor-layout.ts b/src/utils/editor-layout.ts
deleted file mode 100644
index 3aa1027f..00000000
--- a/src/utils/editor-layout.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Attempts to update the layout of all editors. Exported as
- * a debounced version below.
- */
-function _updateEditorLayout() {
-  const { editorMosaic } = window.ElectronFiddle.app.state;
-  editorMosaic.editors.forEach((editor) => editor.layout());
-}
-
-let handle: number;
-export const updateEditorLayout = () => {
-  if (!handle) {
-    handle = window.setTimeout(() => {
-      _updateEditorLayout();
-      handle = 0;
-    }, 100);
-  }
-};
diff --git a/src/utils/editor-model.ts b/src/utils/editor-model.ts
deleted file mode 100644
index bf5c0861..00000000
--- a/src/utils/editor-model.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { editor } from 'monaco-editor';
-
-import { EditorId } from '../interfaces';
-
-/**
- * Return the model for a given editor
- *
- * @param {EditorId} id
- * @returns {editor.ITextModel | null}
- */
-export function getEditorModel(id: EditorId): editor.ITextModel | null {
-  if (window.ElectronFiddle?.app?.state) {
-    const { editorMosaic } = window.ElectronFiddle.app.state;
-
-    const editor = editorMosaic.editors.get(id);
-    if (editor) return editor.getModel();
-  }
-
-  return null;
-}
diff --git a/src/utils/editor-value.ts b/src/utils/editor-value.ts
deleted file mode 100644
index 7f2e9d40..00000000
--- a/src/utils/editor-value.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { EditorId } from '../interfaces';
-
-/**
- * Return the value for a given editor
- *
- * @param {EditorId} id
- * @returns {string}
- */
-export function getEditorValue(id: EditorId): string {
-  if (window.ElectronFiddle?.app?.state) {
-    const { closedPanels, editorMosaic } = window.ElectronFiddle.app.state;
-
-    const editor = editorMosaic.editors.get(id);
-    if (editor) return editor.getValue();
-
-    const backup = closedPanels[id];
-    if (backup?.value) return backup.value;
-  }
-
-  return '';
-}
diff --git a/src/utils/editor-viewstate.ts b/src/utils/editor-viewstate.ts
deleted file mode 100644
index 846a73d5..00000000
--- a/src/utils/editor-viewstate.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { editor } from 'monaco-editor';
-
-import { EditorId } from '../interfaces';
-
-/**
- * Returns the view state for a given editor.
- *
- * @export
- * @param {EditorId} id
- * @returns {(editor.ICodeEditorViewState | null)}
- */
-export function getEditorViewState(
-  id: EditorId,
-): editor.ICodeEditorViewState | null {
-  return (
-    window.ElectronFiddle?.app?.state.editorMosaic.editors
-      .get(id)
-      ?.saveViewState() || null
-  );
-}
diff --git a/yarn.lock b/yarn.lock
index c5a41226..0482d732 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8103,16 +8103,11 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@4.x:
+lodash@4.x, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4:
-  version "4.17.20"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
-  integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
-
 log-symbols@*, log-symbols@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"


Test patch:
diff --git a/tests/mocks/app.ts b/tests/mocks/app.ts
index afa567f0..e9f62199 100644
--- a/tests/mocks/app.ts
+++ b/tests/mocks/app.ts
@@ -10,7 +10,6 @@ import {
 export class AppMock {
   public setup = jest.fn();
   public replaceFiddle = jest.fn();
-  public setEditorValues = jest.fn();
   public getEditorValues = jest.fn().mockResolvedValue({
     [DefaultEditorId.main]: 'main-content',
     [DefaultEditorId.preload]: 'preload-content',
diff --git a/tests/mocks/editor-mosaic.ts b/tests/mocks/editor-mosaic.ts
index d7249921..035bcb70 100644
--- a/tests/mocks/editor-mosaic.ts
+++ b/tests/mocks/editor-mosaic.ts
@@ -1,10 +1,44 @@
 import { MonacoEditorMock } from './monaco-editor';
+import { MosaicNode } from 'react-mosaic-component';
+
 import { observable } from 'mobx';
 
 import { EditorId } from '../../src/interfaces';
+import { EditorBackup } from '../../src/renderer/editor-mosaic';
+
+import { objectDifference } from '../utils';
 
 export type Editor = MonacoEditorMock;
 
 export class EditorMosaicMock {
-  @observable public readonly editors: Map<EditorId, Editor> = new Map();
+  @observable public closedPanels: Record<EditorId, EditorBackup> = {};
+  @observable public customMosaics: EditorId[] = [];
+  @observable public editors: Map<EditorId, Editor> = new Map();
+  @observable public mosaicArrangement: MosaicNode<EditorId> | null = null;
+
+  public getAndRemoveEditorValueBackup = jest.fn();
+  public getEditorBackup = jest
+    .fn()
+    .mockImplementation((id) => this.closedPanels[id]);
+  public getEditorValue = jest
+    .fn()
+    .mockImplementation((id) => this.editors.get(id)?.getValue() || '');
+  public getEditorViewState = jest
+    .fn()
+    .mockImplementation((id) => this.editors.get(id)?.saveViewState() || null);
+  public layout = jest.fn().mockImplementation(() => {
+    this.editors.forEach((editor) => editor.layout());
+  });
+  public hideAndBackupMosaic = jest.fn();
+  public removeCustomMosaic = jest.fn();
+  public resetEditorLayout = jest.fn();
+  public set = jest.fn();
+  public setVisibleMosaics = jest.fn();
+  public showMosaic = jest.fn();
+  public values = jest.fn().mockReturnValue({});
+
+  public toJSON() {
+    const o = objectDifference(this, new EditorMosaicMock());
+    return Object.keys(o).length === 0 ? 'default EditorMosaicMock' : o;
+  }
 }
diff --git a/tests/mocks/monaco-editor.ts b/tests/mocks/monaco-editor.ts
index fda32ef1..286aee22 100644
--- a/tests/mocks/monaco-editor.ts
+++ b/tests/mocks/monaco-editor.ts
@@ -1,3 +1,5 @@
+import { objectDifference } from '../utils';
+
 export class MonacoEditorMock {
   public action = {
     isSupported: jest.fn(),
@@ -34,4 +36,9 @@ export class MonacoEditorMock {
   public updateOptions = jest.fn();
 
   private listener: any;
+
+  public toJSON() {
+    const o = objectDifference(this, new MonacoEditorMock());
+    return Object.keys(o).length === 0 ? 'default MonacoEditorMock' : o;
+  }
 }
diff --git a/tests/mocks/state.ts b/tests/mocks/state.ts
index 1c73643d..8c8a8e75 100644
--- a/tests/mocks/state.ts
+++ b/tests/mocks/state.ts
@@ -1,17 +1,15 @@
-import { observable, toJS } from 'mobx';
-import { MosaicNode } from 'react-mosaic-component';
+import { observable } from 'mobx';
 
 import {
   BlockableAccelerator,
   DefaultEditorId,
-  EditorId,
   ElectronReleaseChannel,
   GenericDialogOptions,
   GistActionState,
   RunnableVersion,
 } from '../../src/interfaces';
-import { EditorBackup } from '../../src/utils/editor-backup';
 
+import { objectDifference } from '../utils';
 import { BisectorMock } from './bisector';
 import { EditorMosaicMock } from './editor-mosaic';
 import { MonacoEditorMock } from './monaco-editor';
@@ -21,9 +19,7 @@ export class StateMock {
   @observable public acceleratorsToBlock: BlockableAccelerator[] = [];
   @observable public activeGistAction = GistActionState.none;
   @observable public channelsToShow: ElectronReleaseChannel[] = [];
-  @observable public closedPanels: Partial<
-    Record<EditorId, EditorBackup | true>
-  > = {};
+  @observable public editorMosaic = new EditorMosaicMock();
   @observable public environmentVariables: string[] = [];
   @observable public executionFlags: string[] = [];
   @observable public genericDialogLastInput: string | null = null;
@@ -57,11 +53,6 @@ export class StateMock {
   @observable public versions: Record<string, RunnableVersion>;
   @observable public versionsToShow: RunnableVersion[] = [];
 
-  // editor mosaic
-  @observable public customMosaics: EditorId[] = [];
-  @observable public editorMosaic = new EditorMosaicMock();
-  @observable public mosaicArrangement: MosaicNode<EditorId> | null = null;
-
   public Bisector = new BisectorMock();
   public addAcceleratorToBlock = jest.fn();
   public addLocalVersion = jest.fn();
@@ -69,18 +60,15 @@ export class StateMock {
   public currentElectronVersion = new VersionsMock().mockVersionsArray.shift()!;
   public disableTour = jest.fn();
   public downloadVersion = jest.fn();
-  public getAndRemoveEditorValueBackup = jest.fn();
   public getName = jest.fn();
   public hasVersion = jest
     .fn()
     .mockImplementation((version: string) => !!this.versions[version]);
-  public hideAndBackupMosaic = jest.fn();
   public hideChannels = jest.fn();
   public pushError = jest.fn();
   public pushOutput = jest.fn();
   public flushOutput = jest.fn();
   public removeAcceleratorToBlock = jest.fn();
-  public removeCustomMosaic = jest.fn();
   public removeVersion = jest.fn();
   public setGenericDialogOptions = jest.fn().mockReturnValue({});
   public setTheme = jest.fn();
@@ -88,10 +76,8 @@ export class StateMock {
     this.version = version;
     this.currentElectronVersion = this.versions[version];
   });
-  public setVisibleMosaics = jest.fn();
   public showChannels = jest.fn();
   public showCustomEditorDialog = jest.fn();
-  public showMosaic = jest.fn();
   public signOutGitHub = jest.fn();
   public toggleAddVersionDialog = jest.fn();
   public toggleAuthDialog = jest.fn();
@@ -130,31 +116,18 @@ export class StateMock {
   // Snapshots get very "noisy" when they include the entire Mock's details.
   // We can avoid most of that noise by hiding fields that hold default values.
   public toJSON() {
-    const defaultValues = new StateMock();
-    const serialize = (input: any) => JSON.stringify(toJS(input));
-    const isDefaultValue = (key: string, value: any) =>
-      serialize(value) === serialize(defaultValues[key]);
+    const o = objectDifference(this, new StateMock());
+
     const terserRunnable = (ver: RunnableVersion) =>
       [ver.version, ver.source, ver.state].join(' ');
-
-    const o = {};
-    for (const entry of Object.entries(this)) {
-      const key = entry[0];
-      let val = toJS(entry[1]);
-
-      // omit any fields that have the default value
-      if (isDefaultValue(key, val)) continue;
-
-      // make some verbose properties a little terser
+    for (const [key, val] of Object.entries(o)) {
       if (key == 'currentElectronVersion') {
-        val = terserRunnable(val);
+        o[key] = terserRunnable(val) as any;
       } else if (key === 'versions') {
-        val = Object.values(val).map(terserRunnable);
+        o[key] = Object.values(val).map(terserRunnable) as any;
       } else if (key === 'versionsToShow') {
-        val = val.map(terserRunnable);
+        o[key] = val.map(terserRunnable);
       }
-
-      o[key] = val;
     }
 
     return Object.keys(o).length === 0 ? 'default StateMock' : o;
diff --git a/tests/renderer/app-spec.tsx b/tests/renderer/app-spec.tsx
index d5c267fa..40bbbdbb 100644
--- a/tests/renderer/app-spec.tsx
+++ b/tests/renderer/app-spec.tsx
@@ -1,12 +1,8 @@
 import { App } from '../../src/renderer/app';
-import { EditorBackup } from '../../src/utils/editor-backup';
 import { waitFor } from '../../src/utils/wait-for';
-import {
-  DefaultEditorId,
-  EditorValues,
-  PACKAGE_NAME,
-} from '../../src/interfaces';
+import { DefaultEditorId } from '../../src/interfaces';
 import { defaultDark, defaultLight } from '../../src/renderer/themes-defaults';
+import { createEditorValues } from '../mocks/mocks';
 
 jest.mock('../../src/renderer/components/header', () => ({
   Header: () => 'Header;',
@@ -21,7 +17,6 @@ jest.mock('../../src/renderer/components/output-editors-wrapper', () => ({
 describe('App component', () => {
   let app: App;
   let ElectronFiddle: any;
-  let editorMosaic: any;
 
   beforeAll(() => {
     document.body.innerHTML = '<div id="app" />';
@@ -29,7 +24,6 @@ describe('App component', () => {
 
   beforeEach(() => {
     ({ ElectronFiddle } = window as any);
-    ({ editorMosaic } = ElectronFiddle.app.state);
 
     // make a real App and inject it into the mocks
     app = new App();
@@ -74,35 +68,17 @@ describe('App component', () => {
 
   describe('getEditorValues()', () => {
     it('gets values', async () => {
-      const value = 'editor-value';
-      for (const editor of editorMosaic.editors.values()) {
-        editor.getValue.mockReturnValue(value);
-      }
+      const values = createEditorValues();
+      (app.state.editorMosaic.values as jest.Mock).mockReturnValue(values);
       const b = await app.getEditorValues({});
-      expect(b[DefaultEditorId.html]).toBe(value);
-      expect(b[DefaultEditorId.main]).toBe(value);
-      expect(b[DefaultEditorId.renderer]).toBe(value);
-      expect(b[PACKAGE_NAME]).toBeTruthy();
-      expect(JSON.parse(b[PACKAGE_NAME]!)).toBeTruthy();
-    });
-
-    it('throws if the Fiddle object is not present', async () => {
-      const app = new App();
-
-      (window as any).ElectronFiddle = null;
-      let threw = false;
-      try {
-        await app.getEditorValues({});
-      } catch (error) {
-        threw = true;
-      }
-
-      expect(threw).toBe(true);
+      expect(b).toStrictEqual(values);
     });
   });
 
   describe('replaceFiddle()', () => {
     it('sets editor values and source info', async () => {
+      const { state } = app;
+
       const editorValues = {
         [DefaultEditorId.html]: 'html-value',
         [DefaultEditorId.main]: 'main-value',
@@ -111,73 +87,15 @@ describe('App component', () => {
         [DefaultEditorId.preload]: '',
       };
 
-      app.setEditorValues = jest.fn();
       await app.replaceFiddle(editorValues, {
         gistId: 'gistId',
         templateName: 'templateName',
         filePath: 'localPath',
       });
-      expect(app.setEditorValues).toHaveBeenCalledWith(editorValues);
-      expect(app.state.gistId).toBe('gistId');
-      expect(app.state.templateName).toBe('templateName');
-      expect(app.state.localPath).toBe('localPath');
-    });
-
-    it('only shows mosaic for non-empty editor contents', async () => {
-      const editorValues = {
-        [DefaultEditorId.html]: 'html-value',
-        [DefaultEditorId.main]: 'main-value',
-        [DefaultEditorId.renderer]: 'renderer-value',
-        [DefaultEditorId.css]: '/* Empty */',
-        [DefaultEditorId.preload]: '',
-      };
-
-      const gistId = 'gistId';
-      await app.replaceFiddle(editorValues, { gistId });
-      expect(app.state.setVisibleMosaics).toHaveBeenCalledWith([
-        DefaultEditorId.main,
-        DefaultEditorId.renderer,
-        DefaultEditorId.html,
-      ]);
-    });
-
-    it('shows visible mosaics for non-empty editor contents with custom mosaics', async () => {
-      const file = 'file.js';
-      const editorValues = {
-        [DefaultEditorId.html]: 'html-value',
-        [DefaultEditorId.main]: 'main-value',
-        [DefaultEditorId.renderer]: 'renderer-value',
-        [DefaultEditorId.css]: '/* Empty */',
-        [DefaultEditorId.preload]: '',
-        [file]: 'file-value',
-      };
-
-      const gistId = 'gistId';
-      await app.replaceFiddle(editorValues, { gistId });
-      expect(app.state.setVisibleMosaics).toHaveBeenCalledWith([
-        DefaultEditorId.main,
-        DefaultEditorId.renderer,
-        DefaultEditorId.html,
-        file,
-      ]);
-    });
-
-    it('shows visible mosaics in the correct pre-defined order', async () => {
-      // this order is defined inside the replaceFiddle() function
-      const editorValues = {
-        [DefaultEditorId.html]: 'html-value',
-        [DefaultEditorId.main]: 'main-value',
-        [DefaultEditorId.renderer]: 'renderer-value',
-        [DefaultEditorId.css]: 'css-value',
-      };
-      const gistId = 'gistId';
-      await app.replaceFiddle(editorValues, { gistId });
-      expect(app.state.setVisibleMosaics).toHaveBeenCalledWith([
-        DefaultEditorId.main,
-        DefaultEditorId.renderer,
-        DefaultEditorId.html,
-        DefaultEditorId.css,
-      ]);
+      expect(state.editorMosaic.set).toHaveBeenCalledWith(editorValues);
+      expect(state.gistId).toBe('gistId');
+      expect(state.templateName).toBe('templateName');
+      expect(state.localPath).toBe('localPath');
     });
 
     it('unsets state of previous source when called', (done) => {
@@ -242,11 +160,12 @@ describe('App component', () => {
 
     describe('when current Fiddle is unsaved and prompt appears', () => {
       it('takes no action if prompt is rejected', (done) => {
-        app.setEditorValues = jest.fn();
-        app.state.isUnsaved = true;
-        expect(app.state.localPath).toBeUndefined();
-        expect(app.state.gistId).toBe('');
-        expect(app.state.templateName).toBeUndefined();
+        const { state } = app;
+
+        state.isUnsaved = true;
+        expect(state.localPath).toBeUndefined();
+        expect(state.gistId).toBe('');
+        expect(state.templateName).toBeUndefined();
 
         app
           .replaceFiddle(
@@ -258,23 +177,23 @@ describe('App component', () => {
             },
           )
           .then(() => {
-            expect(app.setEditorValues).not.toHaveBeenCalled();
-            expect(app.state.localPath).toBeUndefined();
-            expect(app.state.gistId).toBe('');
-            expect(app.state.templateName).toBeUndefined();
+            expect(state.editorMosaic.set).not.toHaveBeenCalled();
+            expect(state.localPath).toBeUndefined();
+            expect(state.gistId).toBe('');
+            expect(state.templateName).toBeUndefined();
             done();
           });
 
         setTimeout(() => {
-          expect(app.state.isGenericDialogShowing).toBe(true);
-          app.state.genericDialogLastResult = false;
-          app.state.isGenericDialogShowing = false;
+          expect(state.isGenericDialogShowing).toBe(true);
+          state.genericDialogLastResult = false;
+          state.isGenericDialogShowing = false;
         });
       });
 
       it('sets editor values and source info if prompt is accepted', (done) => {
-        app.setEditorValues = jest.fn();
-        app.state.isUnsaved = true;
+        const { state } = app;
+        state.isUnsaved = true;
 
         const editorValues = {
           [DefaultEditorId.html]: 'html-value',
@@ -289,90 +208,19 @@ describe('App component', () => {
             filePath: 'localPath',
           })
           .then(() => {
-            expect(app.setEditorValues).toHaveBeenCalledWith(editorValues);
-            expect(app.state.gistId).toBe('gistId');
-            expect(app.state.templateName).toBe('templateName');
-            expect(app.state.localPath).toBe('localPath');
+            expect(state.editorMosaic.set).toHaveBeenCalledWith(editorValues);
+            expect(state.gistId).toBe('gistId');
+            expect(state.templateName).toBe('templateName');
+            expect(state.localPath).toBe('localPath');
             done();
           });
 
         setTimeout(() => {
-          expect(app.state.isGenericDialogShowing).toBe(true);
-          app.state.genericDialogLastResult = true;
-          app.state.isGenericDialogShowing = false;
-        });
-      });
-    });
-  });
-
-  describe('setEditorValues()', () => {
-    it('attempts to set values', () => {
-      const editorValues: EditorValues = {
-        [DefaultEditorId.html]: 'html-value',
-        [DefaultEditorId.main]: 'main-value',
-        [DefaultEditorId.renderer]: 'renderer-value',
-      } as const;
-      app.setEditorValues(editorValues);
-
-      for (const [filename, value] of Object.entries(editorValues)) {
-        const editor = editorMosaic.editors.get(filename);
-        expect(editor.setValue).toHaveBeenCalledWith(value);
-      }
-    });
-
-    it('attempts to set values for closed editors', () => {
-      editorMosaic.editors.delete(DefaultEditorId.main);
-
-      (app.state.closedPanels as any)[DefaultEditorId.main] = {
-        model: { setValue: jest.fn() },
-      };
-      app.state.closedPanels[DefaultEditorId.preload] = {};
-      app.state.closedPanels[DefaultEditorId.css] = {};
-
-      app.setEditorValues({
-        [DefaultEditorId.html]: 'html-value',
-        [DefaultEditorId.main]: 'main-value',
-        [DefaultEditorId.renderer]: 'renderer-value',
-        [DefaultEditorId.preload]: 'preload-value',
-        [DefaultEditorId.css]: 'css-value',
-      });
-
-      expect(
-        (app.state.closedPanels[DefaultEditorId.main] as EditorBackup)!.model!
-          .setValue,
-      ).toHaveBeenCalledWith('main-value');
-      expect(app.state.closedPanels[DefaultEditorId.preload]).toEqual({
-        value: 'preload-value',
-      });
-      expect(app.state.closedPanels[DefaultEditorId.css]).toEqual({
-        value: 'css-value',
-      });
-    });
-
-    it('throws if the Fiddle object is not present', async () => {
-      (window as any).ElectronFiddle = null;
-      let threw = false;
-      try {
-        await app.setEditorValues({
-          [DefaultEditorId.html]: '',
-          [DefaultEditorId.main]: '',
-          [DefaultEditorId.renderer]: '',
+          expect(state.isGenericDialogShowing).toBe(true);
+          state.genericDialogLastResult = true;
+          state.isGenericDialogShowing = false;
         });
-      } catch (error) {
-        threw = true;
-      }
-
-      expect(threw).toBe(true);
-    });
-
-    it('does not set a value if none passed in', async () => {
-      await app.setEditorValues({
-        [DefaultEditorId.html]: 'html-value',
-        [DefaultEditorId.main]: 'main-value',
       });
-
-      const editor = editorMosaic.editors.get(DefaultEditorId.renderer);
-      expect(editor.setValue).not.toHaveBeenCalled();
     });
   });
 
diff --git a/tests/renderer/components/__snapshots__/commands-editors-spec.tsx.snap b/tests/renderer/components/__snapshots__/commands-editors-spec.tsx.snap
index 64e98158..814e8463 100644
--- a/tests/renderer/components/__snapshots__/commands-editors-spec.tsx.snap
+++ b/tests/renderer/components/__snapshots__/commands-editors-spec.tsx.snap
@@ -70,6 +70,7 @@ Array [
       disabled={false}
       icon="grid-view"
       multiline={false}
+      onClick={[MockFunction]}
       popoverProps={Object {}}
       shouldDismissPopover={true}
       text="Reset Layout"
@@ -153,6 +154,7 @@ exports[`EditorDropdown component renders 1`] = `
             disabled={false}
             icon="grid-view"
             multiline={false}
+            onClick={[MockFunction]}
             popoverProps={Object {}}
             shouldDismissPopover={true}
             text="Reset Layout"
@@ -260,6 +262,7 @@ exports[`EditorDropdown component renders the extra button if the FIDDLE_DOCS_DE
             disabled={false}
             icon="grid-view"
             multiline={false}
+            onClick={[MockFunction]}
             popoverProps={Object {}}
             shouldDismissPopover={true}
             text="Reset Layout"
diff --git a/tests/renderer/components/__snapshots__/editors-spec.tsx.snap b/tests/renderer/components/__snapshots__/editors-spec.tsx.snap
index 2b290b21..6159fea7 100644
--- a/tests/renderer/components/__snapshots__/editors-spec.tsx.snap
+++ b/tests/renderer/components/__snapshots__/editors-spec.tsx.snap
@@ -915,20 +915,22 @@ exports[`Editors component renders a toolbar 1`] = `
       <MaximizeButton
         appState={
           Object {
-            "mosaicArrangement": Object {
-              "direction": "row",
-              "first": Object {
-                "direction": "column",
-                "first": "main.js",
-                "second": "renderer.js",
-              },
-              "second": Object {
-                "direction": "column",
-                "first": "preload.js",
+            "editorMosaic": Object {
+              "mosaicArrangement": Object {
+                "direction": "row",
+                "first": Object {
+                  "direction": "column",
+                  "first": "main.js",
+                  "second": "renderer.js",
+                },
                 "second": Object {
                   "direction": "column",
-                  "first": "index.html",
-                  "second": "styles.css",
+                  "first": "preload.js",
+                  "second": Object {
+                    "direction": "column",
+                    "first": "index.html",
+                    "second": "styles.css",
+                  },
                 },
               },
             },
@@ -939,20 +941,22 @@ exports[`Editors component renders a toolbar 1`] = `
       <RemoveButton
         appState={
           Object {
-            "mosaicArrangement": Object {
-              "direction": "row",
-              "first": Object {
-                "direction": "column",
-                "first": "main.js",
-                "second": "renderer.js",
-              },
-              "second": Object {
-                "direction": "column",
-                "first": "preload.js",
+            "editorMosaic": Object {
+              "mosaicArrangement": Object {
+                "direction": "row",
+                "first": Object {
+                  "direction": "column",
+                  "first": "main.js",
+                  "second": "renderer.js",
+                },
                 "second": Object {
                   "direction": "column",
-                  "first": "index.html",
-                  "second": "styles.css",
+                  "first": "preload.js",
+                  "second": Object {
+                    "direction": "column",
+                    "first": "index.html",
+                    "second": "styles.css",
+                  },
                 },
               },
             },
diff --git a/tests/renderer/components/commands-editors-spec.tsx b/tests/renderer/components/commands-editors-spec.tsx
index 6067a7a5..80b8f5b3 100644
--- a/tests/renderer/components/commands-editors-spec.tsx
+++ b/tests/renderer/components/commands-editors-spec.tsx
@@ -37,22 +37,25 @@ describe('EditorDropdown component', () => {
   it('handles a click for an item', () => {
     const wrapper = mount(<EditorDropdown appState={store as any} />);
     const dropdown = wrapper.instance() as EditorDropdown;
+    const { editorMosaic } = store;
 
     dropdown.onItemClick({
       currentTarget: { id: DefaultEditorId.html },
     } as any);
-    expect(store.hideAndBackupMosaic).toHaveBeenCalledTimes(1);
-    expect(store.showMosaic).toHaveBeenCalledTimes(0);
+    expect(editorMosaic.hideAndBackupMosaic).toHaveBeenCalledTimes(1);
+    expect(editorMosaic.showMosaic).toHaveBeenCalledTimes(0);
 
     dropdown.onItemClick({
       currentTarget: { id: DefaultEditorId.main },
     } as any);
-    expect(store.hideAndBackupMosaic).toHaveBeenCalledTimes(1);
-    expect(store.showMosaic).toHaveBeenCalledTimes(1);
+    expect(editorMosaic.hideAndBackupMosaic).toHaveBeenCalledTimes(1);
+    expect(editorMosaic.showMosaic).toHaveBeenCalledTimes(1);
   });
 
   it('disables hide button if only one editor open', () => {
-    store.mosaicArrangement = DefaultEditorId.html;
+    const { editorMosaic } = store;
+
+    editorMosaic.mosaicArrangement = DefaultEditorId.html;
     (getVisibleMosaics as jest.Mock).mockReturnValue([DefaultEditorId.html]);
 
     const wrapper = mount(<EditorDropdown appState={store as any} />);
@@ -63,6 +66,7 @@ describe('EditorDropdown component', () => {
   });
 
   it('can add a valid custom editor', async () => {
+    const { editorMosaic } = store;
     const file = 'file.js';
 
     store.showCustomEditorDialog = jest
@@ -85,12 +89,13 @@ describe('EditorDropdown component', () => {
       wantsInput: true,
     });
 
-    expect(store.showMosaic).toHaveBeenCalledTimes(1);
+    expect(editorMosaic.showMosaic).toHaveBeenCalledTimes(1);
+    expect(editorMosaic.customMosaics).toEqual([file]);
     expect(store.toggleGenericDialog).toHaveBeenCalledTimes(1);
-    expect(store.customMosaics).toEqual([file]);
   });
 
   it('errors when trying to add a duplicate custom editor', async () => {
+    const { editorMosaic } = store;
     const badFile = 'main.js';
 
     store.showCustomEditorDialog = jest
@@ -120,11 +125,12 @@ describe('EditorDropdown component', () => {
     });
 
     expect(store.toggleGenericDialog).toHaveBeenCalledTimes(2);
-    expect(store.showMosaic).toHaveBeenCalledTimes(0);
-    expect(store.customMosaics).toEqual([]);
+    expect(editorMosaic.showMosaic).toHaveBeenCalledTimes(0);
+    expect(editorMosaic.customMosaics).toEqual([]);
   });
 
   it('errors when trying to add an invalid custom editor', async () => {
+    const { editorMosaic } = store;
     const badFile = 'bad.bad';
 
     store.showCustomEditorDialog = jest
@@ -154,14 +160,15 @@ describe('EditorDropdown component', () => {
       cancel: undefined,
     });
 
-    expect(store.showMosaic).toHaveBeenCalledTimes(0);
+    expect(editorMosaic.customMosaics).toEqual([]);
+    expect(editorMosaic.showMosaic).toHaveBeenCalledTimes(0);
     expect(store.toggleGenericDialog).toHaveBeenCalledTimes(2);
-    expect(store.customMosaics).toEqual([]);
   });
 
   it('can remove a custom editor', () => {
+    const { editorMosaic } = store;
     const file = 'file.js';
-    store.customMosaics = [file];
+    editorMosaic.customMosaics = [file];
 
     const wrapper = mount(<EditorDropdown appState={store as any} />);
     const dropdown = wrapper.instance() as EditorDropdown;
@@ -170,6 +177,6 @@ describe('EditorDropdown component', () => {
       currentTarget: { id: file },
     } as any);
 
-    expect(store.removeCustomMosaic).toHaveBeenCalledTimes(1);
+    expect(editorMosaic.removeCustomMosaic).toHaveBeenCalledTimes(1);
   });
 });
diff --git a/tests/renderer/components/editor-spec.tsx b/tests/renderer/components/editor-spec.tsx
index d2ada760..4e440aa1 100644
--- a/tests/renderer/components/editor-spec.tsx
+++ b/tests/renderer/components/editor-spec.tsx
@@ -109,7 +109,7 @@ describe('Editor component', () => {
 
     describe('backups', async () => {
       it('attempts to restore a backup if contains a model', async () => {
-        store.getAndRemoveEditorValueBackup.mockReturnValueOnce({
+        store.editorMosaic.getAndRemoveEditorValueBackup.mockReturnValueOnce({
           model: true,
           viewState: true,
         });
@@ -134,7 +134,7 @@ describe('Editor component', () => {
       });
 
       it('attempts to restore a backup if contains a string value', async () => {
-        store.getAndRemoveEditorValueBackup.mockReturnValueOnce({
+        store.editorMosaic.getAndRemoveEditorValueBackup.mockReturnValueOnce({
           value: 'hello',
         });
 
diff --git a/tests/renderer/components/editors-non-ideal-state-spec.tsx b/tests/renderer/components/editors-non-ideal-state-spec.tsx
index eb22a896..e3eb5d32 100644
--- a/tests/renderer/components/editors-non-ideal-state-spec.tsx
+++ b/tests/renderer/components/editors-non-ideal-state-spec.tsx
@@ -1,17 +1,24 @@
 import { mount } from 'enzyme';
 
+import { StateMock } from '../../mocks/mocks';
+
 import { renderNonIdealState } from '../../../src/renderer/components/editors-non-ideal-state';
 
 describe('renderNonIdealState()', () => {
+  let state: StateMock;
+
+  beforeEach(() => {
+    ({ state } = (window as any).ElectronFiddle.app);
+  });
+
   it('renders a non-ideal state', () => {
     expect(renderNonIdealState({} as any)).toMatchSnapshot();
   });
 
   it('handles a click', () => {
-    const mockState = { setVisibleMosaics: jest.fn() };
-    const wrapper = mount(renderNonIdealState(mockState as any));
+    const wrapper = mount(renderNonIdealState(state as any));
     wrapper.find('button').simulate('click');
 
-    expect(mockState.setVisibleMosaics).toHaveBeenCalledTimes(1);
+    expect(state.editorMosaic.setVisibleMosaics).toHaveBeenCalledTimes(1);
   });
 });
diff --git a/tests/renderer/components/editors-spec.tsx b/tests/renderer/components/editors-spec.tsx
index 1b650564..a71c7d22 100644
--- a/tests/renderer/components/editors-spec.tsx
+++ b/tests/renderer/components/editors-spec.tsx
@@ -8,7 +8,6 @@ import { IpcEvents } from '../../../src/ipc-events';
 import { createMosaicArrangement } from '../../../src/utils/editors-mosaic-arrangement';
 import { getFocusedEditor } from '../../../src/utils/focused-editor';
 import { ipcRendererManager } from '../../../src/renderer/ipc';
-import { updateEditorLayout } from '../../../src/utils/editor-layout';
 
 import {
   EditorMosaicMock,
@@ -30,10 +29,6 @@ jest.mock('../../../src/utils/focused-editor', () => ({
   getFocusedEditor: jest.fn(),
 }));
 
-jest.mock('../../../src/utils/editor-layout', () => ({
-  updateEditorLayout: jest.fn(),
-}));
-
 describe('Editors component', () => {
   let ElectronFiddle: any;
   let monaco: any;
@@ -44,7 +39,9 @@ describe('Editors component', () => {
     ({ ElectronFiddle } = window as any);
     ({ monaco, state: store } = ElectronFiddle.app);
     ({ editorMosaic } = store);
-    store.mosaicArrangement = createMosaicArrangement(DEFAULT_EDITORS);
+    store.editorMosaic.mosaicArrangement = createMosaicArrangement(
+      DEFAULT_EDITORS,
+    );
   });
 
   it('renders', () => {
@@ -109,7 +106,7 @@ describe('Editors component', () => {
   });
 
   it('does not render toolbar controls if only one editor exists', () => {
-    store.mosaicArrangement = DefaultEditorId.main;
+    store.editorMosaic.mosaicArrangement = DefaultEditorId.main;
     const wrapper = shallow(<Editors appState={store as any} />);
     const instance: Editors = wrapper.instance() as any;
     const toolbar = instance.renderToolbar(
@@ -120,23 +117,15 @@ describe('Editors component', () => {
     expect(toolbar).toMatchSnapshot();
   });
 
-  it('componentWillUnmount() unsubscribes the layout reaction', () => {
-    const wrapper = shallow(<Editors appState={store as any} />);
-    const instance: Editors = wrapper.instance() as any;
-    (instance as any).disposeLayoutAutorun = jest.fn();
-
-    wrapper.unmount();
-
-    expect(instance.disposeLayoutAutorun).toHaveBeenCalledTimes(1);
-  });
-
   it('onChange() updates the mosaic arrangement in the appState', () => {
     const wrapper = shallow(<Editors appState={store as any} />);
     const instance: Editors = wrapper.instance() as any;
 
     instance.onChange({ testArrangement: true } as any);
 
-    expect(store.mosaicArrangement).toEqual({ testArrangement: true });
+    expect(store.editorMosaic.mosaicArrangement).toEqual({
+      testArrangement: true,
+    });
   });
 
   describe('IPC commands', () => {
@@ -303,12 +292,4 @@ describe('Editors component', () => {
       expect(spy).toHaveBeenCalledWith({ focused: id });
     });
   });
-
-  describe('disposeLayoutAutorun()', () => {
-    it('automatically updates the layout when the mosaic arrangement changes', () => {
-      shallow(<Editors appState={store as any} />);
-      store.mosaicArrangement = DefaultEditorId.main;
-      expect(updateEditorLayout).toHaveBeenCalledTimes(1);
-    });
-  });
 });
diff --git a/tests/renderer/components/editors-toolbar-button-spec.tsx b/tests/renderer/components/editors-toolbar-button-spec.tsx
index 0e7621e4..bf2b6d7d 100644
--- a/tests/renderer/components/editors-toolbar-button-spec.tsx
+++ b/tests/renderer/components/editors-toolbar-button-spec.tsx
@@ -7,6 +7,8 @@ import {
   RemoveButton,
 } from '../../../src/renderer/components/editors-toolbar-button';
 
+import { StateMock } from '../../mocks/mocks';
+
 let mockContext: any = {};
 
 jest.mock('react-mosaic-component', () => {
@@ -26,7 +28,7 @@ jest.mock('react-mosaic-component', () => {
 });
 
 describe('Editor toolbar button component', () => {
-  let store: any = {};
+  let store: StateMock;
 
   beforeAll(() => {
     mockContext = {
@@ -48,15 +50,13 @@ describe('Editor toolbar button component', () => {
       mosaicId: 'test',
     };
 
-    store = {
-      hideAndBackupMosaic: jest.fn(),
-    };
+    ({ state: store } = (window as any).ElectronFiddle.app);
   });
 
   describe('MaximizeButton', () => {
     it('renders', () => {
       const wrapper = shallow(
-        <MaximizeButton id={DefaultEditorId.main} appState={store} />,
+        <MaximizeButton id={DefaultEditorId.main} appState={store as any} />,
         {
           context: mockContext,
         },
@@ -67,7 +67,7 @@ describe('Editor toolbar button component', () => {
 
     it('handles a click', () => {
       const wrapper = shallow(
-        <MaximizeButton id={DefaultEditorId.main} appState={store} />,
+        <MaximizeButton id={DefaultEditorId.main} appState={store as any} />,
         {
           context: mockContext,
         },
@@ -83,7 +83,7 @@ describe('Editor toolbar button component', () => {
   describe('RemoveButton', () => {
     it('renders', () => {
       const wrapper = shallow(
-        <RemoveButton id={DefaultEditorId.main} appState={store} />,
+        <RemoveButton id={DefaultEditorId.main} appState={store as any} />,
         {
           context: mockContext,
         },
@@ -93,14 +93,14 @@ describe('Editor toolbar button component', () => {
 
     it('handles a click', () => {
       const wrapper = shallow(
-        <RemoveButton id={DefaultEditorId.main} appState={store} />,
+        <RemoveButton id={DefaultEditorId.main} appState={store as any} />,
         {
           context: mockContext,
         },
       );
 
       wrapper.dive().dive().find('button').simulate('click');
-      expect(store.hideAndBackupMosaic).toHaveBeenCalledTimes(1);
+      expect(store.editorMosaic.hideAndBackupMosaic).toHaveBeenCalledTimes(1);
     });
   });
 });
diff --git a/tests/renderer/editor-mosaic-spec.ts b/tests/renderer/editor-mosaic-spec.ts
new file mode 100644
index 00000000..b559db94
--- /dev/null
+++ b/tests/renderer/editor-mosaic-spec.ts
@@ -0,0 +1,388 @@
+import {
+  DEFAULT_EDITORS,
+  DefaultEditorId,
+  EditorId,
+  EditorValues,
+} from '../../src/interfaces';
+import { EditorBackup, EditorMosaic } from '../../src/renderer/editor-mosaic';
+import {
+  DEFAULT_MOSAIC_ARRANGEMENT,
+  SORTED_EDITORS,
+} from '../../src/renderer/constants';
+import { compareEditors } from '../../src/utils/editor-utils';
+import { createMosaicArrangement } from '../../src/utils/editors-mosaic-arrangement';
+import { MonacoEditorMock, createEditorValues } from '../mocks/mocks';
+import { waitForEditorsToMount } from '../../src/utils/editor-mounted';
+import { waitFor } from '../../src/utils/wait-for';
+
+jest.mock('../../src/utils/editor-mounted', () => ({
+  waitForEditorsToMount: jest.fn(),
+}));
+
+describe('EditorMosaic', () => {
+  let editorMosaic: EditorMosaic;
+
+  beforeEach(() => {
+    // inject a real EditorMosaic into our mock scaffolding
+    editorMosaic = new EditorMosaic();
+    (window as any).ElectronFiddle.app.state.editorMosaic = editorMosaic;
+  });
+
+  describe('getAndRemoveEditorValueBackup()', () => {
+    it('returns null if there is no backup', () => {
+      const result = editorMosaic.getAndRemoveEditorValueBackup(
+        DefaultEditorId.main,
+      );
+      expect(result).toEqual(undefined);
+    });
+
+    it('returns and deletes a backup if there is one', () => {
+      editorMosaic.closedPanels[DefaultEditorId.main] = {
+        testBackup: true,
+      } as any;
+      const result = editorMosaic.getAndRemoveEditorValueBackup(
+        DefaultEditorId.main,
+      );
+      expect(result).toEqual({ testBackup: true });
+      expect(editorMosaic.closedPanels[DefaultEditorId.main]).toBeUndefined();
+    });
+  });
+
+  describe('setVisibleMosaics()', () => {
+    it('updates the visible editors and creates a backup', async () => {
+      editorMosaic.mosaicArrangement = createMosaicArrangement(DEFAULT_EDITORS);
+      editorMosaic.closedPanels = {};
+      editorMosaic.customMosaics = [];
+      await editorMosaic.setVisibleMosaics([DefaultEditorId.main]);
+
+      // we just need to mock something truthy here
+      editorMosaic.editors.set(DefaultEditorId.main, {} as any);
+
+      expect(editorMosaic.mosaicArrangement).toEqual(DefaultEditorId.main);
+      expect(editorMosaic.closedPanels[DefaultEditorId.renderer]).toBeTruthy();
+      expect(editorMosaic.closedPanels[DefaultEditorId.html]).toBeTruthy();
+      expect(editorMosaic.closedPanels[DefaultEditorId.main]).toBeUndefined();
+    });
+  });
+
+  describe('removeCustomMosaic()', () => {
+    it('removes a given custom mosaic', () => {
+      const file = 'file.js';
+      editorMosaic.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
+      editorMosaic.customMosaics = [file];
+
+      editorMosaic.removeCustomMosaic(file);
+
+      expect(editorMosaic.customMosaics).toEqual([]);
+    });
+  });
+
+  describe('hideAndBackupMosaic()', () => {
+    it('hides a given editor and creates a backup', () => {
+      editorMosaic.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
+      editorMosaic.closedPanels = {};
+      editorMosaic.hideAndBackupMosaic(SORTED_EDITORS[0]);
+
+      expect(editorMosaic.mosaicArrangement).toEqual({
+        direction: 'row',
+        first: SORTED_EDITORS[1],
+        second: {
+          direction: 'column',
+          first: SORTED_EDITORS[2],
+          second: SORTED_EDITORS[3],
+        },
+      });
+      expect(editorMosaic.closedPanels[DefaultEditorId.main]).toBeTruthy();
+      expect(
+        editorMosaic.closedPanels[DefaultEditorId.renderer],
+      ).toBeUndefined();
+      expect(editorMosaic.closedPanels[DefaultEditorId.html]).toBeUndefined();
+    });
+  });
+
+  describe('showMosaic()', () => {
+    it('shows a given editor', () => {
+      editorMosaic.mosaicArrangement = DefaultEditorId.main;
+      editorMosaic.showMosaic(DefaultEditorId.html);
+
+      expect(editorMosaic.mosaicArrangement).toEqual({
+        direction: 'row',
+        first: DefaultEditorId.main,
+        second: DefaultEditorId.html,
+      });
+    });
+  });
+
+  describe('resetEditorLayout()', () => {
+    it('Puts editors in default arrangement', () => {
+      editorMosaic.hideAndBackupMosaic(SORTED_EDITORS[0]);
+
+      expect(editorMosaic.mosaicArrangement).toEqual({
+        direction: 'row',
+        first: SORTED_EDITORS[1],
+        second: {
+          direction: 'column',
+          first: SORTED_EDITORS[2],
+          second: SORTED_EDITORS[3],
+        },
+      });
+
+      editorMosaic.resetEditorLayout();
+
+      expect(editorMosaic.mosaicArrangement).toEqual(
+        DEFAULT_MOSAIC_ARRANGEMENT,
+      );
+    });
+  });
+
+  describe('values()', () => {
+    it('gets values', async () => {
+      (waitForEditorsToMount as jest.Mock).mockResolvedValue(true);
+      const values = createEditorValues();
+      await editorMosaic.set(values);
+      expect(editorMosaic.values()).toStrictEqual(values);
+    });
+  });
+
+  describe('set()', () => {
+    it('only shows non-empty files', async () => {
+      const spy = jest.spyOn(editorMosaic, 'setVisibleMosaics');
+
+      const values = {
+        [DefaultEditorId.html]: 'html-value',
+        [DefaultEditorId.main]: 'main-value',
+        [DefaultEditorId.renderer]: 'renderer-value',
+        [DefaultEditorId.css]: '/* Empty */',
+        [DefaultEditorId.preload]: '',
+      } as const;
+
+      await editorMosaic.set(values);
+      expect(spy).toHaveBeenCalledWith([
+        DefaultEditorId.main,
+        DefaultEditorId.renderer,
+        DefaultEditorId.html,
+      ]);
+
+      spy.mockRestore();
+    });
+
+    it('shows visible mosaics for non-empty editor contents with custom mosaics', async () => {
+      const spy = jest.spyOn(editorMosaic, 'setVisibleMosaics');
+
+      const file = 'file.js';
+      const values = {
+        [DefaultEditorId.html]: 'html-value',
+        [DefaultEditorId.main]: 'main-value',
+        [DefaultEditorId.renderer]: 'renderer-value',
+        [DefaultEditorId.css]: '/* Empty */',
+        [DefaultEditorId.preload]: '',
+        [file]: 'file-value',
+      } as const;
+
+      await editorMosaic.set(values);
+      expect(spy).toHaveBeenCalledWith([
+        DefaultEditorId.main,
+        DefaultEditorId.renderer,
+        DefaultEditorId.html,
+        file,
+      ]);
+
+      spy.mockRestore();
+    });
+
+    it('sorts the mosaics', async () => {
+      const spy = jest.spyOn(editorMosaic, 'setVisibleMosaics');
+
+      // this order is defined inside the replaceFiddle() function
+      const values = createEditorValues();
+      await editorMosaic.set(values);
+      const expected = Object.keys(values).sort(compareEditors);
+      expect(spy).toHaveBeenCalledWith(expected);
+
+      spy.mockRestore();
+    });
+
+    it('attempts to set values', async () => {
+      const values: EditorValues = {
+        [DefaultEditorId.html]: 'html-value',
+        [DefaultEditorId.main]: 'main-value',
+        [DefaultEditorId.renderer]: 'renderer-value',
+      } as const;
+      for (const filename of Object.keys(values)) {
+        editorMosaic.editors.set(
+          filename as EditorId,
+          new MonacoEditorMock() as any,
+        );
+      }
+      await editorMosaic.set(values);
+
+      for (const [filename, value] of Object.entries(values)) {
+        const editor = editorMosaic.editors.get(filename as EditorId);
+        expect(editor!.setValue).toHaveBeenCalledWith(value);
+      }
+    });
+
+    it('attempts to set values for closed editors', async () => {
+      editorMosaic.editors.delete(DefaultEditorId.main);
+
+      (editorMosaic.closedPanels as any)[DefaultEditorId.main] = {
+        model: { setValue: jest.fn() },
+      };
+      editorMosaic.closedPanels[DefaultEditorId.preload] = {};
+      editorMosaic.closedPanels[DefaultEditorId.css] = {};
+
+      await editorMosaic.set({
+        [DefaultEditorId.html]: 'html-value',
+        [DefaultEditorId.main]: 'main-value',
+        [DefaultEditorId.renderer]: 'renderer-value',
+        [DefaultEditorId.preload]: 'preload-value',
+        [DefaultEditorId.css]: 'css-value',
+      });
+
+      expect(
+        (editorMosaic.closedPanels[DefaultEditorId.main] as EditorBackup)!
+          .model!.setValue,
+      ).toHaveBeenCalledWith('main-value');
+      expect(editorMosaic.closedPanels[DefaultEditorId.preload]).toEqual({
+        value: 'preload-value',
+      });
+      expect(editorMosaic.closedPanels[DefaultEditorId.css]).toEqual({
+        value: 'css-value',
+      });
+    });
+
+    it('does not set a value if none passed in', async () => {
+      const id = DefaultEditorId.renderer;
+      const oldValue = editorMosaic.getEditorValue(id);
+
+      await editorMosaic.set({
+        [DefaultEditorId.html]: 'html-value',
+        [DefaultEditorId.main]: 'main-value',
+      });
+
+      expect(editorMosaic.getEditorValue(id)).toBe(oldValue);
+    });
+  });
+
+  describe('getEditorValue()', () => {
+    const filename = DefaultEditorId.html;
+    const value = 'editor-value';
+
+    beforeEach(() => {
+      const editor = new MonacoEditorMock();
+      editor.getValue.mockReturnValue(value);
+      editorMosaic.editors.set(filename, editor as any);
+    });
+
+    it('returns the value for an editor if it exists', () => {
+      expect(editorMosaic.getEditorValue(filename)).toBe(value);
+    });
+
+    it('returns the value for the editor backup if it exists', () => {
+      // set up mock state that has the editor deleted and a backup
+      editorMosaic.editors.delete(filename);
+      const value = 'editor-backup-value';
+      editorMosaic.closedPanels = { [filename]: { value } };
+
+      expect(editorMosaic.getEditorValue(filename)).toBe(value);
+    });
+
+    it('returns an empty string if the editor does not exist', () => {
+      editorMosaic.editors.delete(filename);
+      expect(editorMosaic.getEditorValue(filename)).toBe('');
+    });
+  });
+
+  describe('getEditorBackup()', () => {
+    const filename = DefaultEditorId.html;
+    const value = 'editor-value';
+
+    beforeEach(() => {
+      const editor = new MonacoEditorMock();
+      editor.getValue.mockReturnValue(value);
+      editorMosaic.editors.set(filename, editor as any);
+    });
+
+    it('returns the value for an editor', () => {
+      const model = { testModel: true };
+      const viewState = { testViewState: true };
+      const value = 'editor-value';
+
+      const editor = editorMosaic.editors.get(filename) as any;
+      editor.model = model as any;
+      editor.saveViewState.mockReturnValue(viewState);
+      editor.value = value;
+
+      expect(editorMosaic.getEditorBackup(filename)).toEqual({
+        model,
+        value,
+        viewState,
+      });
+    });
+  });
+
+  describe('getEditorViewState()', () => {
+    const filename = DefaultEditorId.html;
+
+    it('returns the value for an editor', () => {
+      const editor = new MonacoEditorMock();
+      const viewState = { testViewState: true };
+      editor.saveViewState.mockReturnValue(viewState);
+      editorMosaic.editors.set(filename, editor as any);
+
+      expect(editorMosaic.getEditorViewState(filename)).toBe(viewState);
+    });
+
+    it('returns null if the editor does not exist', () => {
+      editorMosaic.editors.delete(filename);
+      expect(editorMosaic.getEditorViewState(filename)).toBeNull();
+    });
+  });
+
+  describe('getEditorModel()', () => {
+    const filename = DefaultEditorId.html;
+
+    beforeEach(() => {
+      ({ editorMosaic } = (window as any).ElectronFiddle.app.state);
+    });
+
+    it('returns the value for an editor', () => {
+      const model = { testModel: true };
+      const editor = new MonacoEditorMock();
+      editor.getModel.mockReturnValue(model);
+      editorMosaic.editors.set(filename, editor as any);
+
+      expect(editorMosaic.getEditorModel(filename)).toBe(model);
+    });
+
+    it('returns null if the editor does not exist', () => {
+      editorMosaic.editors.delete(filename);
+      expect(editorMosaic.getEditorModel(filename)).toBeNull();
+    });
+  });
+
+  describe('editor-layout', () => {
+    it('layout() calls editor.layout() only once', async () => {
+      const editor = new MonacoEditorMock();
+      const filename = DefaultEditorId.html;
+      await editorMosaic.editors.set(filename, editor as any);
+
+      editorMosaic.layout();
+      editorMosaic.layout();
+      editorMosaic.layout();
+      editorMosaic.layout();
+      await waitFor(() => editor.layout.mock.calls.length > 0);
+
+      expect(editor.layout).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('disposeLayoutAutorun()', () => {
+    it('automatically updates the layout when the mosaic arrangement changes', () => {
+      const spy = jest.spyOn(editorMosaic, 'layout');
+      editorMosaic.mosaicArrangement = DefaultEditorId.main;
+      expect(spy).toHaveBeenCalledTimes(1);
+      spy.mockRestore();
+    });
+  });
+});
diff --git a/tests/renderer/file-manager-spec.ts b/tests/renderer/file-manager-spec.ts
index 0eea7ac4..c28bc9fe 100644
--- a/tests/renderer/file-manager-spec.ts
+++ b/tests/renderer/file-manager-spec.ts
@@ -71,11 +71,11 @@ describe('FileManager', () => {
 
     it('does not do anything with incorrect inputs', async () => {
       await fm.openFiddle({} as any);
-      expect(app.setEditorValues).not.toHaveBeenCalled();
+      expect(app.replaceFiddle).not.toHaveBeenCalled();
     });
 
     it('does not do anything if cancelled', async () => {
-      (app.setEditorValues as jest.Mock).mockResolvedValueOnce(false);
+      (app.replaceFiddle as jest.Mock).mockResolvedValueOnce(false);
       await fm.openFiddle('/fake/path');
     });
   });
@@ -93,7 +93,7 @@ describe('FileManager', () => {
       const file = 'file.js';
       const content = 'hi';
       const values = { ...editorValues, [file]: content };
-      app.state.customMosaics = [file];
+      app.state.editorMosaic.customMosaics = [file];
       app.getEditorValues.mockReturnValueOnce(values);
 
       await fm.saveFiddle('/fake/path');
@@ -250,7 +250,7 @@ describe('FileManager', () => {
       const values = { ...editorValues, [file]: content };
       app.getEditorValues.mockReturnValue(values);
       expected.set(file, content);
-      app.state.customMosaics = [file];
+      app.state.editorMosaic.customMosaics = [file];
 
       expect(await fm.getFiles()).toStrictEqual(expected);
     });
diff --git a/tests/renderer/state-spec.ts b/tests/renderer/state-spec.ts
index 7fb86a4e..25f8c37f 100644
--- a/tests/renderer/state-spec.ts
+++ b/tests/renderer/state-spec.ts
@@ -1,6 +1,5 @@
 import {
   BlockableAccelerator,
-  DEFAULT_EDITORS,
   DefaultEditorId,
   ElectronReleaseChannel,
   GenericDialogType,
@@ -16,10 +15,6 @@ import {
   setupBinary,
 } from '../../src/renderer/binary';
 import { Bisector } from '../../src/renderer/bisect';
-import {
-  DEFAULT_MOSAIC_ARRANGEMENT,
-  SORTED_EDITORS,
-} from '../../src/renderer/constants';
 import { getTemplate, isContentUnchanged } from '../../src/renderer/content';
 import { ipcRendererManager } from '../../src/renderer/ipc';
 import { AppState } from '../../src/renderer/state';
@@ -28,7 +23,6 @@ import {
   getUpdatedElectronVersions,
   saveLocalVersions,
 } from '../../src/renderer/versions';
-import { createMosaicArrangement } from '../../src/utils/editors-mosaic-arrangement';
 import { waitFor } from '../../src/utils/wait-for';
 import { getName } from '../../src/utils/get-name';
 import { MonacoEditorMock, VersionsMock } from '../mocks/mocks';
@@ -600,108 +594,6 @@ describe('AppState', () => {
     });
   });
 
-  describe('getAndRemoveEditorValueBackup()', () => {
-    it('returns null if there is no backup', () => {
-      const result = appState.getAndRemoveEditorValueBackup(
-        DefaultEditorId.main,
-      );
-      expect(result).toEqual(undefined);
-    });
-
-    it('returns and deletes a backup if there is one', () => {
-      appState.closedPanels[DefaultEditorId.main] = { testBackup: true } as any;
-      const result = appState.getAndRemoveEditorValueBackup(
-        DefaultEditorId.main,
-      );
-      expect(result).toEqual({ testBackup: true });
-      expect(appState.closedPanels[DefaultEditorId.main]).toBeUndefined();
-    });
-  });
-
-  describe('setVisibleMosaics()', () => {
-    it('updates the visible editors and creates a backup', async () => {
-      appState.mosaicArrangement = createMosaicArrangement(DEFAULT_EDITORS);
-      appState.closedPanels = {};
-      appState.customMosaics = [];
-      await appState.setVisibleMosaics([DefaultEditorId.main]);
-
-      // we just need to mock something truthy here
-      const { editorMosaic } = (window as any).ElectronFiddle.app.state;
-      editorMosaic.editors.set(DefaultEditorId.main, {});
-
-      expect(appState.mosaicArrangement).toEqual(DefaultEditorId.main);
-      expect(appState.closedPanels[DefaultEditorId.renderer]).toBeTruthy();
-      expect(appState.closedPanels[DefaultEditorId.html]).toBeTruthy();
-      expect(appState.closedPanels[DefaultEditorId.main]).toBeUndefined();
-    });
-  });
-
-  describe('removeCustomMosaic()', () => {
-    it('removes a given custom mosaic', () => {
-      const file = 'file.js';
-      appState.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
-      appState.customMosaics = [file];
-
-      appState.removeCustomMosaic(file);
-
-      expect(appState.customMosaics).toEqual([]);
-    });
-  });
-
-  describe('hideAndBackupMosaic()', () => {
-    it('hides a given editor and creates a backup', () => {
-      appState.mosaicArrangement = DEFAULT_MOSAIC_ARRANGEMENT;
-      appState.closedPanels = {};
-      appState.hideAndBackupMosaic(SORTED_EDITORS[0]);
-
-      expect(appState.mosaicArrangement).toEqual({
-        direction: 'row',
-        first: SORTED_EDITORS[1],
-        second: {
-          direction: 'column',
-          first: SORTED_EDITORS[2],
-          second: SORTED_EDITORS[3],
-        },
-      });
-      expect(appState.closedPanels[DefaultEditorId.main]).toBeTruthy();
-      expect(appState.closedPanels[DefaultEditorId.renderer]).toBeUndefined();
-      expect(appState.closedPanels[DefaultEditorId.html]).toBeUndefined();
-    });
-  });
-
-  describe('showMosaic()', () => {
-    it('shows a given editor', () => {
-      appState.mosaicArrangement = DefaultEditorId.main;
-      appState.showMosaic(DefaultEditorId.html);
-
-      expect(appState.mosaicArrangement).toEqual({
-        direction: 'row',
-        first: DefaultEditorId.main,
-        second: DefaultEditorId.html,
-      });
-    });
-  });
-
-  describe('resetEditorLayout()', () => {
-    it('Puts editors in default arrangement', () => {
-      appState.hideAndBackupMosaic(SORTED_EDITORS[0]);
-
-      expect(appState.mosaicArrangement).toEqual({
-        direction: 'row',
-        first: SORTED_EDITORS[1],
-        second: {
-          direction: 'column',
-          first: SORTED_EDITORS[2],
-          second: SORTED_EDITORS[3],
-        },
-      });
-
-      appState.resetEditorLayout();
-
-      expect(appState.mosaicArrangement).toEqual(DEFAULT_MOSAIC_ARRANGEMENT);
-    });
-  });
-
   describe('blockAccelerators()', () => {
     it('adds an accelerator to be blocked', () => {
       appState.acceleratorsToBlock = [];
diff --git a/tests/utils.ts b/tests/utils.ts
index 81ea5bf7..827f50e8 100644
--- a/tests/utils.ts
+++ b/tests/utils.ts
@@ -1,3 +1,5 @@
+import { toJS } from 'mobx';
+
 const platform = process.platform;
 
 export function overridePlatform(value: NodeJS.Platform) {
@@ -51,3 +53,18 @@ export class FetchMock {
     });
   }
 }
+
+// return an object containing props in 'a' that are different from in 'b'
+export function objectDifference<Type>(a: Type, b: Type): Type {
+  const serialize = (input: any) => JSON.stringify(toJS(input));
+
+  const o = {};
+  for (const entry of Object.entries(a)) {
+    const key = entry[0];
+    const val = toJS(entry[1]);
+    if (serialize(val) == serialize(b[key])) continue;
+
+    o[key] = key === 'editorMosaic' ? objectDifference(val, b[key]) : toJS(val);
+  }
+  return o as Type;
+}
diff --git a/tests/utils/editor-backup-spec.ts b/tests/utils/editor-backup-spec.ts
deleted file mode 100644
index cec9c0dd..00000000
--- a/tests/utils/editor-backup-spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { DefaultEditorId } from '../../src/interfaces';
-import { getEditorBackup } from '../../src/utils/editor-backup';
-import { EditorMosaicMock } from '../mocks/mocks';
-
-describe('getEditorBackup()', () => {
-  let editorMosaic: EditorMosaicMock;
-
-  beforeEach(() => {
-    ({ editorMosaic } = (window as any).ElectronFiddle.app.state);
-  });
-
-  it('returns the value for an editor', () => {
-    const filename = DefaultEditorId.html;
-    const editor = editorMosaic.editors.get(filename);
-    const model = { testModel: true };
-    const viewState = { testViewState: true };
-    const value = 'editor-value';
-
-    (editor as any).model = model;
-    editor!.saveViewState.mockReturnValue(viewState);
-    editor!.value = value;
-
-    expect(getEditorBackup(filename)).toEqual({ model, value, viewState });
-  });
-});
diff --git a/tests/utils/editor-layout-spec.ts b/tests/utils/editor-layout-spec.ts
deleted file mode 100644
index d73b61db..00000000
--- a/tests/utils/editor-layout-spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { updateEditorLayout } from '../../src/utils/editor-layout';
-import { EditorMosaicMock } from '../mocks/mocks';
-
-describe('editor-layout', () => {
-  let editorMosaic: EditorMosaicMock;
-
-  beforeEach(() => {
-    ({ editorMosaic } = (window as any).ElectronFiddle.app.state);
-  });
-
-  it('updateEditorLayout calls layout() just once', (done) => {
-    updateEditorLayout();
-    updateEditorLayout();
-    updateEditorLayout();
-    updateEditorLayout();
-
-    setTimeout(() => {
-      for (const editor of editorMosaic.editors.values()) {
-        expect(editor.layout).toHaveBeenCalledTimes(1);
-      }
-      done();
-    }, 100);
-  });
-});
diff --git a/tests/utils/editor-model-spec.ts b/tests/utils/editor-model-spec.ts
deleted file mode 100644
index a45b40de..00000000
--- a/tests/utils/editor-model-spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { DefaultEditorId } from '../../src/interfaces';
-import { getEditorModel } from '../../src/utils/editor-model';
-import { EditorMosaicMock } from '../mocks/editor-mosaic';
-
-describe('getEditorModel()', () => {
-  const filename = DefaultEditorId.html;
-  let editorMosaic: EditorMosaicMock;
-
-  beforeEach(() => {
-    ({ editorMosaic } = (window as any).ElectronFiddle.app.state);
-  });
-
-  it('returns the value for an editor', () => {
-    const model = { testModel: true };
-    const editor = editorMosaic.editors.get(filename);
-    editor!.getModel.mockReturnValue(model);
-
-    expect(getEditorModel(filename)).toBe(model);
-  });
-
-  it('returns null if the editor does not exist', () => {
-    editorMosaic.editors.delete(filename);
-    expect(getEditorModel(filename)).toBeNull();
-  });
-
-  it('returns null if window.Fiddle is not ready', () => {
-    (window as any).ElectronFiddle = undefined;
-    expect(getEditorModel(filename)).toBeNull();
-  });
-});
diff --git a/tests/utils/editor-value-spec.ts b/tests/utils/editor-value-spec.ts
deleted file mode 100644
index f04aeb33..00000000
--- a/tests/utils/editor-value-spec.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { DefaultEditorId } from '../../src/interfaces';
-import { getEditorValue } from '../../src/utils/editor-value';
-import { AppMock, EditorMosaicMock, StateMock } from '../mocks/mocks';
-
-describe('getEditorValue()', () => {
-  const filename = DefaultEditorId.html;
-  const value = 'editor-value';
-  let app: AppMock;
-  let state: StateMock;
-  let editorMosaic: EditorMosaicMock;
-
-  beforeEach(() => {
-    ({ app } = (window as any).ElectronFiddle);
-    ({ state } = app);
-    ({ editorMosaic } = state);
-
-    for (const editor of editorMosaic.editors.values()) {
-      editor.getValue.mockReturnValue(value);
-    }
-  });
-
-  it('returns the value for an editor if it exists', () => {
-    expect(getEditorValue(filename)).toBe(value);
-  });
-
-  it('returns the value for the editor backup if it exists', () => {
-    // set up mock state that has the editor deleted and a backup
-    editorMosaic.editors.delete(filename);
-    const value = 'editor-backup-value';
-    state.closedPanels = { [filename]: { value } };
-
-    expect(getEditorValue(filename)).toBe(value);
-  });
-
-  it('returns an empty string if the editor does not exist', () => {
-    editorMosaic.editors.delete(filename);
-    expect(getEditorValue(filename)).toBe('');
-  });
-});
diff --git a/tests/utils/editor-viewstate-spec.ts b/tests/utils/editor-viewstate-spec.ts
deleted file mode 100644
index 61944909..00000000
--- a/tests/utils/editor-viewstate-spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { DefaultEditorId } from '../../src/interfaces';
-import { getEditorViewState } from '../../src/utils/editor-viewstate';
-import { EditorMosaicMock } from '../mocks/mocks';
-
-describe('getEditorViewState()', () => {
-  const filename = DefaultEditorId.html;
-  let editorMosaic: EditorMosaicMock;
-
-  beforeEach(() => {
-    ({ editorMosaic } = (window as any).ElectronFiddle.app.state);
-  });
-
-  it('returns the value for an editor', () => {
-    const viewState = { testViewState: true };
-    editorMosaic.editors
-      .get(filename)!
-      .saveViewState.mockReturnValue(viewState);
-    expect(getEditorViewState(filename)).toBe(viewState);
-  });
-
-  it('returns null if the editor does not exist', () => {
-    editorMosaic.editors.delete(filename);
-    expect(getEditorViewState(filename)).toEqual(null);
-  });
-
-  it('throws if window.Fiddle is not ready', () => {
-    (window as any).ElectronFiddle = undefined;
-    expect(getEditorViewState(filename)).toBeNull();
-  });
-});